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

Compare commits

..

1 Commits

Author SHA1 Message Date
Albert Vaca
4b82a2ef3f Commented BatteryPlugin for release
It needs a patch in kdelibs to work.
2013-09-10 11:59:43 +02:00
737 changed files with 5072 additions and 62751 deletions

1
.gitattributes vendored
View File

@@ -1 +0,0 @@
*.bat text eol=crlf

19
.gitignore vendored
View File

@@ -1,17 +1,4 @@
.attach_pid*
local.properties
/.gradle/
/.idea/
/out/
/gen/
/bin/
/build/
/target/
/classes/
*.iml
*.keystore
!debug.keystore
.directory
GPUCache/
/release/
/.kotlin/
.gradle
.idea
out

View File

@@ -1,68 +0,0 @@
# SPDX-FileCopyrightText: 2023 Nicolas Fella <nicolas.fella@gmx.de>
# SPDX-FileCopyrightText: 2023 Simon Redman <simon@ergotech.com>
# SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#
# Test this pipeline locally by:
# - Set up Docker on your machine, if not already set up
# - Set up gitlab-runner, as described here: https://stackoverflow.com/a/52724374
# - Run `gitlab-runner exec docker --docker-privileged assembleDebug`
variables:
ANDROID_COMPILE_SDK: "33"
ANDROID_BUILD_TOOLS: "34.0.0"
ANDROID_SDK_TOOLS: "10406996"
default:
tags:
- Linux
image: ubuntu:22.04
before_script:
# Prepare system for use of sdkmanager
- apt-get --quiet update --yes
- apt-get --quiet install --yes wget tar unzip lib32stdc++6 lib32z1 openjdk-17-jdk-headless
- wget --quiet --output-document=android-sdk.zip https://dl.google.com/android/repository/commandlinetools-linux-10406996_latest.zip
# Create the directory structure around cmdline-tools that sdkmanager is expecting
- mkdir -p sdk/cmdline-tools
- unzip android-sdk.zip
- mv cmdline-tools sdk/cmdline-tools/latest
# Update the environment
- export ANDROID_HOME=$PWD/sdk
- export PATH=$PATH:$PWD/sdk/cmdline-tools/latest/bin
# (this line should fail if sdkmanager is moved out of that directory in the future)
- type sdkmanager
# Install SDK packages
- echo y | sdkmanager "platforms;android-${ANDROID_COMPILE_SDK}" >/dev/null
- echo y | sdkmanager "platform-tools" >/dev/null
- echo y | sdkmanager "emulator" >/dev/null
- echo y | sdkmanager "platform-tools" >/dev/null
- echo y | sdkmanager "tools" >/dev/null
- echo y | sdkmanager "build-tools;${ANDROID_BUILD_TOOLS}" >/dev/null
# temporarily disable checking for EPIPE error and use yes to accept all licenses
- set +o pipefail
- yes | sdkmanager --licenses
# Re-enable checking for EPIPE and allow execution of Gradle wrapper script
- set -o pipefail
- chmod +x ./gradlew
stages:
- build
- test
lintDebug:
stage: build
script:
- ./gradlew generateLicenseReport
- ./gradlew -Pci --console=plain lintDebug -PbuildDir=lint
assembleDebug:
stage: build
script:
- ./gradlew assembleDebug
artifacts:
paths:
- build/outputs/
debugTests:
stage: test
script:
- ./gradlew -Pci --console=plain testDebug

View File

@@ -1,32 +0,0 @@
## Summary
Add a description of your merge request here. What does your new feature do?
Describe in detail what your patch does, why it does that, etc. Merge requests
without an adequate description are difficult to review, and probably we will
ask for more information!
Please also keep this description up-to-date with any discussion that takes
place so that reviewers can understand your intent. This is especially
important if they didn't participate in the discussion.
Make sure to remove this comment when you are done.
Fill in the following lines as appropriate to automatically close GitLab issue or Bugzilla bugs
Fixes <!-- Gitlab Issue Number -->
BUG: <!-- bugzilla bug -->
## Test Plan
### Before:
Add a quick discription of the (buggy) behavior of the app before this fix
This section does not need to be too detailed because it should mostly be
covered by the bug report and the summary. Just share the steps for how to
reproduce the bug.
### After:
Add a more detailed description of how to exercise the new behavior, showing
that the bug has been fixed. If any other behavior has been changed, share
the steps to verify that the new behavior doesn't have any regressions.
/label ~bugfix

View File

@@ -1,27 +0,0 @@
## Summary
Add a description of your merge request here. What does your new feature do?
Describe in detail what your patch does, why it does that, etc. Merge requests
without an adequate description are difficult to review, and probably we will
ask for more information!
Please also keep this description up-to-date with any discussion that takes
place so that reviewers can understand your intent. This is especially
important if they didn't participate in the discussion.
Make sure to remove this comment when you are done.
Implements <!-- GitLab Issue Number -->
## Test Plan
Add a description of how to test your patch here. Tell us how to use the new
feature and what we should be seeing. If applicable, it is great to include
screenshots, either here or in the Summary section.
It can be difficult to understand a new feature from the text description in
the summary, so put enough detail here that so that we can understand how to run
the new feature and we can play with it ourselves to understand it.
/label ~feature

View File

@@ -1,462 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
SPDX-FileCopyrightText: 2023 Albert Vaca Cintora <albertvaka@gmail.com>
SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-feature
android:name="android.hardware.telephony"
android:required="false" />
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false" />
<uses-feature
android:name="android.software.leanback"
android:required="false" />
<uses-feature
android:name="android.hardware.bluetooth"
android:required="false" />
<uses-feature
android:name="android.hardware.location.gps"
android:required="false" />
<uses-feature
android:name="android.hardware.microphone"
android:required="false" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.READ_CALL_LOG" />
<uses-permission android:name="android.permission.SEND_SMS" />
<uses-permission android:name="android.permission.READ_SMS" />
<uses-permission android:name="android.permission.RECEIVE_SMS" />
<uses-permission android:name="android.permission.RECEIVE_MMS" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" tools:ignore="QueryAllPackagesPermission" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission android:name="android.permission.READ_LOGS" tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:label="@string/kde_connect"
android:banner="@mipmap/ic_launcher_banner"
android:supportsRtl="true"
android:allowBackup="false"
android:dataExtractionRules="@xml/data_extraction_rules"
android:networkSecurityConfig="@xml/network_security_config"
android:theme="@style/KdeConnectTheme.NoActionBar"
android:name="org.kde.kdeconnect.KdeConnect"
android:enableOnBackInvokedCallback="true"
android:requestLegacyExternalStorage="true"> <!-- requestLegacyExternalStorage is only used in Android 10: https://developer.android.com/training/data-storage/use-cases#opt-out-in-production-app -->
<receiver
android:name="com.android.mms.transaction.PushReceiver"
android:exported="true"
android:permission="android.permission.BROADCAST_WAP_PUSH">
<intent-filter>
<action android:name="android.provider.Telephony.WAP_PUSH_DELIVER" />
<data android:mimeType="application/vnd.wap.mms-message" />
</intent-filter>
</receiver>
<service
android:name="com.android.mms.transaction.TransactionService"
android:enabled="true"
android:exported="true" />
<service
android:name="org.kde.kdeconnect.BackgroundService"
android:foregroundServiceType="connectedDevice"
android:icon="@drawable/icon"
android:enabled="true" />
<service
android:name="org.kde.kdeconnect.Plugins.RemoteKeyboardPlugin.RemoteKeyboardService"
android:label="@string/remote_keyboard_service"
android:exported="true"
android:permission="android.permission.BIND_INPUT_METHOD">
<intent-filter>
<action android:name="android.view.InputMethod" />
</intent-filter>
<meta-data
android:name="android.view.im"
android:resource="@xml/remotekeyboardplugin_method" />
</service>
<activity
android:name="org.kde.kdeconnect.UserInterface.MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
</intent-filter>
<meta-data android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
</activity>
<activity
android:name="org.kde.kdeconnect.UserInterface.PluginSettingsActivity"
android:label="@string/device_menu_plugins"
android:exported="false"
android:parentActivityName="org.kde.kdeconnect.UserInterface.MainActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.kde.kdeconnect.UserInterface.MainActivity" />
</activity>
<activity
android:name="org.kde.kdeconnect.UserInterface.CustomDevicesActivity"
android:label="@string/custom_devices_settings"
android:exported="false"
android:parentActivityName="org.kde.kdeconnect.UserInterface.MainActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.kde.kdeconnect.UserInterface.MainActivity" />
</activity>
<activity
android:name="org.kde.kdeconnect.Plugins.SharePlugin.SendFileActivity"
android:label="KDE Connect"
android:exported="true"
android:parentActivityName="org.kde.kdeconnect.UserInterface.MainActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.kde.kdeconnect.UserInterface.MainActivity" />
</activity>
<receiver android:name="org.kde.kdeconnect.KdeConnectBroadcastReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MY_PACKAGE_REPLACED"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
<intent-filter>
<action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
</intent-filter>
<intent-filter>
<action android:name="android.net.wifi.WIFI_STATE_CHANGED" />
</intent-filter>
</receiver>
<activity
android:name="org.kde.kdeconnect.Plugins.FindMyPhonePlugin.FindMyPhoneActivity"
android:configChanges="orientation|screenSize"
android:excludeFromRecents="true"
android:label="@string/findmyphone_title"
android:launchMode="singleInstance" />
<receiver android:name="org.kde.kdeconnect.Plugins.FindMyPhonePlugin.FindMyPhoneReceiver"
android:exported="true">
<intent-filter>
<action android:name="org.kde.kdeconnect.Plugins.FindMyPhonePlugin.foundIt" />
</intent-filter>
</receiver>
<!-- Plugin-related activities and services -->
<activity
android:name="org.kde.kdeconnect.Plugins.ClibpoardPlugin.ClipboardFloatingActivity"
android:theme="@style/Theme.Transparent"
android:excludeFromRecents="true"/>
<activity
android:name="org.kde.kdeconnect.Plugins.MprisPlugin.MprisActivity"
android:label="@string/open_mpris_controls"
android:launchMode="singleTop"
android:parentActivityName="org.kde.kdeconnect.UserInterface.MainActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.kde.kdeconnect.UserInterface.MainActivity" />
</activity>
<receiver android:name="org.kde.kdeconnect.Plugins.MprisPlugin.MprisMediaNotificationReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>
<activity
android:name="org.kde.kdeconnect.Plugins.RunCommandPlugin.RunCommandActivity"
android:label="@string/pref_plugin_runcommand"
android:exported="false"
android:parentActivityName="org.kde.kdeconnect.UserInterface.MainActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.kde.kdeconnect.UserInterface.MainActivity" />
</activity>
<activity
android:name="org.kde.kdeconnect.Plugins.RunCommandPlugin.RunCommandWidgetConfigActivity"
android:excludeFromRecents="true"
android:label="@string/pref_plugin_runcommand"
android:launchMode="singleTask"
android:noHistory="true"
android:screenOrientation="user"/>
<service
android:name="org.kde.kdeconnect.Plugins.RunCommandPlugin.CommandsRemoteViewsService"
android:exported="false"
android:permission="android.permission.BIND_REMOTEVIEWS" />
<receiver
android:name="org.kde.kdeconnect.Plugins.RunCommandPlugin.RunCommandWidgetProvider"
android:label="@string/pref_plugin_runcommand"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<intent-filter>
<action android:name="RUN_COMMAND_ACTION" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/remotecommandplugin_widget" />
</receiver>
<activity android:name="org.kde.kdeconnect.Plugins.RunCommandPlugin.RunCommandUrlActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="runcommand"
android:scheme="kdeconnect" />
</intent-filter>
</activity>
<activity
android:name="org.kde.kdeconnect.Plugins.BigscreenPlugin.BigscreenActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
android:label="@string/pref_plugin_bigscreen"
android:exported="true"
android:parentActivityName="org.kde.kdeconnect.UserInterface.MainActivity"
android:windowSoftInputMode="stateHidden|adjustResize">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.kde.kdeconnect.UserInterface.MainActivity" />
</activity>
<activity
android:name="org.kde.kdeconnect.Plugins.MousePadPlugin.MousePadActivity"
android:label="@string/pref_plugin_mousepad"
android:exported="false"
android:launchMode="singleTop"
android:parentActivityName="org.kde.kdeconnect.UserInterface.MainActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.kde.kdeconnect.UserInterface.MainActivity" />
</activity>
<activity
android:name="org.kde.kdeconnect.Plugins.MousePadPlugin.ComposeSendActivity"
android:label="@string/compose_send_title"
android:exported="false"
android:parentActivityName="org.kde.kdeconnect.Plugins.MousePadPlugin.MousePadActivity"
android:windowSoftInputMode="adjustResize">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.kde.kdeconnect.Plugins.MousePadPlugin.MousePadActivity" />
</activity>
<activity
android:name="org.kde.kdeconnect.Plugins.MousePadPlugin.SendKeystrokesToHostActivity"
android:label="@string/pref_plugin_mousepad_send_keystrokes"
android:exported="true"
android:parentActivityName="org.kde.kdeconnect.UserInterface.MainActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.kde.kdeconnect.UserInterface.MainActivity" />
<!-- Accept data with "text/x-keystrokes" to send the text to the connected host and emulate keystrokes -->
<intent-filter>
<action android:name="android.intent.action.SEND"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="text/x-keystrokes"/>
</intent-filter>
</activity>
<activity
android:name="org.kde.kdeconnect.Plugins.PresenterPlugin.PresenterActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
android:label="@string/pref_plugin_presenter"
android:exported="false"
android:parentActivityName="org.kde.kdeconnect.UserInterface.MainActivity"
android:windowSoftInputMode="stateHidden|adjustResize">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.kde.kdeconnect.UserInterface.MainActivity" />
</activity>
<activity
android:name="org.kde.kdeconnect.Plugins.SharePlugin.ShareActivity"
android:exported="true"
android:label="@string/manifest_label_share">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="*/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="*/*" />
</intent-filter>
<meta-data
android:name="android.service.chooser.chooser_target_service"
android:value="org.kde.kdeconnect.Plugins.SharePlugin.ShareChooserTargetService" />
</activity>
<receiver android:name="org.kde.kdeconnect.Plugins.SharePlugin.ShareBroadcastReceiver"
android:exported="true">
<intent-filter>
<action android:name="org.kde.kdeconnect.Plugins.SharePlugin.CancelShare" />
</intent-filter>
</receiver>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/fileprovider_paths" />
</provider>
<service
android:name="org.kde.kdeconnect.Plugins.NotificationsPlugin.NotificationReceiver"
android:exported="true"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
<intent-filter>
<action android:name="android.service.notification.NotificationListenerService" />
</intent-filter>
</service>
<service
android:name="org.kde.kdeconnect.Plugins.SharePlugin.ShareChooserTargetService"
android:exported="true"
android:permission="android.permission.BIND_CHOOSER_TARGET_SERVICE">
<intent-filter>
<action android:name="android.service.chooser.ChooserTargetService" />
</intent-filter>
</service>
<service
android:name="org.kde.kdeconnect.Plugins.MouseReceiverPlugin.MouseReceiverService"
android:exported="true"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>
<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/mouse_receiver_service" />
</service>
<activity
android:name="org.kde.kdeconnect.Plugins.NotificationsPlugin.NotificationFilterActivity"
android:label="@string/title_activity_notification_filter"
android:parentActivityName="org.kde.kdeconnect.UserInterface.PluginSettingsActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.kde.kdeconnect.UserInterface.PluginSettingsActivity" />
</activity>
<activity
android:name="org.kde.kdeconnect.UserInterface.TrustedNetworksActivity"
android:label="@string/trusted_networks"
android:parentActivityName="org.kde.kdeconnect.UserInterface.MainActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.kde.kdeconnect.UserInterface.MainActivity" />
</activity>
<activity
android:name="org.kde.kdeconnect.UserInterface.About.EasterEggActivity"
android:label="@string/easter_egg"
android:parentActivityName="org.kde.kdeconnect.UserInterface.MainActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.kde.kdeconnect.UserInterface.MainActivity" />
</activity>
<activity
android:name="org.kde.kdeconnect.UserInterface.About.AboutKDEActivity"
android:label="@string/about_kde"
android:parentActivityName="org.kde.kdeconnect.UserInterface.MainActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.kde.kdeconnect.UserInterface.MainActivity" />
</activity>
<activity
android:name="org.kde.kdeconnect.UserInterface.About.LicensesActivity"
android:label="@string/licenses"
android:parentActivityName="org.kde.kdeconnect.UserInterface.MainActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.kde.kdeconnect.UserInterface.MainActivity" />
</activity>
<service
android:name="org.kde.kdeconnect.Plugins.ClibpoardPlugin.ClipboardTileService"
android:icon="@drawable/ic_baseline_content_paste_24"
android:exported="true"
android:label="@string/send_clipboard"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
</service>
<service android:name="org.kde.kdeconnect.Plugins.RunCommandPlugin.RunCommandControlsProviderService"
android:label="@string/kde_connect"
android:exported="true"
android:permission="android.permission.BIND_CONTROLS">
<intent-filter>
<action android:name="android.service.controls.ControlsProviderService" />
</intent-filter>
</service>
</application>
</manifest>

339
COPYING
View File

@@ -1,339 +0,0 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
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) any later version.
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, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.

1
KdeConnect/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
build/

79
KdeConnect/KdeConnect.iml Normal file
View File

@@ -0,0 +1,79 @@
<?xml version="1.0" encoding="UTF-8"?>
<module external.linked.project.path="$MODULE_DIR$" external.system.id="GRADLE" type="JAVA_MODULE" version="4">
<component name="FacetManager">
<facet type="android" name="Android">
<configuration>
<option name="SELECTED_BUILD_VARIANT" value="Debug" />
<option name="ASSEMBLE_TASK_NAME" value="assembleDebug" />
<option name="ASSEMBLE_TEST_TASK_NAME" value="assembleTest" />
<option name="SOURCE_GEN_TASK_NAME" value="TODO" />
<option name="ALLOW_USER_CONFIGURATION" value="false" />
<option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" />
<option name="RES_FOLDER_RELATIVE_PATH" value="/src/main/res" />
<option name="RES_FOLDERS_RELATIVE_PATH" value="file://$MODULE_DIR$/src/main/res" />
<option name="ASSETS_FOLDER_RELATIVE_PATH" value="/src/main/assets" />
</configuration>
</facet>
<facet type="android-gradle" name="Android-Gradle">
<configuration>
<option name="GRADLE_PROJECT_PATH" value=":KdeConnect" />
</configuration>
</facet>
</component>
<component name="NewModuleRootManager" inherit-compiler-output="false">
<output url="file://$MODULE_DIR$/build/classes/debug" />
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/build/source/r/debug" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/build/source/aidl/debug" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/build/source/rs/debug" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/build/source/buildConfig/debug" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/build/res/rs/debug" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/build/source/r/test" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/build/source/aidl/test" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/build/source/rs/test" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/build/source/buildConfig/test" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/build/res/rs/test" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/aidl" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/assets" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/jni" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/rs" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/res" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/resources" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/aidl" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/assets" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/jni" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/rs" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/res" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/resources" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/instrumentTest/aidl" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/instrumentTest/assets" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/instrumentTest/java" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/instrumentTest/jni" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/instrumentTest/rs" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/instrumentTest/res" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/instrumentTest/resources" isTestSource="true" />
<excludeFolder url="file://$MODULE_DIR$/build/apk" />
<excludeFolder url="file://$MODULE_DIR$/build/assets" />
<excludeFolder url="file://$MODULE_DIR$/build/bundles" />
<excludeFolder url="file://$MODULE_DIR$/build/classes" />
<excludeFolder url="file://$MODULE_DIR$/build/dependency-cache" />
<excludeFolder url="file://$MODULE_DIR$/build/exploded-bundles" />
<excludeFolder url="file://$MODULE_DIR$/build/incremental" />
<excludeFolder url="file://$MODULE_DIR$/build/libs" />
<excludeFolder url="file://$MODULE_DIR$/build/manifests" />
<excludeFolder url="file://$MODULE_DIR$/build/symbols" />
<excludeFolder url="file://$MODULE_DIR$/build/tmp" />
</content>
<orderEntry type="jdk" jdkName="Android 4.3 Platform" jdkType="Android SDK" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" exported="" name="android-support-v4" level="project" />
<orderEntry type="library" exported="" name="mina-core-2.0.7" level="project" />
<orderEntry type="library" exported="" name="slf4j-api-1.6.6" level="project" />
<orderEntry type="library" exported="" name="support-v4-18.0.0" level="project" />
<orderEntry type="library" exported="" name="ComAndroidSupportAppcompatV71800.aar" level="project" />
</component>
</module>

26
KdeConnect/build.gradle Normal file
View File

@@ -0,0 +1,26 @@
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:0.5.+'
}
}
apply plugin: 'android'
dependencies {
compile "com.android.support:appcompat-v7:18.0.+"
compile files('libs/android-support-v4.jar')
compile files('libs/mina-core-2.0.7.jar')
compile files('libs/slf4j-api-1.6.6.jar')
}
android {
compileSdkVersion 18
buildToolsVersion "18.0.1"
defaultConfig {
minSdkVersion 9
targetSdkVersion 18
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,129 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.kde.kdeconnect_tp"
android:versionCode="4"
android:versionName="0.2.1">
<uses-sdk android:minSdkVersion="9"
android:targetSdkVersion="18" />
<supports-screens
android:smallScreens="true"
android:normalScreens="true"
android:largeScreens="true"
android:xlargeScreens="true"
android:anyDensity="true"
/>
<uses-feature android:name="android.hardware.telephony" android:required="false" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE" android:required="false"/>
<uses-permission android:name="android.permission.BATTERY_STATS" />
<uses-permission android:name="android.permission.RECEIVE_SMS" android:required="false"/>
<uses-permission android:name="android.permission.READ_CONTACTS" />
<application
android:allowBackup="true"
android:icon="@drawable/icon"
android:label="KDE Connect"
>
<activity
android:theme="@style/Theme.AppCompat"
android:name="org.kde.kdeconnect.UserInterface.MainActivity"
android:label="KDE Connect" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:theme="@style/Theme.AppCompat"
android:name="org.kde.kdeconnect.UserInterface.DeviceActivity"
android:label="@string/device"
android:parentActivityName="org.kde.connect.UserInterface.MainActivity"
>
<meta-data android:name="android.support.PARENT_ACTIVITY"
android:value="org.kde.connect.UserInterface.MainActivity" />
</activity>
<activity
android:theme="@style/Theme.AppCompat"
android:name="org.kde.kdeconnect.UserInterface.PairActivity"
android:label="@string/pair_device"
android:parentActivityName="org.kde.connect.UserInterface.MainActivity"
>
<meta-data android:name="android.support.PARENT_ACTIVITY"
android:value="org.kde.connect.UserInterface.MainActivity" />
</activity>
<activity
android:theme="@style/Theme.AppCompat"
android:name="org.kde.kdeconnect.Plugins.MprisPlugin.MprisActivity"
android:label="@string/remote_control"
>
<meta-data android:name="android.support.PARENT_ACTIVITY"
android:value=".DeviceActivity" />
</activity>
<activity
android:name="org.kde.kdeconnect.UserInterface.SettingsActivity"
android:label="@string/settings"
>
<meta-data android:name="android.support.PARENT_ACTIVITY"
android:value=".DeviceActivity" />
</activity>
<service
android:enabled="true"
android:name="org.kde.kdeconnect.BackgroundService">
</service>
<service android:name="org.kde.kdeconnect.Plugins.NotificationsPlugin.NotificationReceiver"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
<intent-filter>
<action android:name="android.service.notification.NotificationListenerService" />
</intent-filter>
</service>
<receiver android:name="org.kde.kdeconnect.KdeConnectBroadcastReceiver">
<intent-filter>
<action android:name="android.intent.action.PACKAGE_REPLACED" />
<data android:scheme="package" android:path="org.kde.kdeconnect" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
<intent-filter>
<action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
</intent-filter>
<intent-filter>
<action android:name="android.net.wifi.WIFI_STATE_CHANGED" />
</intent-filter>
<!--<intent-filter>
<action android:name="android.provider.Telephony.SMS_RECEIVED" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.PHONE_STATE" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.BATTERY_CHANGED" />
</intent-filter>-->
</receiver>
</application>
</manifest>

View File

@@ -0,0 +1,296 @@
package org.kde.kdeconnect;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.os.Binder;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.util.Base64;
import android.util.Log;
import org.kde.kdeconnect.ComputerLinks.BaseComputerLink;
import org.kde.kdeconnect.LinkProviders.BaseLinkProvider;
import org.kde.kdeconnect.LinkProviders.LanLinkProvider;
import org.kde.kdeconnect.LinkProviders.LoopbackLinkProvider;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Set;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class BackgroundService extends Service {
private ArrayList<BaseLinkProvider> linkProviders = new ArrayList<BaseLinkProvider>();
private HashMap<String, Device> devices = new HashMap<String, Device>();
private Device.PairingCallback devicePairingCallback = new Device.PairingCallback() {
@Override
public void incomingRequest() {
if (deviceListChangedCallback != null) deviceListChangedCallback.onDeviceListChanged();
}
@Override
public void pairingSuccessful() {
if (deviceListChangedCallback != null) deviceListChangedCallback.onDeviceListChanged();
}
@Override
public void pairingFailed(String error) {
if (deviceListChangedCallback != null) deviceListChangedCallback.onDeviceListChanged();
}
@Override
public void unpaired() {
if (deviceListChangedCallback != null) deviceListChangedCallback.onDeviceListChanged();
}
};
private void loadRememberedDevicesFromSettings() {
SharedPreferences preferences = getSharedPreferences("trusted_devices", Context.MODE_PRIVATE);
Set<String> trustedDevices = preferences.getAll().keySet();
for(String deviceId : trustedDevices) {
if (preferences.getBoolean(deviceId, false)) {
Device device = new Device(getBaseContext(), deviceId);
devices.put(deviceId,device);
device.addPairingCallback(devicePairingCallback);
}
}
}
public void registerLinkProviders() {
SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(this);
//if (settings.getBoolean("loopback_link", true)) {
// linkProviders.add(new LoopbackLinkProvider(this));
//}
if (settings.getBoolean("lan_link", true)) {
linkProviders.add(new LanLinkProvider(this));
}
}
public Device getDevice(String id) {
return devices.get(id);
}
private BaseLinkProvider.ConnectionReceiver deviceListener = new BaseLinkProvider.ConnectionReceiver() {
@Override
public void onConnectionReceived(final NetworkPackage identityPackage, final BaseComputerLink link) {
Log.i("BackgroundService", "Connection accepted!");
String deviceId = identityPackage.getString("deviceId");
Device device = devices.get(deviceId);
if (device != null) {
Log.i("BackgroundService", "addLink, known device: " + deviceId);
device.addLink(link);
} else {
Log.i("BackgroundService", "addLink,unknown device: " + deviceId);
String name = identityPackage.getString("deviceName");
device = new Device(getBaseContext(), deviceId, name, link);
devices.put(deviceId, device);
device.addPairingCallback(devicePairingCallback);
}
if (deviceListChangedCallback != null) deviceListChangedCallback.onDeviceListChanged();
}
@Override
public void onConnectionLost(BaseComputerLink link) {
Device d = devices.get(link.getDeviceId());
Log.i("onConnectionLost", "removeLink, deviceId: " + link.getDeviceId());
if (d != null) {
d.removeLink(link);
if (!d.isReachable() && !d.isPaired()) {
devices.remove(link.getDeviceId());
d.removePairingCallback(devicePairingCallback);
}
} else {
Log.e("onConnectionLost","Removing connection to unknown device, this should not happen");
}
if (deviceListChangedCallback != null) deviceListChangedCallback.onDeviceListChanged();
}
};
public HashMap<String, Device> getDevices() {
return devices;
}
public void startDiscovery() {
Log.i("BackgroundService","StartDiscovery");
for (BaseLinkProvider a : linkProviders) {
a.onStart();
}
}
public void stopDiscovery() {
Log.i("BackgroundService","StopDiscovery");
for (BaseLinkProvider a : linkProviders) {
a.onStop();
}
}
public void onNetworkChange() {
Log.i("BackgroundService","OnNetworkChange");
for (BaseLinkProvider a : linkProviders) {
a.onNetworkChange();
}
}
public void addConnectionListener(BaseLinkProvider.ConnectionReceiver cr) {
Log.i("BackgroundService","Registering connection listener");
for (BaseLinkProvider a : linkProviders) {
a.addConnectionReceiver(cr);
}
}
public void removeConnectionListener(BaseLinkProvider.ConnectionReceiver cr) {
for (BaseLinkProvider a : linkProviders) {
a.removeConnectionReceiver(cr);
}
}
public interface DeviceListChangedCallback {
void onDeviceListChanged();
}
private DeviceListChangedCallback deviceListChangedCallback = null;
public void setDeviceListChangedCallback(DeviceListChangedCallback callback) {
this.deviceListChangedCallback = callback;
}
//This will called only once, even if we launch the service intent several times
@Override
public void onCreate() {
super.onCreate();
// Register screen on listener
IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_ON);
registerReceiver(new KdeConnectBroadcastReceiver(), filter);
Log.i("BackgroundService","Service not started yet, initializing...");
initializeRsaKeys();
loadRememberedDevicesFromSettings();
registerLinkProviders();
//Link Providers need to be already registered
addConnectionListener(deviceListener);
startDiscovery();
}
private void initializeRsaKeys() {
SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(this);
if (!settings.contains("publicKey") || !settings.contains("privateKey")) {
KeyPair keyPair;
try {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
keyGen.initialize(2048);
keyPair = keyGen.genKeyPair();
} catch(Exception e) {
e.printStackTrace();
Log.e("initializeRsaKeys","Exception");
return;
}
byte[] publicKey = keyPair.getPublic().getEncoded();
byte[] privateKey = keyPair.getPrivate().getEncoded();
SharedPreferences.Editor edit = settings.edit();
edit.putString("publicKey",Base64.encodeToString(publicKey, 0).trim()+"\n");
edit.putString("privateKey",Base64.encodeToString(privateKey, 0));
edit.commit();
}
/*
// Encryption and decryption test
//================================
try {
NetworkPackage np = NetworkPackage.createIdentityPackage(this);
SharedPreferences globalSettings = PreferenceManager.getDefaultSharedPreferences(this);
byte[] publicKeyBytes = Base64.decode(globalSettings.getString("publicKey",""), 0);
PublicKey publicKey = KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(publicKeyBytes));
np.encrypt(publicKey);
byte[] privateKeyBytes = Base64.decode(globalSettings.getString("privateKey",""), 0);
PrivateKey privateKey = KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(privateKeyBytes));
NetworkPackage decrypted = np.decrypt(privateKey);
Log.e("ENCRYPTION AND DECRYPTION TEST", decrypted.serialize());
} catch (Exception e) {
e.printStackTrace();
Log.e("ENCRYPTION AND DECRYPTION TEST","Exception: "+e);
}
*/
}
@Override
public void onDestroy() {
Log.i("BackgroundService", "Destroying");
stopDiscovery();
super.onDestroy();
}
@Override
public IBinder onBind (Intent intent) {
return new Binder();
}
//To use the service from the gui
public interface InstanceCallback {
void onServiceStart(BackgroundService service);
}
private static ArrayList<InstanceCallback> callbacks = new ArrayList<InstanceCallback>();
private static final Lock mutex = new ReentrantLock(true);
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
//This will be called for each intent launch, even if the service is already started and it is reused
Log.i("BackgroundService","onStartCommand");
mutex.lock();
for (InstanceCallback c : callbacks) {
c.onServiceStart(this);
}
callbacks.clear();
mutex.unlock();
return Service.START_STICKY;
}
public static void Start(Context c) {
RunCommand(c, null);
}
public static void RunCommand(Context c, final InstanceCallback callback) {
if (callback != null) {
mutex.lock();
callbacks.add(callback);
mutex.unlock();
}
Intent serviceIntent = new Intent(c, BackgroundService.class);
c.startService(serviceIntent);
}
}

View File

@@ -0,0 +1,50 @@
package org.kde.kdeconnect.ComputerLinks;
import org.kde.kdeconnect.LinkProviders.BaseLinkProvider;
import org.kde.kdeconnect.NetworkPackage;
import java.util.ArrayList;
public abstract class BaseComputerLink {
private BaseLinkProvider linkProvider;
private String deviceId;
private ArrayList<PackageReceiver> receivers = new ArrayList<PackageReceiver>();
protected BaseComputerLink(String deviceId, BaseLinkProvider linkProvider) {
this.linkProvider = linkProvider;
this.deviceId = deviceId;
}
public String getDeviceId() {
return deviceId;
}
public BaseLinkProvider getLinkProvider() {
return linkProvider;
}
public interface PackageReceiver {
public void onPackageReceived(NetworkPackage np);
}
public void addPackageReceiver(PackageReceiver pr) {
receivers.add(pr);
}
public void removePackageReceiver(PackageReceiver pr) {
receivers.remove(pr);
}
//Should be called from a background thread listening to packages
protected void packageReceived(NetworkPackage np) {
for(PackageReceiver pr : receivers) {
pr.onPackageReceived(np);
}
}
//TO OVERRIDE
public abstract boolean sendPackage(NetworkPackage np);
}

View File

@@ -0,0 +1,37 @@
package org.kde.kdeconnect.ComputerLinks;
import android.util.Log;
import org.apache.mina.core.session.IoSession;
import org.kde.kdeconnect.LinkProviders.BaseLinkProvider;
import org.kde.kdeconnect.NetworkPackage;
public class LanComputerLink extends BaseComputerLink {
private IoSession session = null;
public void disconnect() {
Log.i("LanComputerLink","Disconnect: "+session.getRemoteAddress().toString());
session.close(true);
}
public LanComputerLink(IoSession session, String deviceId, BaseLinkProvider linkProvider) {
super(deviceId, linkProvider);
this.session = session;
}
@Override
public boolean sendPackage(NetworkPackage np) {
if (session == null) {
Log.e("LanComputerLink","sendPackage failed: not yet connected");
return false;
} else {
session.write(np.serialize());
return true;
}
}
public void injectNetworkPackage(NetworkPackage np) {
packageReceived(np);
}
}

View File

@@ -0,0 +1,23 @@
package org.kde.kdeconnect.ComputerLinks;
import android.util.Log;
import org.apache.mina.core.session.IoSession;
import org.kde.kdeconnect.LinkProviders.BaseLinkProvider;
import org.kde.kdeconnect.NetworkPackage;
public class LoopbackComputerLink extends BaseComputerLink {
public LoopbackComputerLink(BaseLinkProvider linkProvider) {
super("loopback", linkProvider);
}
@Override
public boolean sendPackage(NetworkPackage in) {
String s = in.serialize();
NetworkPackage out= NetworkPackage.unserialize(s);
packageReceived(out);
return true;
}
}

View File

@@ -0,0 +1,604 @@
package org.kde.kdeconnect;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.os.AsyncTask;
import android.os.Handler;
import android.os.Looper;
import android.preference.PreferenceManager;
import android.support.v4.app.NotificationCompat;
import android.util.Base64;
import android.util.Log;
import org.kde.kdeconnect.ComputerLinks.BaseComputerLink;
import org.kde.kdeconnect.Plugins.Plugin;
import org.kde.kdeconnect.Plugins.PluginFactory;
import org.kde.kdeconnect.UserInterface.PairActivity;
import org.kde.kdeconnect_tp.R;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
public class Device implements BaseComputerLink.PackageReceiver {
private Context context;
private String deviceId;
private String name;
private PublicKey publicKey;
private int notificationId;
private enum PairStatus {
NotPaired,
Requested,
RequestedByPeer,
Paired
}
public interface PairingCallback {
abstract void incomingRequest();
abstract void pairingSuccessful();
abstract void pairingFailed(String error);
abstract void unpaired();
}
private PairStatus pairStatus;
private ArrayList<PairingCallback> pairingCallback = new ArrayList<PairingCallback>();
private Timer pairingTimer;
private ArrayList<BaseComputerLink> links = new ArrayList<BaseComputerLink>();
private HashMap<String, Plugin> plugins = new HashMap<String, Plugin>();
private HashMap<String, Plugin> failedPlugins = new HashMap<String, Plugin>();
SharedPreferences settings;
//Remembered trusted device, we need to wait for a incoming devicelink to communicate
Device(Context context, String deviceId) {
settings = context.getSharedPreferences(deviceId, Context.MODE_PRIVATE);
//Log.e("Device","Constructor A");
this.context = context;
this.deviceId = deviceId;
this.name = settings.getString("deviceName", "unknown device");
this.pairStatus = PairStatus.Paired;
try {
byte[] publicKeyBytes = Base64.decode(settings.getString("publicKey", ""), 0);
publicKey = KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(publicKeyBytes));
} catch (Exception e) {
e.printStackTrace();
Log.e("Device","Exception");
}
reloadPluginsFromSettings();
}
//Device known via an incoming connection sent to us via a devicelink, we know everything but we don't trust it yet
Device(Context context, String deviceId, String name, BaseComputerLink dl) {
settings = context.getSharedPreferences(deviceId, Context.MODE_PRIVATE);
//Log.e("Device","Constructor B");
this.context = context;
this.deviceId = deviceId;
this.name = name;
this.pairStatus = PairStatus.NotPaired;
this.publicKey = null;
addLink(dl);
}
public String getName() {
return name != null? name : context.getString(R.string.unknown_device);
}
public String getDeviceId() {
return deviceId;
}
//
// Pairing-related functions
//
public boolean isPaired() {
return pairStatus == PairStatus.Paired;
}
public boolean isPairRequested() {
return pairStatus == PairStatus.Requested;
}
public void addPairingCallback(PairingCallback callback) {
pairingCallback.add(callback);
if (pairStatus == PairStatus.RequestedByPeer) {
callback.incomingRequest();
}
}
public void removePairingCallback(PairingCallback callback) {
pairingCallback.remove(callback);
}
public void requestPairing() {
Resources res = context.getResources();
if (pairStatus == PairStatus.Paired) {
for (PairingCallback cb : pairingCallback) cb.pairingFailed(res.getString(R.string.error_already_paired));
return;
}
if (pairStatus == PairStatus.Requested) {
for (PairingCallback cb : pairingCallback) cb.pairingFailed(res.getString(R.string.error_already_requested));
return;
}
if (!isReachable()) {
for (PairingCallback cb : pairingCallback) cb.pairingFailed(res.getString(R.string.error_not_reachable));
return;
}
//Send our own public key
NetworkPackage np = NetworkPackage.createPublicKeyPackage(context);
boolean success = sendPackage(np);
if (!success) {
for (PairingCallback cb : pairingCallback) cb.pairingFailed(res.getString(R.string.error_could_not_send_package));
return;
}
pairingTimer = new Timer();
pairingTimer.schedule(new TimerTask() {
@Override
public void run() {
for (PairingCallback cb : pairingCallback) cb.pairingFailed(context.getString(R.string.error_timed_out));
pairStatus = PairStatus.NotPaired;
}
}, 20*1000);
pairStatus = PairStatus.Requested;
}
public int getNotificationId() {
return notificationId;
}
public void unpair() {
if (!isPaired()) return;
pairStatus = PairStatus.NotPaired;
SharedPreferences preferences = context.getSharedPreferences("trusted_devices", Context.MODE_PRIVATE);
preferences.edit().remove(deviceId).commit();
NetworkPackage np = new NetworkPackage(NetworkPackage.PACKAGE_TYPE_PAIR);
np.set("pair", false);
sendPackage(np);
for (PairingCallback cb : pairingCallback) cb.unpaired();
reloadPluginsFromSettings();
}
public void acceptPairing() {
Log.i("Device","Accepted pairing");
//Send our own public key
NetworkPackage np = NetworkPackage.createPublicKeyPackage(context);
boolean success = sendPackage(np);
if (!success) return;
pairStatus = PairStatus.Paired;
//Store as trusted device
String encodedPublicKey = Base64.encodeToString(publicKey.getEncoded(), 0);
SharedPreferences preferences = context.getSharedPreferences("trusted_devices", Context.MODE_PRIVATE);
preferences.edit().putBoolean(deviceId,true).commit();
//Store device information needed to create a Device object in a future
SharedPreferences.Editor editor = settings.edit();
editor.putString("deviceName", getName());
editor.putString("publicKey", encodedPublicKey);
editor.commit();
reloadPluginsFromSettings();
for (PairingCallback cb : pairingCallback) cb.pairingSuccessful();
}
public void rejectPairing() {
Log.i("Device","Rejected pairing");
pairStatus = PairStatus.NotPaired;
NetworkPackage np = new NetworkPackage(NetworkPackage.PACKAGE_TYPE_PAIR);
np.set("pair", false);
sendPackage(np);
for (PairingCallback cb : pairingCallback) cb.pairingFailed(context.getString(R.string.error_canceled_by_user));
}
//
// ComputerLink-related functions
//
public boolean isReachable() {
return !links.isEmpty();
}
public void addLink(BaseComputerLink link) {
links.add(link);
Log.i("Device","addLink "+link.getLinkProvider().getName()+" -> "+getName() + " active links: "+ links.size());
Collections.sort(links, new Comparator<BaseComputerLink>() {
@Override
public int compare(BaseComputerLink o, BaseComputerLink o2) {
return o2.getLinkProvider().getPriority() - o.getLinkProvider().getPriority();
}
});
link.addPackageReceiver(this);
if (links.size() == 1) {
reloadPluginsFromSettings();
}
}
public void removeLink(BaseComputerLink link) {
link.removePackageReceiver(this);
links.remove(link);
Log.i("Device","removeLink: "+link.getLinkProvider().getName() + " -> "+getName() + " active links: "+ links.size());
if (links.isEmpty()) {
reloadPluginsFromSettings();
}
}
@Override
public void onPackageReceived(NetworkPackage np) {
if (np.getType().equals(NetworkPackage.PACKAGE_TYPE_PAIR)) {
Log.i("Device","Pair package");
boolean wantsPair = np.getBoolean("pair");
if (wantsPair == isPaired()) {
if (pairStatus == PairStatus.Requested) {
pairStatus = PairStatus.NotPaired;
pairingTimer.cancel();
for (PairingCallback cb : pairingCallback) cb.pairingFailed(context.getString(R.string.error_canceled_by_other_peer));
}
return;
}
if (wantsPair) {
//Retrieve their public key
try {
String publicKeyContent = np.getString("publicKey").replace("-----BEGIN PUBLIC KEY-----\n","").replace("-----END PUBLIC KEY-----\n","");
byte[] publicKeyBytes = Base64.decode(publicKeyContent, 0);
publicKey = KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(publicKeyBytes));
} catch(Exception e) {
e.printStackTrace();
Log.e("Device","Pairing exception: Received incorrect key");
for (PairingCallback cb : pairingCallback) cb.pairingFailed(context.getString(R.string.error_invalid_key));
return;
}
if (pairStatus == PairStatus.Requested) { //We started pairing
Log.i("Pairing","Pair answer");
pairStatus = PairStatus.Paired;
pairingTimer.cancel();
//Store as trusted device
String encodedPublicKey = Base64.encodeToString(publicKey.getEncoded(), 0);
SharedPreferences preferences = context.getSharedPreferences("trusted_devices", Context.MODE_PRIVATE);
preferences.edit().putBoolean(deviceId,true).commit();
//Store device information needed to create a Device object in a future
SharedPreferences.Editor editor = settings.edit();
editor.putString("deviceName", getName());
editor.putString("publicKey", encodedPublicKey);
editor.commit();
reloadPluginsFromSettings();
for (PairingCallback cb : pairingCallback) cb.pairingSuccessful();
} else {
Log.i("Pairing","Pair request");
Intent intent = new Intent(context, PairActivity.class);
intent.putExtra("deviceId", deviceId);
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_ONE_SHOT);
Resources res = context.getResources();
Notification noti = new NotificationCompat.Builder(context)
.setContentTitle(res.getString(R.string.pairing_request_from, getName()))
.setContentText(res.getString(R.string.tap_to_answer))
.setContentIntent(pendingIntent)
.setTicker(res.getString(R.string.pair_requested))
.setSmallIcon(android.R.drawable.ic_menu_help)
.setAutoCancel(true)
.setDefaults(Notification.DEFAULT_SOUND)
.build();
final NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
notificationId = (int)System.currentTimeMillis();
notificationManager.notify(notificationId, noti);
pairingTimer = new Timer();
pairingTimer.schedule(new TimerTask() {
@Override
public void run() {
pairStatus = PairStatus.NotPaired;
notificationManager.cancel(notificationId);
}
}, 19*1000); //Time to show notification
pairStatus = PairStatus.RequestedByPeer;
for (PairingCallback cb : pairingCallback) cb.incomingRequest();
}
} else {
Log.i("Pairing","Unpair request");
if (pairStatus == PairStatus.Requested) {
pairingTimer.cancel();
for (PairingCallback cb : pairingCallback) cb.pairingFailed(context.getString(R.string.error_canceled_by_other_peer));
} else if (pairStatus == PairStatus.Paired) {
SharedPreferences preferences = context.getSharedPreferences("trusted_devices", Context.MODE_PRIVATE);
preferences.edit().remove(deviceId).commit();
reloadPluginsFromSettings();
}
pairStatus = PairStatus.NotPaired;
for (PairingCallback cb : pairingCallback) cb.unpaired();
}
} else if (!isPaired()) {
//TODO: Notify the other side that we don't trust them
Log.e("onPackageReceived","Device not paired, ignoring package!");
} else {
if (np.getType().equals(NetworkPackage.PACKAGE_TYPE_ENCRYPTED)) {
try {
//TODO: Do not read the key every time
SharedPreferences globalSettings = PreferenceManager.getDefaultSharedPreferences(context);
byte[] privateKeyBytes = Base64.decode(globalSettings.getString("privateKey",""), 0);
PrivateKey privateKey = KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(privateKeyBytes));
np = np.decrypt(privateKey);
} catch(Exception e) {
e.printStackTrace();
Log.e("onPackageReceived","Exception reading the key needed to decrypt the package");
}
} else {
//TODO: The other side doesn't know that we are already paired, do something
Log.e("onPackageReceived","WARNING: Received unencrypted package from paired device!");
}
for (Plugin plugin : plugins.values()) {
plugin.onPackageReceived(np);
}
}
}
public boolean sendPackage(final NetworkPackage np) {
if (!np.getType().equals(NetworkPackage.PACKAGE_TYPE_PAIR) && isPaired()) {
try {
np.encrypt(publicKey);
} catch(Exception e) {
e.printStackTrace();
Log.e("Device","sendPackage exception - could not encrypt");
}
}
new AsyncTask<Void,Void,Void>() {
@Override
protected Void doInBackground(Void... voids) {
for(BaseComputerLink link : links) {
//Log.e("sendPackage","Trying "+link.getLinkProvider().getName());
if (link.sendPackage(np)) {
//Log.e("sent using", link.getLinkProvider().getName());
return null;
}
}
Log.e("sendPackage","Error: Package could not be sent ("+links.size()+" links available)");
return null;
}
}.execute();
//TODO: Detect when unable to send a package and try again somehow
return !links.isEmpty();
}
//
// Plugin-related functions
//
public Plugin getPlugin(String name) {
return plugins.get(name);
}
private void addPlugin(final String name) {
Plugin existing = plugins.get(name);
if (existing != null) {
Log.w("addPlugin","plugin already present:" + name);
return;
}
final Plugin plugin = PluginFactory.instantiatePluginForDevice(context, name, this);
if (plugin == null) {
Log.e("addPlugin","could not instantiate plugin: "+name);
failedPlugins.put(name, plugin);
return;
}
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
try {
boolean success = plugin.onCreate();
if (!success) {
Log.e("addPlugin", "plugin failed to load " + name);
failedPlugins.put(name, plugin);
return;
}
} catch (Exception e) {
failedPlugins.put(name, plugin);
e.printStackTrace();
Log.e("addPlugin", "Exception loading plugin " + name);
return;
}
//Log.e("addPlugin","added " + name);
failedPlugins.remove(name);
plugins.put(name, plugin);
for (PluginsChangedListener listener : pluginsChangedListeners) {
listener.onPluginsChanged(Device.this);
}
}
});
}
private boolean removePlugin(String name) {
Plugin plugin = plugins.remove(name);
Plugin failedPlugin = failedPlugins.remove(name);
if (plugin == null) {
if (failedPlugin == null) {
return false;
}
plugin = failedPlugin;
}
try {
plugin.onDestroy();
} catch (Exception e) {
e.printStackTrace();
Log.e("removePlugin","Exception calling onDestroy for plugin "+name);
return false;
}
//Log.e("removePlugin","removed " + name);
for (PluginsChangedListener listener : pluginsChangedListeners) {
listener.onPluginsChanged(this);
}
return true;
}
public void setPluginEnabled(String pluginName, boolean value) {
settings.edit().putBoolean(pluginName,value).commit();
if (value) addPlugin(pluginName);
else removePlugin(pluginName);
}
public boolean isPluginEnabled(String pluginName) {
boolean enabledByDefault = PluginFactory.getPluginInfo(context, pluginName).isEnabledByDefault();
boolean enabled = settings.getBoolean(pluginName, enabledByDefault);
return enabled;
}
public void reloadPluginsFromSettings() {
failedPlugins.clear();
Set<String> availablePlugins = PluginFactory.getAvailablePlugins();
for(String pluginName : availablePlugins) {
boolean enabled = false;
if (isPaired() && isReachable()) {
enabled = isPluginEnabled(pluginName);
}
if (enabled) {
addPlugin(pluginName);
} else {
removePlugin(pluginName);
}
}
for (PluginsChangedListener listener : pluginsChangedListeners) {
listener.onPluginsChanged(this);
}
}
public HashMap<String,Plugin> getLoadedPlugins() {
return plugins;
}
public HashMap<String,Plugin> getFailedPlugins() {
return failedPlugins;
}
public interface PluginsChangedListener {
void onPluginsChanged(Device device);
}
private ArrayList<PluginsChangedListener> pluginsChangedListeners = new ArrayList<PluginsChangedListener>();
public void addPluginsChangedListener(PluginsChangedListener listener) {
pluginsChangedListeners.add(listener);
}
public void removePluginsChangedListener(PluginsChangedListener listener) {
pluginsChangedListeners.remove(listener);
}
}

View File

@@ -0,0 +1,52 @@
package org.kde.kdeconnect.Helpers;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
import android.util.Log;
public class AppsHelper {
public static String appNameLookup(Context context, String packageName) {
try {
PackageManager pm = context.getPackageManager();
ApplicationInfo ai = pm.getApplicationInfo( packageName, 0);
return pm.getApplicationLabel(ai).toString();
} catch (final PackageManager.NameNotFoundException e) {
e.printStackTrace();
Log.e("AppsHelper","Could not resolve name "+packageName);
return null;
}
}
public static Drawable appIconLookup(Context context, String packageName) {
try {
PackageManager pm = context.getPackageManager();
ApplicationInfo ai = pm.getApplicationInfo( packageName, 0);
return pm.getApplicationIcon(ai);
} catch (final PackageManager.NameNotFoundException e) {
e.printStackTrace();
Log.e("AppsHelper","Could not find icon for "+packageName);
return null;
}
}
}

View File

@@ -0,0 +1,45 @@
package org.kde.kdeconnect.Helpers;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.provider.ContactsContract;
import android.provider.ContactsContract.PhoneLookup;
import android.util.Log;
public class ContactsHelper {
public static String phoneNumberLookup(Context context, String number) {
//Log.e("PhoneNumberLookup", number);
Uri uri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number));
Cursor cursor = null;
try {
cursor = context.getContentResolver().query(
uri,
new String[] {
PhoneLookup.DISPLAY_NAME
/*, PhoneLookup.TYPE
, PhoneLookup.LABEL
, PhoneLookup.ID */
},
null, null, null);
} catch (IllegalArgumentException e) {
return number;
}
// Take the first match only
if (cursor != null && cursor.moveToFirst()) {
int nameIndex = cursor.getColumnIndex(ContactsContract.PhoneLookup.DISPLAY_NAME);
if (nameIndex != -1) {
String name = cursor.getString(nameIndex);
//Log.e("PhoneNumberLookup", "success: " + name);
return name;
}
}
return number;
}
}

View File

@@ -0,0 +1,23 @@
package org.kde.kdeconnect.Helpers;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
public class ImagesHelper {
public static Bitmap drawableToBitmap (Drawable drawable) {
if (drawable instanceof BitmapDrawable) {
return ((BitmapDrawable)drawable).getBitmap();
}
Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
drawable.draw(canvas);
return bitmap;
}
}

View File

@@ -0,0 +1,170 @@
package org.kde.kdeconnect;
import android.os.Build;
import java.util.HashMap;
public class HumanDeviceNames {
//from https://github.com/meetup/android-device-names
//Converted to java using:
//cat android_models.properties | awk -F'=' '{sub(/ *$/, "", $1)} sub(/^ */, "", $2) { if ($2 != "") print "humanReadableNames.put(\""$1"\",\"" $2 "\");"}'
private static HashMap<String,String> humanReadableNames = new HashMap<String,String>();
static {
humanReadableNames.put("5860E","Coolpad Quattro 4G");
humanReadableNames.put("ADR6300","HTC Droid Incredible");
humanReadableNames.put("ADR6330VW","HTC Rhyme");
humanReadableNames.put("ADR6350","HTC Droid Incredible 2");
humanReadableNames.put("ADR6400L","HTC Thunderbolt");
humanReadableNames.put("ADR6410LVW","HTC Droid Incredible 4G");
humanReadableNames.put("ADR6425LVW","HTC Rezound 4G");
humanReadableNames.put("ASUS_Transformer_Pad_TF300T","Asus Transformer Pad");
humanReadableNames.put("C5155","Kyocera Rise");
humanReadableNames.put("C5170","Kyocera Hydro");
humanReadableNames.put("C6603","Sony Xperia Z");
humanReadableNames.put("Desire_HD","HTC Desire HD");
humanReadableNames.put("DROID2_GLOBAL","Motorola Droid 2 Global");
humanReadableNames.put("DROID2","Motorola Droid 2");
humanReadableNames.put("DROID3","Motorola Droid 3");
humanReadableNames.put("DROID4","Motorola Droid 4");
humanReadableNames.put("DROID_BIONIC","Motorola Droid Bionic");
humanReadableNames.put("Droid","Motorola Droid");
humanReadableNames.put("DROID_Pro","Motorola Droid Pro");
humanReadableNames.put("DROID_RAZR_HD","Motorola Droid Razr HD");
humanReadableNames.put("DROID_RAZR","Motorola Droid Razr");
humanReadableNames.put("DROID_X2","Motorola Droid X2");
humanReadableNames.put("DROIDX","Motorola Droid X");
humanReadableNames.put("EVO","HTC Evo");
humanReadableNames.put("Galaxy_Nexus","Samsung Galaxy Nexus");
humanReadableNames.put("google_sdk","Android Emulator");
humanReadableNames.put("GT-I8160","Samsung Galaxy Ace 2");
humanReadableNames.put("GT-I8190","Samsung Galaxy S III Mini");
humanReadableNames.put("GT-I9000","Samsung Galaxy S");
humanReadableNames.put("GT-I9001","Samsung Galaxy S Plus");
humanReadableNames.put("GT-I9100M","Samsung Galaxy S II");
humanReadableNames.put("GT-I9100P","Samsung Galaxy S II");
humanReadableNames.put("GT-I9100","Samsung Galaxy S II");
humanReadableNames.put("GT-I9100T","Samsung Galaxy S II");
humanReadableNames.put("GT-I9300","Samsung Galaxy S III");
humanReadableNames.put("GT-I9300T","Samsung Galaxy S III");
humanReadableNames.put("GT-I9305","Samsung Galaxy S III");
humanReadableNames.put("GT-I9505","Samsung Galaxy S 4");
humanReadableNames.put("GT-N7000","Samsung Galaxy Note");
humanReadableNames.put("GT-N7100","Samsung Galaxy Note II");
humanReadableNames.put("GT-N7105","Samsung Galaxy Note II");
humanReadableNames.put("GT-N8013","Samsung Galaxy Note 10.1");
humanReadableNames.put("GT-P3113","Samsung Galaxy Tab 2 7.0");
humanReadableNames.put("GT-P5113","Samsnung Galaxy Tab 2 10.1");
humanReadableNames.put("GT-P7510","Samsung Galaxy Tab 10.1");
humanReadableNames.put("GT-S5360","Samsung Galaxy Y");
humanReadableNames.put("GT-S5570","Samsung Galaxy Mini");
humanReadableNames.put("GT-S5830i","Samsung Galaxy Ace");
humanReadableNames.put("GT-S5830","Samsung Galaxy Ace");
humanReadableNames.put("HTC6435LVW","HTC Droid DNA");
humanReadableNames.put("HTC_Desire_HD_A9191","HTC Desire HD");
humanReadableNames.put("HTCEVODesign4G","HTC Evo Design 4G");
humanReadableNames.put("HTCEVOV4G","HTC Evo V 4G");
humanReadableNames.put("HTCONE","HTC One");
humanReadableNames.put("HTC_PH39100","HTC Vivid 4G");
humanReadableNames.put("HTC_Sensation_Z710e","HTC Sensation");
humanReadableNames.put("HTC_VLE_U","HTC One S");
humanReadableNames.put("KFJWA","Kindle Fire HD 8.9");
humanReadableNames.put("KFJWI","Kindle Fire HD 8.9");
humanReadableNames.put("KFOT","Kindle Fire");
humanReadableNames.put("KFTT","Kindle Fire HD 7");
humanReadableNames.put("LG-C800","LG myTouch Q");
humanReadableNames.put("LG-E739","LG MyTouch e739");
humanReadableNames.put("LGL55C","LG LGL55C");
humanReadableNames.put("LG-LS970","LG Optimus G");
humanReadableNames.put("LG-MS770","LG Motion 4G");
humanReadableNames.put("LG-MS910","LG Esteem");
humanReadableNames.put("LG-P509","LG Optimus T");
humanReadableNames.put("LG-P769","LG Optimus L9");
humanReadableNames.put("LG-P999","LG G2X P999");
humanReadableNames.put("LG-VM696","LG Optimus Elite");
humanReadableNames.put("LS670","LG Optimus S");
humanReadableNames.put("LT26i","Sony Xperia S");
humanReadableNames.put("MB855","Motorola Photon 4G");
humanReadableNames.put("MB860","Motorola Atrix 4G");
humanReadableNames.put("MB865","Motorola Atrix 2");
humanReadableNames.put("MB886","Motorola Atrix HD");
humanReadableNames.put("MOTWX435KT","Motorola Triumph");
humanReadableNames.put("myTouch_4G_Slide","HTC myTouch 4G Slide");
humanReadableNames.put("N860","ZTE Warp N860");
humanReadableNames.put("Nexus_10","Google Nexus 10");
humanReadableNames.put("Nexus_4","Google Nexus 4");
humanReadableNames.put("Nexus_7","Asus Nexus 7");
humanReadableNames.put("Nexus_S_4G","Samsung Nexus S 4G");
humanReadableNames.put("Nexus_S","Samsung Nexus S");
humanReadableNames.put("PantechP9070","Pantech Burst");
humanReadableNames.put("PC36100","HTC Evo 4G");
humanReadableNames.put("PG06100","HTC EVO Shift 4G");
humanReadableNames.put("PG86100","HTC Evo 3D");
humanReadableNames.put("PH44100","HTC Evo Design 4G");
humanReadableNames.put("SAMSUNG-SGH-I317","Samsung Galaxy Note II");
humanReadableNames.put("SAMSUNG-SGH-I337","Samsung Galaxy S 4");
humanReadableNames.put("SAMSUNG-SGH-I717","Samsung Galaxy Note");
humanReadableNames.put("SAMSUNG-SGH-I727","Samsung Skyrocket");
humanReadableNames.put("SAMSUNG-SGH-I747","Samsung Galaxy S III");
humanReadableNames.put("SAMSUNG-SGH-I777","Samsung Galaxy S II");
humanReadableNames.put("SAMSUNG-SGH-I897","Samsung Captivate");
humanReadableNames.put("SAMSUNG-SGH-I927","Samsung Captivate Glide");
humanReadableNames.put("SAMSUNG-SGH-I997","Samsung Infuse 4G");
humanReadableNames.put("SCH-I200","Samsung Galaxy Stellar");
humanReadableNames.put("SCH-I405","Samsung Stratosphere");
humanReadableNames.put("SCH-I500","Samsung Fascinate");
humanReadableNames.put("SCH-I510","Samsung Droid Charge");
humanReadableNames.put("SCH-I535","Samsung Galaxy S III");
humanReadableNames.put("SCH-I545","Samsung Galaxy S 4");
humanReadableNames.put("SCH-I605","Samsung Galaxy Note II");
humanReadableNames.put("SCH-I800","Samsung Galaxy Tab 7.0");
humanReadableNames.put("SCH-R530M","Samsung Galaxy S III");
humanReadableNames.put("SCH-R530U","Samsung Galaxy S III");
humanReadableNames.put("SCH-R720","Samsung Admire");
humanReadableNames.put("SCH-S720C","Samsung Proclaim");
humanReadableNames.put("SGH-I317M","Samsung Galaxy Note II");
humanReadableNames.put("SGH-I727R","Samsung Galaxy S II");
humanReadableNames.put("SGH-I747M","Samsung Galaxy S III");
humanReadableNames.put("SGH-M919","Samsung Galaxy S 4");
humanReadableNames.put("SGH-T679","Samsung Exhibit II");
humanReadableNames.put("SGH-T769","Samsung Galaxy S Blaze");
humanReadableNames.put("SGH-T889","Samsung Galaxy Note II");
humanReadableNames.put("SGH-T959","Samsung Galaxy S Vibrant");
humanReadableNames.put("SGH-T959V","Samsung Galaxy S 4G");
humanReadableNames.put("SGH-T989D","Samsung Galaxy S II");
humanReadableNames.put("SGH-T989","Samsung Galaxy S II");
humanReadableNames.put("SGH-T999","Samsung Galaxy S III");
humanReadableNames.put("SPH-D600","Samsung Conquer 4G");
humanReadableNames.put("SPH-D700","Samsung Epic 4G");
humanReadableNames.put("SPH-D710BST","Samsung Galaxy S II");
humanReadableNames.put("SPH-D710","Samsung Epic");
humanReadableNames.put("SPH-D710VMUB","Samsung Galaxy S II");
humanReadableNames.put("SPH-L710","Samsung Galaxy S III");
humanReadableNames.put("SPH-L720","Samsung Galaxy S 4");
humanReadableNames.put("SPH-L900","Samsung Galaxy Note II");
humanReadableNames.put("SPH-M820-BST","Samsung Galaxy Prevail");
humanReadableNames.put("SPH-M930BST","Samsung Transform Ultra");
humanReadableNames.put("Transformer_Prime_TF201","Asus Eee Pad Transformer Prime");
humanReadableNames.put("Transformer_TF101","Asus Eee Pad Transformer");
humanReadableNames.put("VM670","LG Optimus V");
humanReadableNames.put("VS840_4G","LG Lucid 4G");
humanReadableNames.put("VS910_4G","LG Revolution 4G");
humanReadableNames.put("VS920_4G","LG Spectrum 4G");
humanReadableNames.put("Xoom","Motorola Xoom");
humanReadableNames.put("XT907","Motorola Droid Razr M");
}
static String getDeviceName() {
String dictName = humanReadableNames.get(Build.MODEL.replace(' ','_'));
if (dictName != null) return dictName;
if (Build.BRAND.equals("samsung") || Build.BRAND.equals("Samsung")) {
return "Samsung" + Build.MODEL;
}
return Build.MODEL;
}
}

View File

@@ -0,0 +1,66 @@
package org.kde.kdeconnect;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.net.ConnectivityManager;
import android.net.wifi.WifiManager;
import android.util.Log;
public class KdeConnectBroadcastReceiver extends BroadcastReceiver
{
public void onReceive(Context context, Intent intent) {
//Log.e("KdeConnect", "Broadcast event: "+intent.getAction());
String action = intent.getAction();
if(action.equals(Intent.ACTION_PACKAGE_REPLACED)) {
Log.i("KdeConnect", "UpdateReceiver");
if (!intent.getData().getSchemeSpecificPart().equals(context.getPackageName())) {
Log.i("KdeConnect", "Ignoring, it's not me!");
return;
}
BackgroundService.RunCommand(context, new BackgroundService.InstanceCallback() {
@Override
public void onServiceStart(BackgroundService service) {
}
});
} else if (action.equals(Intent.ACTION_BOOT_COMPLETED)) {
Log.i("KdeConnect", "KdeConnectBroadcastReceiver");
BackgroundService.RunCommand(context, new BackgroundService.InstanceCallback() {
@Override
public void onServiceStart(BackgroundService service) {
}
});
} else if (action.equals(WifiManager.SUPPLICANT_CONNECTION_CHANGE_ACTION)
|| action.equals(WifiManager.WIFI_STATE_CHANGED_ACTION)
|| action.equals(ConnectivityManager.CONNECTIVITY_ACTION)
) {
Log.i("KdeConnect", "Connection state changed, trying to connect");
BackgroundService.RunCommand(context, new BackgroundService.InstanceCallback() {
@Override
public void onServiceStart(BackgroundService service) {
service.onNetworkChange();
}
});
} else if (action.equals(Intent.ACTION_SCREEN_ON)) {
BackgroundService.RunCommand(context, new BackgroundService.InstanceCallback() {
@Override
public void onServiceStart(BackgroundService service) {
service.onNetworkChange();
}
});
} else {
Log.i("KdeConnectBroadcastReceiver", "Ignoring broadcast event: "+intent.getAction());
}
}
}

View File

@@ -0,0 +1,49 @@
package org.kde.kdeconnect.LinkProviders;
import android.util.Log;
import org.kde.kdeconnect.ComputerLinks.BaseComputerLink;
import org.kde.kdeconnect.NetworkPackage;
import java.util.ArrayList;
public abstract class BaseLinkProvider {
private ArrayList<ConnectionReceiver> connectionReceivers = new ArrayList<ConnectionReceiver>();
public interface ConnectionReceiver {
public void onConnectionReceived(NetworkPackage identityPackage, BaseComputerLink link);
public void onConnectionLost(BaseComputerLink link);
}
public void addConnectionReceiver(ConnectionReceiver cr) {
connectionReceivers.add(cr);
}
public boolean removeConnectionReceiver(ConnectionReceiver cr) {
return connectionReceivers.remove(cr);
}
//These two should be called when the provider links to a new computer
protected void connectionAccepted(NetworkPackage identityPackage, BaseComputerLink link) {
Log.i("LinkProvider", "connectionAccepted");
for(ConnectionReceiver cr : connectionReceivers) {
cr.onConnectionReceived(identityPackage, link);
}
}
protected void connectionLost(BaseComputerLink link) {
Log.i("LinkProvider", "connectionLost");
for(ConnectionReceiver cr : connectionReceivers) {
cr.onConnectionLost(link);
}
}
//To override
public abstract void onStart();
public abstract void onStop();
public abstract void onNetworkChange();
public abstract int getPriority();
public abstract String getName();
}

View File

@@ -0,0 +1,283 @@
package org.kde.kdeconnect.LinkProviders;
import android.content.Context;
import android.os.AsyncTask;
import android.util.Log;
import org.apache.mina.core.future.ConnectFuture;
import org.apache.mina.core.future.IoFuture;
import org.apache.mina.core.future.IoFutureListener;
import org.apache.mina.core.service.IoHandler;
import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.ProtocolCodecFilter;
import org.apache.mina.filter.codec.textline.LineDelimiter;
import org.apache.mina.filter.codec.textline.TextLineCodecFactory;
import org.apache.mina.transport.socket.nio.NioDatagramAcceptor;
import org.apache.mina.transport.socket.nio.NioSocketAcceptor;
import org.apache.mina.transport.socket.nio.NioSocketConnector;
import org.kde.kdeconnect.ComputerLinks.LanComputerLink;
import org.kde.kdeconnect.NetworkPackage;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.charset.Charset;
import java.util.HashMap;
public class LanLinkProvider extends BaseLinkProvider {
private final static int port = 1714;
private Context context;
private HashMap<String, LanComputerLink> visibleComputers = new HashMap<String, LanComputerLink>();
private HashMap<Long, LanComputerLink> nioSessions = new HashMap<Long, LanComputerLink>();
private NioSocketAcceptor tcpAcceptor = null;
private NioDatagramAcceptor udpAcceptor = null;
private final IoHandler tcpHandler = new IoHandlerAdapter() {
@Override
public void sessionClosed(IoSession session) throws Exception {
LanComputerLink brokenLink = nioSessions.remove(session.getId());
if (brokenLink != null) {
connectionLost(brokenLink);
brokenLink.disconnect();
String deviceId = brokenLink.getDeviceId();
if (visibleComputers.get(deviceId) == brokenLink) {
visibleComputers.remove(deviceId);
}
}
}
@Override
public void messageReceived(IoSession session, Object message) throws Exception {
super.messageReceived(session, message);
//Log.e("LanLinkProvider","Incoming package, address: "+session.getRemoteAddress()).toString());
String theMessage = (String) message;
NetworkPackage np = NetworkPackage.unserialize(theMessage);
LanComputerLink prevLink = nioSessions.get(session.getId());
if (np.getType().equals(NetworkPackage.PACKAGE_TYPE_IDENTITY)) {
String myId = NetworkPackage.createIdentityPackage(context).getString("deviceId");
if (np.getString("deviceId").equals(myId)) {
return;
}
LanComputerLink link = new LanComputerLink(session, np.getString("deviceId"), LanLinkProvider.this);
nioSessions.put(session.getId(),link);
addLink(np, link);
} else {
if (prevLink == null) {
Log.e("LanLinkProvider","2 Expecting an identity package");
} else {
prevLink.injectNetworkPackage(np);
}
}
}
};
private IoHandler udpHandler = new IoHandlerAdapter() {
@Override
public void messageReceived(IoSession udpSession, Object message) throws Exception {
super.messageReceived(udpSession, message);
//Log.e("LanLinkProvider", "Udp message received (" + message.getClass() + ") " + message.toString());
NetworkPackage np = null;
try {
//We should receive a string thanks to the TextLineCodecFactory filter
String theMessage = (String) message;
np = NetworkPackage.unserialize(theMessage);
} catch (Exception e) {
e.printStackTrace();
Log.e("LanLinkProvider", "Could not unserialize package");
}
if (np != null) {
final NetworkPackage identityPackage = np;
if (!np.getType().equals(NetworkPackage.PACKAGE_TYPE_IDENTITY)) {
Log.e("LanLinkProvider", "1 Expecting an identity package");
return;
} else {
String myId = NetworkPackage.createIdentityPackage(context).getString("deviceId");
if (np.getString("deviceId").equals(myId)) {
return;
}
}
Log.i("LanLinkProvider", "Identity package received, creating link");
try {
final InetSocketAddress address = (InetSocketAddress) udpSession.getRemoteAddress();
final NioSocketConnector connector = new NioSocketConnector();
connector.setHandler(tcpHandler);
//TextLineCodecFactory will split incoming data delimited by the given string
connector.getFilterChain().addLast("codec",
new ProtocolCodecFilter(
new TextLineCodecFactory(Charset.defaultCharset(), LineDelimiter.UNIX, LineDelimiter.UNIX)
)
);
connector.getSessionConfig().setKeepAlive(true);
int tcpPort = np.getInt("tcpPort",port);
ConnectFuture future = connector.connect(new InetSocketAddress(address.getAddress(), tcpPort));
future.addListener(new IoFutureListener<IoFuture>() {
@Override
public void operationComplete(IoFuture ioFuture) {
IoSession session = ioFuture.getSession();
Log.i("LanLinkProvider", "Connection successful: " + session.isConnected());
LanComputerLink link = new LanComputerLink(session, identityPackage.getString("deviceId"), LanLinkProvider.this);
NetworkPackage np2 = NetworkPackage.createIdentityPackage(context);
link.sendPackage(np2);
nioSessions.put(session.getId(), link);
addLink(identityPackage, link);
}
});
} catch (Exception e) {
Log.e("LanLinkProvider","Exception!!");
e.printStackTrace();
}
}
}
};
private void addLink(NetworkPackage identityPackage, LanComputerLink link) {
String deviceId = identityPackage.getString("deviceId");
Log.i("LanLinkProvider","addLink to "+deviceId);
LanComputerLink oldLink = visibleComputers.get(deviceId);
visibleComputers.put(deviceId, link);
connectionAccepted(identityPackage, link);
if (oldLink != null) {
Log.i("LanLinkProvider","Removing old connection to same device");
oldLink.disconnect();
connectionLost(oldLink);
}
}
public LanLinkProvider(Context context) {
this.context = context;
//This handles the case when I'm the new device in the network and somebody answers my introduction package
tcpAcceptor = new NioSocketAcceptor();
tcpAcceptor.setHandler(tcpHandler);
tcpAcceptor.getSessionConfig().setKeepAlive(true);
tcpAcceptor.getSessionConfig().setReuseAddress(true);
tcpAcceptor.setCloseOnDeactivation(false);
//TextLineCodecFactory will split incoming data delimited by the given string
tcpAcceptor.getFilterChain().addLast("codec",
new ProtocolCodecFilter(
new TextLineCodecFactory(Charset.defaultCharset(), LineDelimiter.UNIX, LineDelimiter.UNIX)
)
);
udpAcceptor = new NioDatagramAcceptor();
udpAcceptor.getSessionConfig().setReuseAddress(true); //Share port if existing
//TextLineCodecFactory will split incoming data delimited by the given string
udpAcceptor.getFilterChain().addLast("codec",
new ProtocolCodecFilter(
new TextLineCodecFactory(Charset.defaultCharset(), LineDelimiter.UNIX, LineDelimiter.UNIX)
)
);
}
@Override
public void onStart() {
//This handles the case when I'm the existing device in the network and receive a "hello" UDP package
udpAcceptor.setHandler(udpHandler);
try {
udpAcceptor.bind(new InetSocketAddress(port));
} catch(Exception e) {
Log.e("LanLinkProvider", "Error: Could not bind udp socket");
e.printStackTrace();
}
boolean success = false;
int tcpPort = port;
while(!success) {
try {
tcpAcceptor.bind(new InetSocketAddress(tcpPort));
success = true;
} catch(Exception e) {
tcpPort++;
}
}
Log.i("LanLinkProvider","Using tcpPort "+tcpPort);
//I'm on a new network, let's be polite and introduce myself
final int finalTcpPort = tcpPort;
new AsyncTask<Void,Void,Void>() {
@Override
protected Void doInBackground(Void... voids) {
try {
NetworkPackage identity = NetworkPackage.createIdentityPackage(context);
identity.set("tcpPort",finalTcpPort);
byte[] b = identity.serialize().getBytes("UTF-8");
DatagramPacket packet = new DatagramPacket(b, b.length, InetAddress.getByAddress(new byte[]{-1,-1,-1,-1}), port);
DatagramSocket socket = new DatagramSocket();
socket.setReuseAddress(true);
socket.setBroadcast(true);
socket.send(packet);
//Log.e("LanLinkProvider","Udp identity package sent");
} catch(Exception e) {
e.printStackTrace();
Log.e("LanLinkProvider","Sending udp identity package failed");
}
return null;
}
}.execute();
}
@Override
public void onNetworkChange() {
onStop();
onStart();
}
@Override
public void onStop() {
udpAcceptor.unbind();
tcpAcceptor.unbind();
}
@Override
public int getPriority() {
return 1000;
}
@Override
public String getName() {
return "LanLinkProvider";
}
}

View File

@@ -0,0 +1,65 @@
package org.kde.kdeconnect.LinkProviders;
import android.content.Context;
import android.os.AsyncTask;
import android.util.Log;
import org.apache.mina.core.future.ConnectFuture;
import org.apache.mina.core.future.IoFuture;
import org.apache.mina.core.future.IoFutureListener;
import org.apache.mina.core.service.IoHandler;
import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.ProtocolCodecFilter;
import org.apache.mina.filter.codec.textline.LineDelimiter;
import org.apache.mina.filter.codec.textline.TextLineCodecFactory;
import org.apache.mina.transport.socket.nio.NioDatagramAcceptor;
import org.apache.mina.transport.socket.nio.NioSocketAcceptor;
import org.apache.mina.transport.socket.nio.NioSocketConnector;
import org.kde.kdeconnect.ComputerLinks.LanComputerLink;
import org.kde.kdeconnect.ComputerLinks.LoopbackComputerLink;
import org.kde.kdeconnect.NetworkPackage;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.charset.Charset;
import java.util.HashMap;
public class LoopbackLinkProvider extends BaseLinkProvider {
private Context context;
public LoopbackLinkProvider(Context context) {
this.context = context;
}
@Override
public void onStart() {
onNetworkChange();
}
@Override
public void onStop() {
}
@Override
public void onNetworkChange() {
NetworkPackage np = NetworkPackage.createIdentityPackage(context);
connectionAccepted(np, new LoopbackComputerLink(this));
}
@Override
public int getPriority() {
return 0;
}
@Override
public String getName() {
return "LoopbackLinkProvider";
}
}

View File

@@ -0,0 +1,211 @@
package org.kde.kdeconnect;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.provider.Settings;
import android.util.Base64;
import android.util.Log;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.nio.charset.Charset;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.ArrayList;
import javax.crypto.Cipher;
public class NetworkPackage {
public final static int ProtocolVersion = 3;
public final static String PACKAGE_TYPE_IDENTITY = "kdeconnect.identity";
public final static String PACKAGE_TYPE_PAIR = "kdeconnect.pair";
public final static String PACKAGE_TYPE_ENCRYPTED = "kdeconnect.encrypted";
public final static String PACKAGE_TYPE_PING = "kdeconnect.ping";
public final static String PACKAGE_TYPE_TELEPHONY = "kdeconnect.telephony";
public final static String PACKAGE_TYPE_BATTERY = "kdeconnect.battery";
public final static String PACKAGE_TYPE_NOTIFICATION = "kdeconnect.notification";
public final static String PACKAGE_TYPE_CLIPBOARD = "kdeconnect.clipboard";
public final static String PACKAGE_TYPE_MPRIS = "kdeconnect.mpris";
private long mId;
private String mType;
private JSONObject mBody;
private NetworkPackage() {
}
public NetworkPackage(String type) {
mId = System.currentTimeMillis();
mType = type;
mBody = new JSONObject();
}
public String getType() {
return mType;
}
//Most commons getters and setters defined for convenience
public String getString(String key) { return mBody.optString(key,""); }
public String getString(String key, String defaultValue) { return mBody.optString(key,defaultValue); }
public void set(String key, String value) { if (value == null) return; try { mBody.put(key,value); } catch(Exception e) { } }
public int getInt(String key) { return mBody.optInt(key,-1); }
public int getInt(String key, int defaultValue) { return mBody.optInt(key,defaultValue); }
public void set(String key, int value) { try { mBody.put(key,value); } catch(Exception e) { } }
public boolean getBoolean(String key) { return mBody.optBoolean(key,false); }
public boolean getBoolean(String key, boolean defaultValue) { return mBody.optBoolean(key,defaultValue); }
public void set(String key, boolean value) { try { mBody.put(key,value); } catch(Exception e) { } }
public double getDouble(String key) { return mBody.optDouble(key,Double.NaN); }
public double getDouble(String key, double defaultValue) { return mBody.optDouble(key,defaultValue); }
public void set(String key, double value) { try { mBody.put(key,value); } catch(Exception e) { } }
public ArrayList<String> getStringList(String key) {
JSONArray jsonArray = mBody.optJSONArray(key);
ArrayList<String> list = new ArrayList<String>();
int length = jsonArray.length();
for (int i = 0; i < length; i++) {
try {
String str = jsonArray.getString(i);
list.add(str);
} catch(Exception e) {
}
}
return list;
}
public ArrayList<String> getStringList(String key, ArrayList<String> defaultValue) {
if (mBody.has(key)) return getStringList(key);
else return defaultValue;
}
public void set(String key, ArrayList<String> value) {
try {
JSONArray jsonArray = new JSONArray();
for(String str : value) {
jsonArray.put(str);
}
mBody.put(key,jsonArray);
} catch(Exception e) {
}
}
public boolean has(String key) { return mBody.has(key); }
public String serialize() {
JSONObject jo = new JSONObject();
try {
jo.put("id",mId);
jo.put("type",mType);
jo.put("body",mBody);
} catch(Exception e) {
}
//QJSon does not escape slashes, but Java JSONObject does. Converting to QJson format.
String json = jo.toString().replace("\\/","/")+"\n";
//Log.e("NetworkPackage.serialize",json);
return json;
}
static public NetworkPackage unserialize(String s) {
//Log.e("NetworkPackage.unserialize", s);
NetworkPackage np = new NetworkPackage();
try {
JSONObject jo = new JSONObject(s);
np.mId = jo.getLong("id");
np.mType = jo.getString("type");
np.mBody = jo.getJSONObject("body");
} catch (Exception e) {
return null;
}
return np;
}
public void encrypt(PublicKey publicKey) throws Exception {
String serialized = serialize();
int chunkSize = 128;
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1PADDING");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
JSONArray chunks = new JSONArray();
while (serialized.length() > 0) {
if (serialized.length() < chunkSize) {
chunkSize = serialized.length();
}
String chunk = serialized.substring(0, chunkSize);
serialized = serialized.substring(chunkSize);
byte[] chunkBytes = chunk.getBytes(Charset.defaultCharset());
byte[] encryptedChunk;
encryptedChunk = cipher.doFinal(chunkBytes);
chunks.put(Base64.encodeToString(encryptedChunk, Base64.NO_WRAP));
}
mId = System.currentTimeMillis();
mType = NetworkPackage.PACKAGE_TYPE_ENCRYPTED;
mBody = new JSONObject();
try {
mBody.put("data", chunks);
}catch(Exception e){
e.printStackTrace();
Log.e("NetworkPackage","Exception");
}
Log.i("NetworkPackage", "Encrypted " + chunks.length()+" chunks");
}
public NetworkPackage decrypt(PrivateKey privateKey) throws Exception {
JSONArray chunks = mBody.getJSONArray("data");
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1PADDING");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
String decryptedJson = "";
for (int i = 0; i < chunks.length(); i++) {
byte[] encryptedChunk = Base64.decode(chunks.getString(i), Base64.NO_WRAP);
String decryptedChunk = new String(cipher.doFinal(encryptedChunk));
decryptedJson += decryptedChunk;
}
return unserialize(decryptedJson);
}
static public NetworkPackage createIdentityPackage(Context context) {
NetworkPackage np = new NetworkPackage(NetworkPackage.PACKAGE_TYPE_IDENTITY);
String deviceId = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
try {
np.mBody.put("deviceId", deviceId);
np.mBody.put("deviceName", HumanDeviceNames.getDeviceName());
np.mBody.put("protocolVersion", NetworkPackage.ProtocolVersion);
} catch (JSONException e) {
}
return np;
}
static public NetworkPackage createPublicKeyPackage(Context context) {
NetworkPackage np = new NetworkPackage(NetworkPackage.PACKAGE_TYPE_PAIR);
np.set("pair", true);
SharedPreferences globalSettings = PreferenceManager.getDefaultSharedPreferences(context);
String publicKey = "-----BEGIN PUBLIC KEY-----\n" + globalSettings.getString("publicKey", "").trim()+ "\n-----END PUBLIC KEY-----\n";
np.set("publicKey", publicKey);
return np;
}
}

View File

@@ -0,0 +1,121 @@
package org.kde.kdeconnect.Plugins.BatteryPlugin;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.drawable.Drawable;
import android.os.BatteryManager;
import android.util.Log;
import android.widget.Button;
import org.kde.kdeconnect.NetworkPackage;
import org.kde.kdeconnect.Plugins.Plugin;
import org.kde.kdeconnect_tp.R;
public class BatteryPlugin extends Plugin {
private NetworkPackage lastPackage = null;
private IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
/*static {
PluginFactory.registerPlugin(BatteryPlugin.class);
}*/
@Override
public String getPluginName() {
return "plugin_battery";
}
@Override
public String getDisplayName() {
return context.getResources().getString(R.string.pref_plugin_battery);
}
@Override
public String getDescription() {
return context.getResources().getString(R.string.pref_plugin_battery_desc);
}
@Override
public Drawable getIcon() {
return context.getResources().getDrawable(R.drawable.icon);
}
@Override
public boolean isEnabledByDefault() {
return true;
}
private BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Log.i("BatteryPlugin", "Battery event");
boolean isCharging = (0 != intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0));
int currentCharge = 100;
int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
if (status != BatteryManager.BATTERY_STATUS_FULL) {
Intent batteryStatus = context.registerReceiver(null, filter);
int level = batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
int scale = batteryStatus.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
currentCharge = level*100 / scale;
}
//Only notify if change is meaningful enough
if (lastPackage == null
|| (
isCharging != lastPackage.getBoolean("isCharging")
|| currentCharge != lastPackage.getInt("currentCharge")
)
) {
NetworkPackage np = new NetworkPackage(NetworkPackage.PACKAGE_TYPE_BATTERY);
np.set("isCharging", isCharging);
np.set("currentCharge", currentCharge);
device.sendPackage(np);
lastPackage = np;
}
}
};
@Override
public boolean onCreate() {
context.registerReceiver(receiver, filter);
return true;
}
@Override
public void onDestroy() {
context.unregisterReceiver(receiver);
}
@Override
public boolean onPackageReceived(NetworkPackage np) {
if (!np.getType().equals(NetworkPackage.PACKAGE_TYPE_BATTERY)) return false;
if (np.getBoolean("request")) {
if (lastPackage != null) {
device.sendPackage(lastPackage);
}
}
return true;
}
@Override
public AlertDialog getErrorDialog(Context baseContext) {
return null;
}
@Override
public Button getInterfaceButton(Activity activity) {
return null;
}
}

View File

@@ -0,0 +1,52 @@
package org.kde.kdeconnect.Plugins.ClibpoardPlugin;
import android.content.ClipData;
import android.content.Context;
import android.content.ClipboardManager;
import org.kde.kdeconnect.Device;
import org.kde.kdeconnect.NetworkPackage;
public class ClipboardListener {
private String currentContent;
private ClipboardManager cm = null;
ClipboardManager.OnPrimaryClipChangedListener listener;
ClipboardListener(final Context context, final Device device) {
cm = (ClipboardManager)context.getSystemService(Context.CLIPBOARD_SERVICE);
listener = new ClipboardManager.OnPrimaryClipChangedListener() {
@Override
public void onPrimaryClipChanged() {
try {
ClipData.Item item = cm.getPrimaryClip().getItemAt(0);
String content = item.coerceToText(context).toString();
if (!content.equals(currentContent)) {
NetworkPackage np = new NetworkPackage(NetworkPackage.PACKAGE_TYPE_CLIPBOARD);
np.set("content", content);
device.sendPackage(np);
currentContent = content;
}
} catch(Exception e) {
//Probably clipboard was not text
}
}
};
cm.addPrimaryClipChangedListener(listener);
}
public void stop() {
cm.removePrimaryClipChangedListener(listener);
}
public void setText(String text) {
currentContent = text;
cm.setText(text);
}
}

View File

@@ -0,0 +1,100 @@
package org.kde.kdeconnect.Plugins.ClibpoardPlugin;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.widget.Button;
import org.kde.kdeconnect.NetworkPackage;
import org.kde.kdeconnect.Plugins.Plugin;
import org.kde.kdeconnect_tp.R;
public class ClipboardPlugin extends Plugin {
/*static {
PluginFactory.registerPlugin(ClipboardPlugin.class);
}*/
@Override
public String getPluginName() {
return "plugin_clipboard";
}
@Override
public String getDisplayName() {
return context.getResources().getString(R.string.pref_plugin_clipboard);
}
@Override
public String getDescription() {
return context.getResources().getString(R.string.pref_plugin_clipboard_desc);
}
@Override
public Drawable getIcon() {
return context.getResources().getDrawable(R.drawable.icon);
}
@Override
public boolean isEnabledByDefault() {
return (Build.VERSION.SDK_INT >= 11);
}
private ClipboardListener listener;
@Override
public boolean onCreate() {
if (Build.VERSION.SDK_INT < 11) {
return false;
}
listener = new ClipboardListener(context, device);
return true;
}
@Override
public void onDestroy() {
if (Build.VERSION.SDK_INT < 11) return;
listener.stop();
}
@Override
public boolean onPackageReceived(NetworkPackage np) {
if (!np.getType().equals(NetworkPackage.PACKAGE_TYPE_CLIPBOARD)) {
return false;
}
String content = np.getString("content");
listener.setText(content);
return true;
}
@Override
public AlertDialog getErrorDialog(Context baseContext) {
return new AlertDialog.Builder(baseContext)
.setTitle(R.string.pref_plugin_clipboard)
.setMessage(R.string.plugin_not_available)
.setPositiveButton("Ok",new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
}
})
.create();
}
@Override
public Button getInterfaceButton(Activity activity) {
return null;
}
}

View File

@@ -0,0 +1,230 @@
package org.kde.kdeconnect.Plugins.MprisPlugin;
import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.ImageButton;
import android.widget.SeekBar;
import android.widget.Spinner;
import android.widget.TextView;
import org.kde.kdeconnect.BackgroundService;
import org.kde.kdeconnect.ComputerLinks.BaseComputerLink;
import org.kde.kdeconnect.Device;
import org.kde.kdeconnect.LinkProviders.BaseLinkProvider;
import org.kde.kdeconnect.NetworkPackage;
import org.kde.kdeconnect_tp.R;
import java.util.ArrayList;
public class MprisActivity extends Activity {
//TODO: Add a loading spinner at the begginning (to distinguish the loading state from a no-players state).
//TODO 2: Add a message when no players are detected after loading completes
protected void connectToPlugin() {
final String deviceId = getIntent().getStringExtra("deviceId");
BackgroundService.RunCommand(this, new BackgroundService.InstanceCallback() {
@Override
public void onServiceStart(BackgroundService service) {
Device device = service.getDevice(deviceId);
final MprisPlugin mpris = (MprisPlugin) device.getPlugin("plugin_mpris");
if (mpris == null) {
Log.e("MprisActivity", "device has no mpris plugin!");
return;
}
mpris.setPlayerStatusUpdatedHandler(new Handler() {
@Override
public void handleMessage(Message msg) {
runOnUiThread(new Runnable() {
@Override
public void run() {
String s = mpris.getCurrentSong();
((TextView) findViewById(R.id.now_playing_textview)).setText(s);
int volume = mpris.getVolume();
((SeekBar) findViewById(R.id.volume_seek)).setProgress(volume);
boolean isPlaying = mpris.isPlaying();
if (isPlaying) {
((ImageButton) findViewById(R.id.play_button)).setImageResource(android.R.drawable.ic_media_pause);
} else {
((ImageButton) findViewById(R.id.play_button)).setImageResource(android.R.drawable.ic_media_play);
}
}
});
}
});
mpris.setPlayerListUpdatedHandler(new Handler() {
boolean firstLoad = true;
@Override
public void handleMessage(Message msg) {
final ArrayList<String> playerList = mpris.getPlayerList();
final ArrayAdapter<String> adapter = new ArrayAdapter<String>(MprisActivity.this,
android.R.layout.simple_spinner_item,
playerList.toArray(new String[playerList.size()])
);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
runOnUiThread(new Runnable() {
@Override
public void run() {
Spinner spinner = (Spinner) findViewById(R.id.player_spinner);
//String prevPlayer = (String)spinner.getSelectedItem();
spinner.setAdapter(adapter);
spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> arg0, View arg1, int pos, long id) {
((TextView) findViewById(R.id.now_playing_textview)).setText("");
String player = playerList.get(pos);
mpris.setPlayer(player);
//Spotify doesn't support changing the volume yet...
if (player.equals("Spotify")) {
findViewById(R.id.volume_layout).setVisibility(View.INVISIBLE);
} else {
findViewById(R.id.volume_layout).setVisibility(View.VISIBLE);
}
}
@Override
public void onNothingSelected(AdapterView<?> arg0) {
}
});
}
});
if (firstLoad) {
firstLoad = false;
if (playerList.size() > 0) {
mpris.setPlayer(playerList.get(0));
}
}
}
});
}
});
}
BaseLinkProvider.ConnectionReceiver connectionReceiver = new BaseLinkProvider.ConnectionReceiver() {
@Override
public void onConnectionReceived(NetworkPackage identityPackage, BaseComputerLink link) {
connectToPlugin();
}
@Override
public void onConnectionLost(BaseComputerLink link) {
}
};
@Override
protected void onDestroy() {
super.onDestroy();
BackgroundService.RunCommand(MprisActivity.this, new BackgroundService.InstanceCallback() {
@Override
public void onServiceStart(BackgroundService service) {
service.removeConnectionListener(connectionReceiver);
}
});
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.mpris_control);
final String deviceId = getIntent().getStringExtra("deviceId");
BackgroundService.RunCommand(MprisActivity.this, new BackgroundService.InstanceCallback() {
@Override
public void onServiceStart(BackgroundService service) {
service.addConnectionListener(connectionReceiver);
}
});
connectToPlugin();
findViewById(R.id.play_button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
BackgroundService.RunCommand(MprisActivity.this, new BackgroundService.InstanceCallback() {
@Override
public void onServiceStart(BackgroundService service) {
Device device = service.getDevice(deviceId);
MprisPlugin mpris = (MprisPlugin)device.getPlugin("plugin_mpris");
if (mpris == null) return;
mpris.sendAction("PlayPause");
}
});
}
});
findViewById(R.id.prev_button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
BackgroundService.RunCommand(MprisActivity.this, new BackgroundService.InstanceCallback() {
@Override
public void onServiceStart(BackgroundService service) {
Device device = service.getDevice(deviceId);
MprisPlugin mpris = (MprisPlugin)device.getPlugin("plugin_mpris");
if (mpris == null) return;
mpris.sendAction("Previous");
}
});
}
});
findViewById(R.id.next_button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
BackgroundService.RunCommand(MprisActivity.this, new BackgroundService.InstanceCallback() {
@Override
public void onServiceStart(BackgroundService service) {
Device device = service.getDevice(deviceId);
MprisPlugin mpris = (MprisPlugin)device.getPlugin("plugin_mpris");
if (mpris == null) return;
mpris.sendAction("Next");
}
});
}
});
((SeekBar)findViewById(R.id.volume_seek)).setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int i, boolean b) { }
@Override
public void onStartTrackingTouch(SeekBar seekBar) { }
@Override
public void onStopTrackingTouch(final SeekBar seekBar) {
BackgroundService.RunCommand(MprisActivity.this, new BackgroundService.InstanceCallback() {
@Override
public void onServiceStart(BackgroundService service) {
Device device = service.getDevice(deviceId);
MprisPlugin mpris = (MprisPlugin) device.getPlugin("plugin_mpris");
if (mpris == null) return;
mpris.setVolume(seekBar.getProgress());
}
});
}
});
}
}

View File

@@ -0,0 +1,217 @@
package org.kde.kdeconnect.Plugins.MprisPlugin;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import org.kde.kdeconnect.NetworkPackage;
import org.kde.kdeconnect.Plugins.Plugin;
import org.kde.kdeconnect_tp.R;
import java.util.ArrayList;
public class MprisPlugin extends Plugin {
private String currentSong = "";
private int volume = 50;
private Handler playerStatusUpdated = null;
private ArrayList<String> playerList = new ArrayList<String>();
private Handler playerListUpdated = null;
private String player = "";
private boolean playing = false;
/*static {
PluginFactory.registerPlugin(MprisPlugin.class);
}*/
@Override
public String getPluginName() {
return "plugin_mpris";
}
@Override
public String getDisplayName() {
return context.getResources().getString(R.string.pref_plugin_mpris);
}
@Override
public String getDescription() {
return context.getResources().getString(R.string.pref_plugin_mpris_desc);
}
@Override
public Drawable getIcon() {
return context.getResources().getDrawable(R.drawable.icon);
}
@Override
public boolean isEnabledByDefault() {
return true;
}
@Override
public boolean onCreate() {
requestPlayerList();
return true;
}
@Override
public void onDestroy() {
playerList.clear();
}
public void sendAction(String s) {
NetworkPackage np = new NetworkPackage(NetworkPackage.PACKAGE_TYPE_MPRIS);
np.set("player",player);
np.set("action",s);
device.sendPackage(np);
}
public void setVolume(int volume) {
NetworkPackage np = new NetworkPackage(NetworkPackage.PACKAGE_TYPE_MPRIS);
np.set("player",player);
np.set("setVolume",volume);
device.sendPackage(np);
}
@Override
public boolean onPackageReceived(NetworkPackage np) {
if (!np.getType().equals(NetworkPackage.PACKAGE_TYPE_MPRIS)) return false;
if (np.has("nowPlaying") || np.has("volume") || np.has("isPlaying")) {
if (np.getString("player").equals(player)) {
currentSong = np.getString("nowPlaying", currentSong);
volume = np.getInt("volume", volume);
playing = np.getBoolean("isPlaying", playing);
if (playerStatusUpdated != null) {
try {
playerStatusUpdated.dispatchMessage(new Message());
} catch(Exception e) {
e.printStackTrace();
Log.e("MprisControl","Exception");
playerStatusUpdated = null;
}
}
}
}
if (np.has("playerList")) {
ArrayList<String> newPlayerList = np.getStringList("playerList");
boolean equals = false;
if (newPlayerList.size() == playerList.size()) {
equals = true;
for (int i=0; i<newPlayerList.size(); i++) {
if (!newPlayerList.get(i).equals(playerList.get(i))) {
equals = false;
break;
}
}
}
if (!equals) {
playerList = newPlayerList;
if (playerListUpdated != null) {
try {
playerListUpdated.dispatchMessage(new Message());
} catch(Exception e) {
e.printStackTrace();
Log.e("MprisControl","Exception");
playerListUpdated = null;
}
}
}
}
return true;
}
public void setPlayerStatusUpdatedHandler(Handler h) {
playerStatusUpdated = h;
if (currentSong.length() > 0) h.dispatchMessage(new Message());
requestPlayerStatus();
}
public String getCurrentSong() {
return currentSong;
}
public void setPlayerListUpdatedHandler(Handler h) {
playerListUpdated = h;
if (playerList.size() > 0) h.dispatchMessage(new Message());
requestPlayerList();
}
public ArrayList<String> getPlayerList() {
return playerList;
}
public void setPlayer(String s) {
player = s;
currentSong = "";
volume = 50;
playing = false;
if (playerStatusUpdated != null) {
try {
playerStatusUpdated.dispatchMessage(new Message());
} catch(Exception e) {
e.printStackTrace();
Log.e("MprisControl","Exception");
playerStatusUpdated = null;
}
}
requestPlayerStatus();
}
public int getVolume() {
return volume;
}
public boolean isPlaying() {
return playing;
}
private void requestPlayerList() {
NetworkPackage np = new NetworkPackage(NetworkPackage.PACKAGE_TYPE_MPRIS);
np.set("requestPlayerList",true);
device.sendPackage(np);
}
private void requestPlayerStatus() {
NetworkPackage np = new NetworkPackage(NetworkPackage.PACKAGE_TYPE_MPRIS);
np.set("player",player);
np.set("requestNowPlaying",true);
np.set("requestVolume",true);
device.sendPackage(np);
}
@Override
public AlertDialog getErrorDialog(Context baseContext) {
return null;
}
@Override
public Button getInterfaceButton(final Activity activity) {
Button b = new Button(activity);
b.setText(R.string.open_mpris_controls);
b.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent intent = new Intent(activity, MprisActivity.class);
intent.putExtra("deviceId", device.getDeviceId());
activity.startActivity(intent);
}
});
return b;
}
}

View File

@@ -1,9 +1,3 @@
/*
* SPDX-FileCopyrightText: 2014 Albert Vaca Cintora <albertvaka@gmail.com>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
package org.kde.kdeconnect.Plugins.NotificationsPlugin;
import android.app.Service;
@@ -11,36 +5,29 @@ import android.content.Context;
import android.content.Intent;
import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;
import android.util.Log;
import java.util.ArrayList;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class NotificationReceiver extends NotificationListenerService {
private boolean connected;
public interface NotificationListener {
void onNotificationPosted(StatusBarNotification statusBarNotification);
void onNotificationRemoved(StatusBarNotification statusBarNotification);
void onListenerConnected(NotificationReceiver service);
}
private final ArrayList<NotificationListener> listeners = new ArrayList<>();
private ArrayList<NotificationListener> listeners = new ArrayList<NotificationListener>();
public void addListener(NotificationListener listener) {
listeners.add(listener);
}
public void removeListener(NotificationListener listener) {
listeners.remove(listener);
}
@Override
public void onNotificationPosted(StatusBarNotification statusBarNotification) {
//Log.e("NotificationReceiver.onNotificationPosted","listeners: " + listeners.size());
Log.i("NotificationReceiver.onNotificationPosted","listeners: " + listeners.size());
for(NotificationListener listener : listeners) {
listener.onNotificationPosted(statusBarNotification);
}
@@ -53,65 +40,35 @@ public class NotificationReceiver extends NotificationListenerService {
}
}
@Override
public void onListenerConnected() {
super.onListenerConnected();
for (NotificationListener listener : listeners) {
listener.onListenerConnected(this);
}
connected = true;
}
@Override
public void onListenerDisconnected() {
super.onListenerDisconnected();
connected = false;
}
public boolean isConnected() {
return connected;
}
//To use the service from the outer (name)space
//This will be called for each intent launch, even if the service is already started and is reused
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.i("NotificationReceiver", "onStartCommand");
for (InstanceCallback c : callbacks) {
c.onServiceStart(this);
}
callbacks.clear();
return Service.START_STICKY;
}
public interface InstanceCallback {
void onServiceStart(NotificationReceiver service);
}
private final static ArrayList<InstanceCallback> callbacks = new ArrayList<>();
private final static Lock mutex = new ReentrantLock(true);
//This will be called for each intent launch, even if the service is already started and is reused
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
//Log.e("NotificationReceiver", "onStartCommand");
mutex.lock();
try {
for (InstanceCallback c : callbacks) {
c.onServiceStart(this);
}
callbacks.clear();
} finally {
mutex.unlock();
}
return Service.START_STICKY;
}
private static ArrayList<InstanceCallback> callbacks = new ArrayList<InstanceCallback>();
public static void Start(Context c) {
RunCommand(c, null);
}
public static void RunCommand(Context c, final InstanceCallback callback) {
if (callback != null) {
mutex.lock();
try {
callbacks.add(callback);
} finally {
mutex.unlock();
}
}
if (callback != null) callbacks.add(callback);
Intent serviceIntent = new Intent(c, NotificationReceiver.class);
c.startService(serviceIntent);
}

View File

@@ -0,0 +1,317 @@
package org.kde.kdeconnect.Plugins.NotificationsPlugin;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Notification;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.provider.Settings;
import android.service.notification.StatusBarNotification;
import android.util.Base64;
import android.util.Log;
import android.widget.Button;
import org.kde.kdeconnect.Helpers.AppsHelper;
import org.kde.kdeconnect.Helpers.ImagesHelper;
import org.kde.kdeconnect.NetworkPackage;
import org.kde.kdeconnect.Plugins.Plugin;
import org.kde.kdeconnect_tp.R;
import java.io.ByteArrayOutputStream;
import java.nio.charset.Charset;
public class NotificationsPlugin extends Plugin implements NotificationReceiver.NotificationListener {
/*static {
PluginFactory.registerPlugin(NotificationsPlugin.class);
}*/
@Override
public String getPluginName() {
return "plugin_notifications";
}
@Override
public String getDisplayName() {
return context.getResources().getString(R.string.pref_plugin_notifications);
}
@Override
public String getDescription() {
return context.getResources().getString(R.string.pref_plugin_notifications_desc);
}
@Override
public Drawable getIcon() {
return context.getResources().getDrawable(R.drawable.icon);
}
@Override
public boolean isEnabledByDefault() {
return true;
}
static class NotificationId {
String packageName;
String tag;
int id;
public static NotificationId fromNotification(StatusBarNotification statusBarNotification) {
NotificationId nid = new NotificationId();
nid.packageName = statusBarNotification.getPackageName();
nid.tag = statusBarNotification.getTag();
nid.id = statusBarNotification.getId();
return nid;
}
public static NotificationId unserialize(String s) {
NotificationId nid = new NotificationId();
int first = s.indexOf(':');
int last = s.lastIndexOf(':');
nid.packageName = s.substring(0, first);
nid.tag = s.substring(first+1, last);
if (nid.tag.length() == 0) nid.tag = null;
String idString = s.substring(last+1);
try {
nid.id = Integer.parseInt(idString);
} catch(Exception e) {
nid.id = 0;
}
//Log.e("NotificationId","unserialize: " + nid.packageName+ ", "+nid.tag+ ", "+nid.id);
return nid;
}
public String serialize() {
//Log.e("NotificationId","serialize: " + packageName+ ", "+tag+ ", "+id);
String safePackageName = (packageName == null)? "" : packageName;
String safeTag = (tag == null)? "" : tag;
return safePackageName+":"+safeTag+":"+id;
}
public String getPackageName() {
return packageName;
}
public String getTag() {
return tag;
}
public int getId() {
return id;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof NotificationId)) return false;
NotificationId other = (NotificationId)o;
return other.getTag().equals(tag) && other.getId() == id && other.getPackageName().equals(packageName);
}
}
@Override
public boolean onCreate() {
if (Build.VERSION.SDK_INT < 18) return false;
//Check for permissions
String notificationListenerList = Settings.Secure.getString(context.getContentResolver(), "enabled_notification_listeners");
if (notificationListenerList != null && notificationListenerList.contains(context.getPackageName())) {
NotificationReceiver.RunCommand(context, new NotificationReceiver.InstanceCallback() {
@Override
public void onServiceStart(NotificationReceiver service) {
try {
service.addListener(NotificationsPlugin.this);
/*
StatusBarNotification[] notifications = service.getActiveNotifications();
for (StatusBarNotification notification : notifications) {
onNotificationPosted(notification);
}
*/
} catch(Exception e) {
e.printStackTrace();
Log.e("NotificationsPlugin","Exception");
}
}
});
return true;
} else {
return false;
}
}
@Override
public void onDestroy() {
NotificationReceiver.RunCommand(context, new NotificationReceiver.InstanceCallback() {
@Override
public void onServiceStart(NotificationReceiver service) {
service.removeListener(NotificationsPlugin.this);
}
});
}
@Override
public void onNotificationRemoved(StatusBarNotification statusBarNotification) {
NotificationId id = NotificationId.fromNotification(statusBarNotification);
NetworkPackage np = new NetworkPackage(NetworkPackage.PACKAGE_TYPE_NOTIFICATION);
np.set("id", id.serialize());
np.set("isCancel", true);
device.sendPackage(np);
}
@Override
public void onNotificationPosted(StatusBarNotification statusBarNotification) {
onNotificationPosted(statusBarNotification, false);
}
public void onNotificationPosted(StatusBarNotification statusBarNotification, boolean requestAnswer) {
Notification notification = statusBarNotification.getNotification();
NotificationId id = NotificationId.fromNotification(statusBarNotification);
NetworkPackage np = new NetworkPackage(NetworkPackage.PACKAGE_TYPE_NOTIFICATION);
String packageName = statusBarNotification.getPackageName();
String appName = AppsHelper.appNameLookup(context, packageName);
try {
Drawable drawableAppIcon = AppsHelper.appIconLookup(context, packageName);
Bitmap appIcon = ImagesHelper.drawableToBitmap(drawableAppIcon);
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
appIcon.compress(Bitmap.CompressFormat.PNG, 90, outStream);
byte[] bitmapData = outStream.toByteArray();
byte[] serializedBitmapData = Base64.encode(bitmapData, Base64.NO_WRAP);
String stringBitmapData = new String(serializedBitmapData, Charset.defaultCharset());
//The icon is super big, better sending it as a file transfer when we support that
//np.set("base64icon", stringBitmapData);
} catch(Exception e) {
e.printStackTrace();
Log.e("NotificationsPlugin","Error retrieving icon");
}
np.set("id", id.serialize());
np.set("appName", appName == null? packageName : appName);
np.set("isClearable", statusBarNotification.isClearable());
np.set("ticker", notification.tickerText.toString());
np.set("time", Long.toString(statusBarNotification.getPostTime()));
if (requestAnswer) np.set("requestAnswer", true);
device.sendPackage(np);
}
@Override
public boolean onPackageReceived(final NetworkPackage np) {
if (!np.getType().equals(NetworkPackage.PACKAGE_TYPE_NOTIFICATION)) return false;
if (np.getBoolean("request")) {
NotificationReceiver.RunCommand(context, new NotificationReceiver.InstanceCallback() {
private void sendCurrentNotifications(NotificationReceiver service) {
StatusBarNotification[] notifications = service.getActiveNotifications();
for (StatusBarNotification notification : notifications) {
onNotificationPosted(notification, true);
}
}
@Override
public void onServiceStart(final NotificationReceiver service) {
try {
//If service just started, this call will throw an exception because the answer is not ready yet
sendCurrentNotifications(service);
} catch(Exception e) {
Log.e("onPackageReceived","Error when answering 'request': Service failed to start. Retrying in 100ms...");
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(100);
Log.e("onPackageReceived","Error when answering 'request': Service failed to start. Retrying...");
sendCurrentNotifications(service);
} catch (Exception e) {
Log.e("onPackageReceived","Error when answering 'request': Service failed to start twice!");
e.printStackTrace();
}
}
}).start();
}
}
});
} else if (np.has("cancel")) {
NotificationReceiver.RunCommand(context, new NotificationReceiver.InstanceCallback() {
@Override
public void onServiceStart(NotificationReceiver service) {
NotificationId dismissedId = NotificationId.unserialize(np.getString("cancel"));
service.cancelNotification(dismissedId.getPackageName(), dismissedId.getTag(), dismissedId.getId());
}
});
} else {
Log.w("NotificationsPlugin","Nothing to do");
}
return true;
}
@Override
public AlertDialog getErrorDialog(final Context baseContext) {
if (Build.VERSION.SDK_INT < 18) {
return new AlertDialog.Builder(baseContext)
.setTitle(R.string.pref_plugin_notifications)
.setMessage(R.string.plugin_not_available)
.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
}
})
.create();
} else {
return new AlertDialog.Builder(baseContext)
.setTitle(R.string.pref_plugin_notifications)
.setMessage(R.string.no_permissions)
.setPositiveButton(R.string.open_settings, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
Intent intent = new Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS");
baseContext.startActivity(intent);
}
})
.setNegativeButton(R.string.cancel,new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
//Do nothing
}
})
.create();
}
}
@Override
public Button getInterfaceButton(Activity activity) {
return null;
}
}

View File

@@ -0,0 +1,101 @@
package org.kde.kdeconnect.Plugins.PingPlugin;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Notification;
import android.app.NotificationManager;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.support.v4.app.NotificationCompat;
import android.view.View;
import android.widget.Button;
import org.kde.kdeconnect.NetworkPackage;
import org.kde.kdeconnect.Plugins.Plugin;
import org.kde.kdeconnect_tp.R;
public class PingPlugin extends Plugin {
/*static {
PluginFactory.registerPlugin(PingPlugin.class);
}*/
@Override
public String getPluginName() {
return "plugin_ping";
}
@Override
public String getDisplayName() {
return context.getResources().getString(R.string.pref_plugin_ping);
}
@Override
public String getDescription() {
return context.getResources().getString(R.string.pref_plugin_ping_desc);
}
@Override
public Drawable getIcon() {
return context.getResources().getDrawable(R.drawable.icon);
}
@Override
public boolean isEnabledByDefault() {
return true;
}
@Override
public boolean onCreate() {
return true;
}
@Override
public void onDestroy() {
}
@Override
public boolean onPackageReceived(NetworkPackage np) {
//Log.e("PingPackageReceiver", "onPackageReceived");
if (np.getType().equals(NetworkPackage.PACKAGE_TYPE_PING)) {
//Log.e("PingPackageReceiver", "was a ping!");
Notification noti = new NotificationCompat.Builder(context)
.setContentTitle(device.getName())
.setContentText("Ping!")
.setTicker("Ping!")
.setSmallIcon(android.R.drawable.ic_dialog_alert)
.setAutoCancel(true)
.setDefaults(Notification.DEFAULT_SOUND)
.build();
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.notify(42 /*a unique id to create only one notification*/, noti);
return true;
}
return false;
}
@Override
public AlertDialog getErrorDialog(Context baseContext) {
return null;
}
@Override
public Button getInterfaceButton(Activity activity) {
Button b = new Button(activity);
b.setText(R.string.send_ping);
b.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
device.sendPackage(new NetworkPackage(NetworkPackage.PACKAGE_TYPE_PING));
}
});
return b;
}
}

View File

@@ -0,0 +1,87 @@
package org.kde.kdeconnect.Plugins;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.widget.Button;
import org.kde.kdeconnect.Device;
import org.kde.kdeconnect.NetworkPackage;
public abstract class Plugin {
protected Device device;
protected Context context;
public void setContext(Context context, Device device) {
this.device = device;
this.context = context;
}
/**
* Return the internal plugin name, that will be used as a
* unique key to distinguish it. This function can not access
* this.context nor this.device.
*/
public abstract String getPluginName();
/**
* 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 an icon associated to this plugin. This function can
* access this.context to load the image from resources.
*/
public abstract Drawable getIcon();
/**
* 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 abstract boolean isEnabledByDefault();
/**
* Initialize the listeners and structures in your plugin.
* Should return true if initialization was successful.
*/
public abstract boolean onCreate();
/**
* Finish any ongoing operations, remove listeners... so
* this object could be garbage collected.
*/
public abstract void onDestroy();
/**
* If onCreate returns false, should create a dialog explaining
* the problem (and how to fix it, if possible) to the user.
*/
public abstract boolean onPackageReceived(NetworkPackage np);
/**
* If onCreate returns false, should create a dialog explaining
* the problem (and how to fix it, if possible) to the user.
*/
public abstract AlertDialog getErrorDialog(Context baseContext);
/**
* 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.
*/
public abstract Button getInterfaceButton(Activity activity);
}

View File

@@ -0,0 +1,123 @@
package org.kde.kdeconnect.Plugins;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.util.Log;
import org.kde.kdeconnect.Device;
import org.kde.kdeconnect.Plugins.BatteryPlugin.BatteryPlugin;
import org.kde.kdeconnect.Plugins.ClibpoardPlugin.ClipboardPlugin;
import org.kde.kdeconnect.Plugins.MprisPlugin.MprisPlugin;
import org.kde.kdeconnect.Plugins.NotificationsPlugin.NotificationsPlugin;
import org.kde.kdeconnect.Plugins.PingPlugin.PingPlugin;
import org.kde.kdeconnect.Plugins.TelephonyPlugin.TelephonyPlugin;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
public class PluginFactory {
public static class PluginInfo {
public PluginInfo(String pluginName, String displayName, String description, Drawable icon, boolean enabledByDefault) {
this.pluginName = pluginName;
this.displayName = displayName;
this.description = description;
this.icon = icon;
this.enabledByDefault = enabledByDefault;
}
public String getPluginName() {
return pluginName;
}
public String getDisplayName() {
return displayName;
}
public String getDescription() {
return description;
}
public Drawable getIcon() {
return icon;
}
public boolean isEnabledByDefault() {
return enabledByDefault;
}
private String pluginName;
private String displayName;
private String description;
private final Drawable icon;
private boolean enabledByDefault;
}
private static final Map<String, Class> availablePlugins = new TreeMap<String, Class>();
private static final Map<String, PluginInfo> availablePluginsInfo = new TreeMap<String, PluginInfo>();
static {
//TODO: Avoid this factory having to know every plugin
PluginFactory.registerPlugin(TelephonyPlugin.class);
PluginFactory.registerPlugin(PingPlugin.class);
PluginFactory.registerPlugin(MprisPlugin.class);
PluginFactory.registerPlugin(ClipboardPlugin.class);
//PluginFactory.registerPlugin(BatteryPlugin.class);
PluginFactory.registerPlugin(NotificationsPlugin.class);
}
public static PluginInfo getPluginInfo(Context context, String pluginName) {
PluginInfo info = availablePluginsInfo.get(pluginName); //Is it cached?
if (info != null) return info;
try {
Plugin p = ((Plugin)availablePlugins.get(pluginName).newInstance());
p.setContext(context, null);
info = new PluginInfo(pluginName, p.getDisplayName(), p.getDescription(), p.getIcon(), p.isEnabledByDefault());
availablePluginsInfo.put(pluginName, info); //Cache it
return info;
} catch(Exception e) {
e.printStackTrace();
Log.e("PluginFactory","getPluginInfo exception");
return null;
}
}
public static Set<String> getAvailablePlugins() {
return availablePlugins.keySet();
}
public static Plugin instantiatePluginForDevice(Context context, String pluginName, Device device) {
Class c = availablePlugins.get(pluginName);
if (c == null) {
Log.e("PluginFactory", "Plugin not found: "+pluginName);
return null;
}
try {
Plugin plugin = (Plugin)c.newInstance();
plugin.setContext(context, device);
return plugin;
} catch(Exception e) {
e.printStackTrace();
Log.e("PluginFactory", "Could not instantiate plugin: "+pluginName);
return null;
}
}
public static void registerPlugin(Class pluginClass) {
try {
//I hate this but I need to create an instance because abstract static functions can't be declared
String pluginName = ((Plugin)pluginClass.newInstance()).getPluginName();
availablePlugins.put(pluginName, pluginClass);
} catch(Exception e) {
Log.e("PluginFactory","addPlugin exception");
e.printStackTrace();
}
}
}

View File

@@ -0,0 +1,191 @@
package org.kde.kdeconnect.Plugins.TelephonyPlugin;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.telephony.SmsMessage;
import android.telephony.TelephonyManager;
import android.util.Log;
import android.widget.Button;
import org.kde.kdeconnect.Helpers.ContactsHelper;
import org.kde.kdeconnect.NetworkPackage;
import org.kde.kdeconnect.Plugins.Plugin;
import org.kde.kdeconnect_tp.R;
public class TelephonyPlugin extends Plugin {
/*static {
PluginFactory.registerPlugin(TelephonyPlugin.class);
}*/
@Override
public String getPluginName() {
return "plugin_telephony";
}
@Override
public String getDisplayName() {
return context.getResources().getString(R.string.pref_plugin_telephony);
}
@Override
public String getDescription() {
return context.getResources().getString(R.string.pref_plugin_telephony_desc);
}
@Override
public Drawable getIcon() {
return context.getResources().getDrawable(R.drawable.icon);
}
@Override
public boolean isEnabledByDefault() {
return true;
}
private BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
//Log.e("TelephonyPlugin","Telephony event: " + action);
if("android.provider.Telephony.SMS_RECEIVED".equals(action)) {
final Bundle bundle = intent.getExtras();
if (bundle == null) return;
final Object[] pdus = (Object[]) bundle.get("pdus");
for (Object pdu : pdus) {
SmsMessage message = SmsMessage.createFromPdu((byte[])pdu);
smsBroadcastReceived(message);
}
} else if (TelephonyManager.ACTION_PHONE_STATE_CHANGED.equals(action)) {
String state = intent.getStringExtra(TelephonyManager.EXTRA_STATE);
int intState = TelephonyManager.CALL_STATE_IDLE;
if (state.equals(TelephonyManager.EXTRA_STATE_RINGING))
intState = TelephonyManager.CALL_STATE_RINGING;
else if (state.equals(TelephonyManager.EXTRA_STATE_OFFHOOK))
intState = TelephonyManager.CALL_STATE_OFFHOOK;
String number = intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER);
if (number == null) number = intent.getStringExtra(TelephonyManager.EXTRA_INCOMING_NUMBER);
final int finalIntState = intState;
final String finalNumber = number;
callBroadcastReceived(finalIntState, finalNumber);
}
}
};
private int lastState = TelephonyManager.CALL_STATE_IDLE;
private NetworkPackage lastPackage = null;
public void callBroadcastReceived(int state, String phoneNumber) {
NetworkPackage np = new NetworkPackage(NetworkPackage.PACKAGE_TYPE_TELEPHONY);
if (phoneNumber != null) {
phoneNumber = ContactsHelper.phoneNumberLookup(context,phoneNumber);
np.set("phoneNumber", phoneNumber);
}
switch (state) {
case TelephonyManager.CALL_STATE_RINGING:
np.set("event", "ringing");
device.sendPackage(np);
break;
case TelephonyManager.CALL_STATE_OFFHOOK: //Ongoing call
np.set("event", "talking");
device.sendPackage(np);
break;
case TelephonyManager.CALL_STATE_IDLE:
if (lastState != TelephonyManager.CALL_STATE_IDLE && lastPackage != null) {
//Resend a cancel of the last event (can either be "ringing" or "talking")
lastPackage.set("isCancel","true");
device.sendPackage(lastPackage);
//Emit a missed call notification if needed
if (lastState == TelephonyManager.CALL_STATE_RINGING) {
np.set("event","missedCall");
np.set("phoneNumber", lastPackage.getString("phoneNumber",null));
device.sendPackage(np);
}
}
break;
}
lastPackage = np;
lastState = state;
}
public void smsBroadcastReceived(SmsMessage message) {
//Log.e("SmsBroadcastReceived", message.toString());
NetworkPackage np = new NetworkPackage(NetworkPackage.PACKAGE_TYPE_TELEPHONY);
np.set("event","sms");
String messageBody = message.getMessageBody();
if (messageBody != null) {
np.set("messageBody",messageBody);
}
String phoneNumber = message.getOriginatingAddress();
if (phoneNumber != null) {
phoneNumber = ContactsHelper.phoneNumberLookup(context, phoneNumber);
np.set("phoneNumber",phoneNumber);
}
device.sendPackage(np);
}
@Override
public boolean onCreate() {
IntentFilter filter = new IntentFilter("android.provider.Telephony.SMS_RECEIVED");
filter.addAction(TelephonyManager.ACTION_PHONE_STATE_CHANGED);
context.registerReceiver(receiver, filter);
return true;
}
@Override
public void onDestroy() {
context.unregisterReceiver(receiver);
}
@Override
public boolean onPackageReceived(NetworkPackage np) {
//Do nothing
return false;
}
@Override
public AlertDialog getErrorDialog(Context baseContext) {
return null;
}
@Override
public Button getInterfaceButton(Activity activity) {
return null;
}
}

View File

@@ -0,0 +1,148 @@
package org.kde.kdeconnect.UserInterface;
import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.ActionBar;
import android.support.v7.app.ActionBarActivity;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.ListView;
import android.widget.TextView;
import org.kde.kdeconnect.BackgroundService;
import org.kde.kdeconnect.Device;
import org.kde.kdeconnect.Plugins.Plugin;
import org.kde.kdeconnect.UserInterface.List.ButtonItem;
import org.kde.kdeconnect.UserInterface.List.ListAdapter;
import org.kde.kdeconnect.UserInterface.List.SectionItem;
import org.kde.kdeconnect_tp.R;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
public class DeviceActivity extends ActionBarActivity {
private String deviceId;
private Device device;
private Device.PluginsChangedListener pluginsChangedListener = new Device.PluginsChangedListener() {
@Override
public void onPluginsChanged(final Device device) {
runOnUiThread(new Runnable() {
@Override
public void run() {
//Errors list
final HashMap<String, Plugin> failedPlugins = device.getFailedPlugins();
final String[] ids = failedPlugins.keySet().toArray(new String[failedPlugins.size()]);
String[] names = new String[failedPlugins.size()];
for(int i = 0; i < ids.length; i++) {
Plugin p = failedPlugins.get(ids[i]);
names[i] = p.getDisplayName();
}
ListView errorList = (ListView)findViewById(R.id.errors_list);
if (!failedPlugins.isEmpty() && errorList.getHeaderViewsCount() == 0) {
TextView header = new TextView(DeviceActivity.this);
header.setPadding(0,24,0,0);
header.setText(getResources().getString(R.string.plugins_failed_to_load));
errorList.addHeaderView(header);
}
errorList.setAdapter(new ArrayAdapter<String>(DeviceActivity.this, android.R.layout.simple_list_item_1, names));
errorList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) {
Plugin p = failedPlugins.get(ids[position - 1]); //Header is position 0, so we have to substract one
p.getErrorDialog(DeviceActivity.this).show();
}
});
//Buttons list
ArrayList<ListAdapter.Item> items = new ArrayList<ListAdapter.Item>();
final Collection<Plugin> plugins = device.getLoadedPlugins().values();
for (Plugin p : plugins) {
Button b = p.getInterfaceButton(DeviceActivity.this);
if (b != null) {
items.add(new SectionItem(p.getDisplayName()));
items.add(new ButtonItem(b));
}
}
ListView buttonsList = (ListView)findViewById(R.id.buttons_list);
buttonsList.setAdapter(new ListAdapter(DeviceActivity.this, items));
}
});
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_device);
ActionBar actionBar = getSupportActionBar();
actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_HOME | ActionBar.DISPLAY_SHOW_TITLE);
actionBar.setDisplayHomeAsUpEnabled(true);
deviceId = getIntent().getStringExtra("deviceId");
BackgroundService.RunCommand(DeviceActivity.this, new BackgroundService.InstanceCallback() {
@Override
public void onServiceStart(BackgroundService service) {
device = service.getDevice(deviceId);
setTitle(device.getName());
device.addPluginsChangedListener(pluginsChangedListener);
pluginsChangedListener.onPluginsChanged(device);
}
});
}
@Override
protected void onDestroy() {
BackgroundService.RunCommand(DeviceActivity.this, new BackgroundService.InstanceCallback() {
@Override
public void onServiceStart(BackgroundService service) {
Device device = service.getDevice(deviceId);
device.removePluginsChangedListener(pluginsChangedListener);
}
});
super.onDestroy();
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
menu.clear();
if (device.isPaired()) {
menu.add(R.string.device_menu_plugins).setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem menuItem) {
Intent intent = new Intent(DeviceActivity.this, SettingsActivity.class);
intent.putExtra("deviceId", deviceId);
startActivity(intent);
return true;
}
});
menu.add(R.string.device_menu_unpair).setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem menuItem) {
device.unpair();
finish();
return true;
}
});
return true;
} else {
return false;
}
}
}

View File

@@ -0,0 +1,20 @@
package org.kde.kdeconnect.UserInterface.List;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
public class ButtonItem implements ListAdapter.Item {
private final Button button;
public ButtonItem(Button b) {
this.button = b;
}
@Override
public View inflateView(LayoutInflater layoutInflater) {
return button;
}
}

View File

@@ -0,0 +1,49 @@
package org.kde.kdeconnect.UserInterface.List;
import android.app.Activity;
import android.content.Intent;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.TextView;
import org.kde.kdeconnect.Device;
import org.kde.kdeconnect.UserInterface.DeviceActivity;
import org.kde.kdeconnect.UserInterface.PairActivity;
import org.kde.kdeconnect_tp.R;
public class DeviceItem implements ListAdapter.Item {
private final Device device;
private final Activity activity;
public DeviceItem(Activity activity, Device device) {
this.device = device;
this.activity = activity;
}
@Override
public View inflateView(LayoutInflater layoutInflater) {
View v = layoutInflater.inflate(R.layout.list_item_entry, null);
TextView titleView = (TextView)v.findViewById(R.id.list_item_entry_title);
if (titleView != null) titleView.setText(device.getName());
v.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent intent;
if (device.isPaired()) {
intent = new Intent(activity, DeviceActivity.class);
} else {
intent = new Intent(activity, PairActivity.class);
}
intent.putExtra("deviceId", device.getDeviceId());
activity.startActivity(intent);
}
});
return v;
}
}

View File

@@ -0,0 +1,33 @@
package org.kde.kdeconnect.UserInterface.List;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import java.util.ArrayList;
public class ListAdapter extends ArrayAdapter<ListAdapter.Item> {
public interface Item {
public View inflateView(LayoutInflater layoutInflater);
}
private ArrayList<Item> items;
private LayoutInflater layoutInflater;
public ListAdapter(Context context, ArrayList<Item> items) {
super(context, 0, items);
this.items = items;
layoutInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
final Item i = items.get(position);
return i.inflateView(layoutInflater);
}
}

View File

@@ -0,0 +1,37 @@
package org.kde.kdeconnect.UserInterface.List;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.TextView;
import org.kde.kdeconnect_tp.R;
public class SectionItem implements ListAdapter.Item {
private final String title;
public boolean isEmpty;
public SectionItem(String title) {
this.title = title;
}
@Override
public View inflateView(LayoutInflater layoutInflater) {
View v = layoutInflater.inflate(R.layout.list_item_category, null);
v.setOnClickListener(null);
v.setOnLongClickListener(null);
v.setLongClickable(false);
TextView sectionView = (TextView) v.findViewById(R.id.list_item_category_text);
sectionView.setText(title);
if (isEmpty) {
v.findViewById(R.id.list_item_category_empty_placeholder).setVisibility(View.VISIBLE);
}
return v;
}
}

View File

@@ -0,0 +1,189 @@
package org.kde.kdeconnect.UserInterface;
import android.content.res.Resources;
import android.os.Bundle;
import android.support.v7.app.ActionBar;
import android.support.v7.app.ActionBarActivity;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ListView;
import org.kde.kdeconnect.BackgroundService;
import org.kde.kdeconnect.Device;
import org.kde.kdeconnect.UserInterface.List.DeviceItem;
import org.kde.kdeconnect.UserInterface.List.ListAdapter;
import org.kde.kdeconnect.UserInterface.List.SectionItem;
import org.kde.kdeconnect_tp.R;
import java.util.ArrayList;
import java.util.Collection;
public class MainActivity extends ActionBarActivity {
//
// Action bar
//
private MenuItem menuProgress;
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.main, menu);
menuProgress = menu.findItem(R.id.menu_progress);
return true;
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_refresh:
updateComputerList();
BackgroundService.RunCommand(MainActivity.this, new BackgroundService.InstanceCallback() {
@Override
public void onServiceStart(BackgroundService service) {
service.onNetworkChange();
}
});
item.setVisible(false);
menuProgress.setVisible(true);
new Thread(new Runnable() {
@Override
public void run() {
try { Thread.sleep(1500); } catch (InterruptedException e) { }
runOnUiThread(new Runnable() {
@Override
public void run() {
menuProgress.setVisible(false);
item.setVisible(true);
}
});
}
}).start();
break;
default:
break;
}
return true;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ActionBar actionBar = getSupportActionBar();
actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_HOME | ActionBar.DISPLAY_SHOW_TITLE | ActionBar.DISPLAY_SHOW_CUSTOM);
}
//
// Device list
//
void updateComputerList() {
BackgroundService.RunCommand(MainActivity.this, new BackgroundService.InstanceCallback() {
@Override
public void onServiceStart(final BackgroundService service) {
Collection<Device> devices = service.getDevices().values();
final ArrayList<ListAdapter.Item> items = new ArrayList<ListAdapter.Item>();
SectionItem section;
Resources res = getResources();
section = new SectionItem(res.getString(R.string.category_connected_devices));
section.isEmpty = true;
items.add(section);
for(Device d : devices) {
if (d.isReachable() && d.isPaired()) {
items.add(new DeviceItem(MainActivity.this, d));
section.isEmpty = false;
}
}
section = new SectionItem(res.getString(R.string.category_not_paired_devices));
section.isEmpty = true;
items.add(section);
for(Device d : devices) {
if (d.isReachable() && !d.isPaired()) {
items.add(new DeviceItem(MainActivity.this, d));
section.isEmpty = false;
}
}
section = new SectionItem(res.getString(R.string.category_remembered_devices));
section.isEmpty = true;
items.add(section);
for(Device d : devices) {
if (!d.isReachable() && d.isPaired()) {
items.add(new DeviceItem(MainActivity.this, d));
section.isEmpty = false;
}
}
if (section.isEmpty) {
items.remove(items.size()-1); //Remove section
}
runOnUiThread(new Runnable() {
@Override
public void run() {
ListView list = (ListView)findViewById(R.id.listView1);
list.setAdapter(new ListAdapter(MainActivity.this, items));
list.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
view.callOnClick();
}
});
}
});
}
});
}
@Override
protected void onStart() {
super.onStart();
BackgroundService.RunCommand(MainActivity.this, new BackgroundService.InstanceCallback() {
@Override
public void onServiceStart(BackgroundService service) {
service.onNetworkChange();
service.setDeviceListChangedCallback(new BackgroundService.DeviceListChangedCallback() {
@Override
public void onDeviceListChanged() {
updateComputerList();
}
});
}
});
}
@Override
protected void onStop() {
BackgroundService.RunCommand(MainActivity.this, new BackgroundService.InstanceCallback() {
@Override
public void onServiceStart(BackgroundService service) {
service.setDeviceListChangedCallback(null);
}
});
super.onStop();
}
@Override
protected void onResume() {
super.onResume();
updateComputerList();
}
}

View File

@@ -0,0 +1,148 @@
package org.kde.kdeconnect.UserInterface;
import android.app.NotificationManager;
import android.content.Context;
import android.os.Bundle;
import android.support.v7.app.ActionBar;
import android.support.v7.app.ActionBarActivity;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import org.kde.kdeconnect.BackgroundService;
import org.kde.kdeconnect.Device;
import org.kde.kdeconnect_tp.R;
public class PairActivity extends ActionBarActivity {
private String deviceId;
private Device device = null;
private Device.PairingCallback pairingCallback = new Device.PairingCallback() {
@Override
public void incomingRequest() {
runOnUiThread(new Runnable() {
@Override
public void run() {
((TextView) findViewById(R.id.pair_message)).setText(R.string.pair_requested);
findViewById(R.id.pair_progress).setVisibility(View.GONE);
findViewById(R.id.pair_button).setVisibility(View.GONE);
findViewById(R.id.pair_request).setVisibility(View.VISIBLE);
}
});
NotificationManager notificationManager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel(device.getNotificationId());
}
@Override
public void pairingSuccessful() {
finish();
}
@Override
public void pairingFailed(final String error) {
runOnUiThread(new Runnable() {
@Override
public void run() {
((TextView) findViewById(R.id.pair_message)).setText(error);
findViewById(R.id.pair_progress).setVisibility(View.GONE);
findViewById(R.id.pair_button).setVisibility(View.VISIBLE);
findViewById(R.id.pair_request).setVisibility(View.GONE);
}
});
}
@Override
public void unpaired() {
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_pair);
ActionBar actionBar = getSupportActionBar();
actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_HOME | ActionBar.DISPLAY_SHOW_TITLE);
actionBar.setDisplayHomeAsUpEnabled(true);
deviceId = getIntent().getStringExtra("deviceId");
BackgroundService.RunCommand(PairActivity.this, new BackgroundService.InstanceCallback() {
@Override
public void onServiceStart(BackgroundService service) {
device = service.getDevice(deviceId);
setTitle(device.getName());
NotificationManager notificationManager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel(device.getNotificationId());
}
});
final Button pairButton = (Button)findViewById(R.id.pair_button);
pairButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
pairButton.setVisibility(View.GONE);
((TextView) findViewById(R.id.pair_message)).setText("");
findViewById(R.id.pair_progress).setVisibility(View.VISIBLE);
BackgroundService.RunCommand(PairActivity.this, new BackgroundService.InstanceCallback() {
@Override
public void onServiceStart(BackgroundService service) {
device = service.getDevice(deviceId);
device.requestPairing();
}
});
}
});
findViewById(R.id.accept_button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
BackgroundService.RunCommand(PairActivity.this, new BackgroundService.InstanceCallback() {
@Override
public void onServiceStart(BackgroundService service) {
device.acceptPairing();
finish();
}
});
}
});
findViewById(R.id.reject_button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
BackgroundService.RunCommand(PairActivity.this, new BackgroundService.InstanceCallback() {
@Override
public void onServiceStart(BackgroundService service) {
device.rejectPairing();
finish();
}
});
}
});
}
@Override
protected void onStart() {
super.onStart();
BackgroundService.RunCommand(PairActivity.this, new BackgroundService.InstanceCallback() {
@Override
public void onServiceStart(BackgroundService service) {
device.addPairingCallback(pairingCallback);
}
});
}
@Override
protected void onStop() {
if (device != null) device.removePairingCallback(pairingCallback);
super.onStop();
}
}

View File

@@ -0,0 +1,28 @@
package org.kde.kdeconnect.UserInterface;
import android.content.Context;
import android.preference.Preference;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import java.util.ArrayList;
public class PreferenceListAdapter extends ArrayAdapter<Preference> {
private ArrayList<Preference> localList;
public PreferenceListAdapter(Context context, ArrayList<Preference> items) {
super(context,0, items);
localList = items;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
Preference preference = localList.get(position);
return preference.getView(convertView, parent);
}
}

View File

@@ -0,0 +1,70 @@
package org.kde.kdeconnect.UserInterface;
import android.app.ListActivity;
import android.os.Bundle;
import android.preference.CheckBoxPreference;
import android.preference.Preference;
import android.util.Log;
import android.view.View;
import android.widget.AdapterView;
import org.kde.kdeconnect.BackgroundService;
import org.kde.kdeconnect.Device;
import org.kde.kdeconnect.Plugins.PluginFactory;
import java.util.ArrayList;
import java.util.Set;
public class SettingsActivity extends ListActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final String deviceId = getIntent().getStringExtra("deviceId");
BackgroundService.RunCommand(getApplicationContext(), new BackgroundService.InstanceCallback() {
@Override
public void onServiceStart(BackgroundService service) {
final Device device = service.getDevice(deviceId);
Set<String> plugins = PluginFactory.getAvailablePlugins();
final ArrayList<Preference> preferences = new ArrayList<Preference>();
for (final String pluginName : plugins) {
CheckBoxPreference pref = new CheckBoxPreference(getBaseContext());
PluginFactory.PluginInfo info = PluginFactory.getPluginInfo(getBaseContext(), pluginName);
pref.setKey(pluginName);
pref.setTitle(info.getDisplayName());
pref.setSummary(info.getDescription());
pref.setChecked(device.isPluginEnabled(pluginName));
preferences.add(pref);
}
setListAdapter(new PreferenceListAdapter(SettingsActivity.this, preferences));
getListView().setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
CheckBoxPreference pref = (CheckBoxPreference)preferences.get(i);
boolean enabled = device.isPluginEnabled(pref.getKey());
device.setPluginEnabled(pref.getKey(), !enabled);
pref.setChecked(!enabled);
getListAdapter().getView(i, view, null); //This will refresh the view (yes, this is the way to do it)
}
});
getListView().setPadding(16,16,16,16);
}
});
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,26 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
tools:context=".MainActivity"
android:orientation="vertical">
<ListView
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:id="@+id/buttons_list"
android:layout_weight="1"
/>
<ListView
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:id="@+id/errors_list"
android:layout_weight="1"
/>
</LinearLayout>

View File

@@ -0,0 +1,14 @@
<ListView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
tools:context=".MainActivity"
android:id="@+id/listView1"
android:addStatesFromChildren="true"
android:orientation="vertical">
</ListView>

View File

@@ -0,0 +1,60 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
tools:context=".MainActivity"
android:orientation="vertical">
<ProgressBar
android:visibility="gone"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:id="@+id/pair_progress" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:text="@string/device_not_paired"
android:id="@+id/pair_message"
android:layout_gravity="left|center_vertical"
/>
<Button
android:id="@+id/pair_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/request_pairing"
/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
android:id="@+id/pair_request"
android:layout_gravity="center">
<Button
android:id="@+id/accept_button"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/pairing_accept"
android:layout_weight="1"
/>
<Button
android:id="@+id/reject_button"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/pairing_reject"
android:layout_weight="1" />
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<include
android:id="@+id/list_item_category_text"
layout="@android:layout/preference_category" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dip"
android:visibility="gone"
android:textAppearance="?android:attr/textAppearanceSmall"
android:text="@string/device_list_empty"
android:id="@+id/list_item_category_empty_placeholder"
android:layout_gravity="center" />
</LinearLayout>

View File

@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:baselineAligned="false"
android:layout_height="wrap_content"
android:minHeight="?android:attr/listPreferredItemHeight"
android:gravity="center_vertical"
android:paddingRight="?android:attr/scrollbarSize">
<!--
<ImageView
android:id="@+id/list_item_entry_drawable"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:src="@android:drawable/ic_menu_preferences"
android:paddingLeft="9dp"/>
<RelativeLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="10dip"
android:layout_marginRight="6dip"
android:layout_marginTop="3dip"
android:layout_marginBottom="3dip"
android:layout_weight="0">
-->
<TextView android:id="@+id/list_item_entry_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceLarge"
android:ellipsize="marquee"
android:fadingEdge="horizontal" />
<!--
<TextView android:id="@+id/list_item_entry_summary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/list_item_entry_title"
android:layout_alignLeft="@id/list_item_entry_title"
android:textAppearance="?android:attr/textAppearanceSmall"
android:singleLine="true"
android:textColor="?android:attr/textColorSecondary" />
</RelativeLayout>
-->
</LinearLayout>

View File

@@ -0,0 +1,110 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/mpris_control_view"
android:gravity="center">
<Spinner
android:layout_width="200dip"
android:layout_height="wrap_content"
android:id="@+id/player_spinner"
android:layout_gravity="center"
/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:id="@+id/now_playing_textview"
android:singleLine="true"
android:gravity="center"
android:padding="5dip"
android:layout_gravity="center"
/>
<ImageButton
android:layout_width="200dip"
android:layout_height="75dip"
android:id="@+id/play_button"
android:src="@android:drawable/ic_media_play"
android:contentDescription="@string/mpris_play"
android:layout_gravity="center"
/>
<LinearLayout
android:orientation="horizontal"
android:layout_width="200dip"
android:layout_height="70dip"
android:layout_gravity="center"
>
<ImageButton
android:layout_width="match_parent"
android:layout_height="fill_parent"
android:id="@+id/prev_button"
android:src="@android:drawable/ic_media_rew"
android:contentDescription="@string/mpris_previous"
android:layout_weight="0.5"
/>
<ImageButton
android:layout_width="match_parent"
android:layout_height="fill_parent"
android:id="@+id/next_button"
android:src="@android:drawable/ic_media_ff"
android:layout_gravity="center"
android:contentDescription="@string/mpris_next"
android:layout_weight="0.5"
/>
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="200dip"
android:layout_height="70dip"
android:id="@+id/volume_layout"
android:layout_gravity="center">
<ImageView
android:layout_width="50dip"
android:layout_height="50dip"
android:layout_marginRight="10dip"
android:id="@+id/imageView"
android:layout_weight="1"
android:layout_gravity="left|center_vertical"
android:contentDescription="@string/mpris_volume"
android:src="@drawable/volume"
/>
<SeekBar
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:id="@+id/volume_seek"
android:layout_weight="1"
android:max="100"
android:layout_gravity="center"
/>
</LinearLayout>
<!--
<ImageButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/pause_button"
android:src="@android:drawable/ic_media_pause"
android:layout_gravity="center"/>
<ImageButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/pause_button"
android:src="@android:drawable/ic_media_stop"
android:layout_gravity="center"/>
-->
</LinearLayout>

View File

@@ -0,0 +1,21 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:kdeconnect="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/menu_refresh"
android:icon="@drawable/navigation_refresh"
android:orderInCategory="200"
kdeconnect:showAsAction="always"
android:title="@string/reconnect"
/>
<item
android:id="@+id/menu_progress"
android:icon="@drawable/navigation_refresh"
android:orderInCategory="200"
android:visible="false"
kdeconnect:showAsAction="always"
kdeconnect:actionViewClass="android.widget.ProgressBar"
/>
</menu>

View File

@@ -0,0 +1,4 @@
<resources>
<!-- Customize dimensions originally defined in res/values/dimens.xml (such as
screen margins) for sw600dp devices (e.g. 7" tablets) here. -->
</resources>

View File

@@ -0,0 +1,5 @@
<resources>
<!-- Customize dimensions originally defined in res/values/dimens.xml (such as
screen margins) for sw720dp devices (e.g. 10" tablets) in landscape here. -->
<dimen name="activity_horizontal_margin">128dp</dimen>
</resources>

View File

@@ -0,0 +1,11 @@
<resources>
<!--
Base application theme for API 11+. This theme completely replaces
AppBaseTheme from res/values/styles.xml on API 11+ devices.
-->
<style name="AppBaseTheme" parent="android:Theme.Holo">
<!-- API 11 theme customizations can go here. -->
</style>
</resources>

View File

@@ -0,0 +1,5 @@
<resources>
<!-- Default screen margins, per the Android Design guidelines. -->
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen>
</resources>

View File

@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="pref_plugin_telephony">Telephony notifier</string>
<string name="pref_plugin_telephony_desc">Send notifications for SMS and calls</string>
<string name="pref_plugin_battery">Battery report</string>
<string name="pref_plugin_battery_desc">Periodically report battery status</string>
<string name="pref_plugin_clipboard">Clipboard sync</string>
<string name="pref_plugin_clipboard_desc">Share the clipboard content</string>
<string name="pref_plugin_mpris">Multimedia remote controls</string>
<string name="pref_plugin_mpris_desc">Control audio/video from your phone</string>
<string name="pref_plugin_ping">Ping</string>
<string name="pref_plugin_ping_desc">Send and receive pings</string>
<string name="pref_plugin_notifications">Notification sync</string>
<string name="pref_plugin_notifications_desc">Access your notifications from other devices</string>
<string name="plugin_not_available">This feature is not available in your Android version</string>
<string name="device_list_empty">No devices</string>
<string name="ok">OK</string>
<string name="cancel">Cancel</string>
<string name="open_settings">Open settings</string>
<string name="no_permissions">You need to grant permission to access notifications</string>
<string name="send_ping">Send ping</string>
<string name="open_mpris_controls">Open remote control</string>
<string name="category_connected_devices">Connected devices</string>
<string name="category_not_paired_devices">Not paired devices</string>
<string name="category_remembered_devices">Remembered devices</string>
<string name="plugins_failed_to_load">Plugins failed to load (tap for more info):</string>
<string name="device_menu_plugins">Select plugins</string>
<string name="device_menu_unpair">Unpair</string>
<string name="unknown_device">Unknown device</string>
<string name="error_not_reachable">Device not reachable</string>
<string name="error_already_requested">Pairing already requested</string>
<string name="error_already_paired">Device already paired</string>
<string name="error_could_not_send_package">Could not send package</string>
<string name="error_timed_out">Timed out</string>
<string name="error_canceled_by_user">Canceled by user</string>
<string name="error_canceled_by_other_peer">Canceled by other peer</string>
<string name="error_invalid_key">Invalid key received</string>
<string name="pair_requested">Pair requested</string>
<string name="pairing_request_from">Pairing request from %1s</string>
<string name="tap_to_answer">Tap to answer</string>
<string name="reconnect">Reconnect</string>
<string name="device_not_paired">Device not paired</string>
<string name="request_pairing">Request pairing</string>
<string name="pairing_accept">Accept</string>
<string name="pairing_reject">Reject</string>
<string name="device">Device</string>
<string name="pair_device">Pair device</string>
<string name="remote_control">Remote control</string>
<string name="settings">KDE Connect Settings</string>
<string name="mpris_play">Play</string>
<string name="mpris_previous">Previous</string>
<string name="mpris_next">Next</string>
<string name="mpris_volume">Volume</string>
</resources>

View File

@@ -0,0 +1,20 @@
<resources>
<!--
Base application theme, dependent on API level. This theme is replaced
by AppBaseTheme from res/values-vXX/styles.xml on newer devices.
-->
<style name="AppBaseTheme" parent="android:Theme">
<!--
Theme customizations available in newer API levels can go in
res/values-vXX/styles.xml, while customizations related to
backward-compatibility can go here.
-->
</style>
<!-- Application theme. -->
<style name="AppTheme" parent="AppBaseTheme">
<!-- All customizations that are NOT specific to a particular API-level can go here. -->
</style>
</resources>

View File

@@ -1,208 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION,
AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and distribution
as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright
owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities
that control, are controlled by, or are under common control with that entity.
For the purposes of this definition, "control" means (i) the power, direct
or indirect, to cause the direction or management of such entity, whether
by contract or otherwise, or (ii) ownership of fifty percent (50%) or more
of the outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions
granted by this License.
"Source" form shall mean the preferred form for making modifications, including
but not limited to software source code, documentation source, and configuration
files.
"Object" form shall mean any form resulting from mechanical transformation
or translation of a Source form, including but not limited to compiled object
code, generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form,
made available under the License, as indicated by a copyright notice that
is included in or attached to the work (an example is provided in the Appendix
below).
"Derivative Works" shall mean any work, whether in Source or Object form,
that is based on (or derived from) the Work and for which the editorial revisions,
annotations, elaborations, or other modifications represent, as a whole, an
original work of authorship. For the purposes of this License, Derivative
Works shall not include works that remain separable from, or merely link (or
bind by name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version
of the Work and any modifications or additions to that Work or Derivative
Works thereof, that is intentionally submitted to Licensor for inclusion in
the Work by the copyright owner or by an individual or Legal Entity authorized
to submit on behalf of the copyright owner. For the purposes of this definition,
"submitted" means any form of electronic, verbal, or written communication
sent to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems, and
issue tracking systems that are managed by, or on behalf of, the Licensor
for the purpose of discussing and improving the Work, but excluding communication
that is conspicuously marked or otherwise designated in writing by the copyright
owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
of whom a Contribution has been received by Licensor and subsequently incorporated
within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of this
License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive,
no-charge, royalty-free, irrevocable copyright license to reproduce, prepare
Derivative Works of, publicly display, publicly perform, sublicense, and distribute
the Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of this License,
each Contributor hereby grants to You a perpetual, worldwide, non-exclusive,
no-charge, royalty-free, irrevocable (except as stated in this section) patent
license to make, have made, use, offer to sell, sell, import, and otherwise
transfer the Work, where such license applies only to those patent claims
licensable by such Contributor that are necessarily infringed by their Contribution(s)
alone or by combination of their Contribution(s) with the Work to which such
Contribution(s) was submitted. If You institute patent litigation against
any entity (including a cross-claim or counterclaim in a lawsuit) alleging
that the Work or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses granted to You
under this License for that Work shall terminate as of the date such litigation
is filed.
4. Redistribution. You may reproduce and distribute copies of the Work or
Derivative Works thereof in any medium, with or without modifications, and
in Source or Object form, provided that You meet the following conditions:
(a) You must give any other recipients of the Work or Derivative Works a copy
of this License; and
(b) You must cause any modified files to carry prominent notices stating that
You changed the files; and
(c) You must retain, in the Source form of any Derivative Works that You distribute,
all copyright, patent, trademark, and attribution notices from the Source
form of the Work, excluding those notices that do not pertain to any part
of the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its distribution,
then any Derivative Works that You distribute must include a readable copy
of the attribution notices contained within such NOTICE file, excluding those
notices that do not pertain to any part of the Derivative Works, in at least
one of the following places: within a NOTICE text file distributed as part
of the Derivative Works; within the Source form or documentation, if provided
along with the Derivative Works; or, within a display generated by the Derivative
Works, if and wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and do not modify the
License. You may add Your own attribution notices within Derivative Works
that You distribute, alongside or as an addendum to the NOTICE text from the
Work, provided that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and may provide
additional or different license terms and conditions for use, reproduction,
or distribution of Your modifications, or for any such Derivative Works as
a whole, provided Your use, reproduction, and distribution of the Work otherwise
complies with the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise, any
Contribution intentionally submitted for inclusion in the Work by You to the
Licensor shall be under the terms and conditions of this License, without
any additional terms or conditions. Notwithstanding the above, nothing herein
shall supersede or modify the terms of any separate license agreement you
may have executed with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade names,
trademarks, service marks, or product names of the Licensor, except as required
for reasonable and customary use in describing the origin of the Work and
reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or agreed to
in writing, Licensor provides the Work (and each Contributor provides its
Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied, including, without limitation, any warranties
or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR
A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness
of using or redistributing the Work and assume any risks associated with Your
exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory, whether
in tort (including negligence), contract, or otherwise, unless required by
applicable law (such as deliberate and grossly negligent acts) or agreed to
in writing, shall any Contributor be liable to You for damages, including
any direct, indirect, special, incidental, or consequential damages of any
character arising as a result of this License or out of the use or inability
to use the Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all other commercial
damages or losses), even if such Contributor has been advised of the possibility
of such damages.
9. Accepting Warranty or Additional Liability. While redistributing the Work
or Derivative Works thereof, You may choose to offer, and charge a fee for,
acceptance of support, warranty, indemnity, or other liability obligations
and/or rights consistent with this License. However, in accepting such obligations,
You may act only on Your own behalf and on Your sole responsibility, not on
behalf of any other Contributor, and only if You agree to indemnify, defend,
and hold each Contributor harmless for any liability incurred by, or claims
asserted against, such Contributor by reason of your accepting any such warranty
or additional liability. END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following boilerplate
notice, with the fields enclosed by brackets "[]" replaced with your own identifying
information. (Don't include the brackets!) The text should be enclosed in
the appropriate comment syntax for the file format. We also recommend that
a file or class name and description of purpose be included on the same "printed
page" as the copyright notice for easier identification within third-party
archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -1,319 +0,0 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
Everyone is permitted to copy and distribute verbatim copies of this license
document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your freedom to share
and change it. By contrast, the GNU General Public License is intended to
guarantee your freedom to share and change free software--to make sure the
software is free for all its users. This General Public License applies to
most of the Free Software Foundation's software and to any other program whose
authors commit to using it. (Some other Free Software Foundation software
is covered by the GNU Lesser General Public License instead.) You can apply
it to your programs, too.
When we speak of free software, we are referring to freedom, not price. Our
General Public Licenses are designed to make sure that you have the freedom
to distribute copies of free software (and charge for this service if you
wish), that you receive source code or can get it if you want it, that you
can change the software or use pieces of it in new free programs; and that
you know you can do these things.
To protect your rights, we need to make restrictions that forbid anyone to
deny you these rights or to ask you to surrender the rights. These restrictions
translate to certain responsibilities for you if you distribute copies of
the software, or if you modify it.
For example, if you distribute copies of such a program, whether gratis or
for a fee, you must give the recipients all the rights that you have. You
must make sure that they, too, receive or can get the source code. And you
must show them these terms so they know their rights.
We protect your rights with two steps: (1) copyright the software, and (2)
offer you this license which gives you legal permission to copy, distribute
and/or modify the software.
Also, for each author's protection and ours, we want to make certain that
everyone understands that there is no warranty for this free software. If
the software is modified by someone else and passed on, we want its recipients
to know that what they have is not the original, so that any problems introduced
by others will not reflect on the original authors' reputations.
Finally, any free program is threatened constantly by software patents. We
wish to avoid the danger that redistributors of a free program will individually
obtain patent licenses, in effect making the program proprietary. To prevent
this, we have made it clear that any patent must be licensed for everyone's
free use or not licensed at all.
The precise terms and conditions for copying, distribution and modification
follow.
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains a notice
placed by the copyright holder saying it may be distributed under the terms
of this General Public License. The "Program", below, refers to any such program
or work, and a "work based on the Program" means either the Program or any
derivative work under copyright law: that is to say, a work containing the
Program or a portion of it, either verbatim or with modifications and/or translated
into another language. (Hereinafter, translation is included without limitation
in the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not covered
by this License; they are outside its scope. The act of running the Program
is not restricted, and the output from the Program is covered only if its
contents constitute a work based on the Program (independent of having been
made by running the Program). Whether that is true depends on what the Program
does.
1. You may copy and distribute verbatim copies of the Program's source code
as you receive it, in any medium, provided that you conspicuously and appropriately
publish on each copy an appropriate copyright notice and disclaimer of warranty;
keep intact all the notices that refer to this License and to the absence
of any warranty; and give any other recipients of the Program a copy of this
License along with the Program.
You may charge a fee for the physical act of transferring a copy, and you
may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion of it,
thus forming a work based on the Program, and copy and distribute such modifications
or work under the terms of Section 1 above, provided that you also meet all
of these conditions:
a) You must cause the modified files to carry prominent notices stating that
you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in whole or
in part contains or is derived from the Program or any part thereof, to be
licensed as a whole at no charge to all third parties under the terms of this
License.
c) If the modified program normally reads commands interactively when run,
you must cause it, when started running for such interactive use in the most
ordinary way, to print or display an announcement including an appropriate
copyright notice and a notice that there is no warranty (or else, saying that
you provide a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this License.
(Exception: if the Program itself is interactive but does not normally print
such an announcement, your work based on the Program is not required to print
an announcement.)
These requirements apply to the modified work as a whole. If identifiable
sections of that work are not derived from the Program, and can be reasonably
considered independent and separate works in themselves, then this License,
and its terms, do not apply to those sections when you distribute them as
separate works. But when you distribute the same sections as part of a whole
which is a work based on the Program, the distribution of the whole must be
on the terms of this License, whose permissions for other licensees extend
to the entire whole, and thus to each and every part regardless of who wrote
it.
Thus, it is not the intent of this section to claim rights or contest your
rights to work written entirely by you; rather, the intent is to exercise
the right to control the distribution of derivative or collective works based
on the Program.
In addition, mere aggregation of another work not based on the Program with
the Program (or with a work based on the Program) on a volume of a storage
or distribution medium does not bring the other work under the scope of this
License.
3. You may copy and distribute the Program (or a work based on it, under Section
2) in object code or executable form under the terms of Sections 1 and 2 above
provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable source code,
which must be distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three years, to give
any third party, for a charge no more than your cost of physically performing
source distribution, a complete machine-readable copy of the corresponding
source code, to be distributed under the terms of Sections 1 and 2 above on
a medium customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer to distribute
corresponding source code. (This alternative is allowed only for noncommercial
distribution and only if you received the program in object code or executable
form with such an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for making
modifications to it. For an executable work, complete source code means all
the source code for all modules it contains, plus any associated interface
definition files, plus the scripts used to control compilation and installation
of the executable. However, as a special exception, the source code distributed
need not include anything that is normally distributed (in either source or
binary form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component itself
accompanies the executable.
If distribution of executable or object code is made by offering access to
copy from a designated place, then offering equivalent access to copy the
source code from the same place counts as distribution of the source code,
even though third parties are not compelled to copy the source along with
the object code.
4. You may not copy, modify, sublicense, or distribute the Program except
as expressly provided under this License. Any attempt otherwise to copy, modify,
sublicense or distribute the Program is void, and will automatically terminate
your rights under this License. However, parties who have received copies,
or rights, from you under this License will not have their licenses terminated
so long as such parties remain in full compliance.
5. You are not required to accept this License, since you have not signed
it. However, nothing else grants you permission to modify or distribute the
Program or its derivative works. These actions are prohibited by law if you
do not accept this License. Therefore, by modifying or distributing the Program
(or any work based on the Program), you indicate your acceptance of this License
to do so, and all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the Program),
the recipient automatically receives a license from the original licensor
to copy, distribute or modify the Program subject to these terms and conditions.
You may not impose any further restrictions on the recipients' exercise of
the rights granted herein. You are not responsible for enforcing compliance
by third parties to this License.
7. If, as a consequence of a court judgment or allegation of patent infringement
or for any other reason (not limited to patent issues), conditions are imposed
on you (whether by court order, agreement or otherwise) that contradict the
conditions of this License, they do not excuse you from the conditions of
this License. If you cannot distribute so as to satisfy simultaneously your
obligations under this License and any other pertinent obligations, then as
a consequence you may not distribute the Program at all. For example, if a
patent license would not permit royalty-free redistribution of the Program
by all those who receive copies directly or indirectly through you, then the
only way you could satisfy both it and this License would be to refrain entirely
from distribution of the Program.
If any portion of this section is held invalid or unenforceable under any
particular circumstance, the balance of the section is intended to apply and
the section as a whole is intended to apply in other circumstances.
It is not the purpose of this section to induce you to infringe any patents
or other property right claims or to contest validity of any such claims;
this section has the sole purpose of protecting the integrity of the free
software distribution system, which is implemented by public license practices.
Many people have made generous contributions to the wide range of software
distributed through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing to
distribute software through any other system and a licensee cannot impose
that choice.
This section is intended to make thoroughly clear what is believed to be a
consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in certain
countries either by patents or by copyrighted interfaces, the original copyright
holder who places the Program under this License may add an explicit geographical
distribution limitation excluding those countries, so that distribution is
permitted only in or among countries not thus excluded. In such case, this
License incorporates the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions of
the General Public License from time to time. Such new versions will be similar
in spirit to the present version, but may differ in detail to address new
problems or concerns.
Each version is given a distinguishing version number. If the Program specifies
a version number of this License which applies to it and "any later version",
you have the option of following the terms and conditions either of that version
or of any later version published by the Free Software Foundation. If the
Program does not specify a version number of this License, you may choose
any version ever published by the Free Software Foundation.
10. If you wish to incorporate parts of the Program into other free programs
whose distribution conditions are different, write to the author to ask for
permission. For software which is copyrighted by the Free Software Foundation,
write to the Free Software Foundation; we sometimes make exceptions for this.
Our decision will be guided by the two goals of preserving the free status
of all derivatives of our free software and of promoting the sharing and reuse
of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR
THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE
STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM
"AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING,
BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE
OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE
OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA
OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES
OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH
HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest possible
use to the public, the best way to achieve this is to make it free software
which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest to attach
them to the start of each source file to most effectively convey the exclusion
of warranty; and each file should have at least the "copyright" line and a
pointer to where the full notice is found.
<one line to give the program's name and an idea of what it does.>
Copyright (C)< yyyy> <name of author>
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) any later
version.
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, write to the Free Software Foundation, Inc., 51 Franklin
Street, Fifth Floor, Boston, MA 02110-1301, USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this when
it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author Gnomovision comes
with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software,
and you are welcome to redistribute it under certain conditions; type `show
c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may be
called something other than `show w' and `show c'; they could even be mouse-clicks
or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your school,
if any, to sign a "copyright disclaimer" for the program, if necessary. Here
is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision'
(which makes passes at compilers) written by James Hacker.
<signature of Ty Coon >, 1 April 1989 Ty Coon, President of Vice This General
Public License does not permit incorporating your program into proprietary
programs. If your program is a subroutine library, you may consider it more
useful to permit linking proprietary applications with the library. If this
is what you want to do, use the GNU Lesser General Public License instead
of this License.

View File

@@ -1,625 +0,0 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright © 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies of this license
document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for software and
other kinds of works.
The licenses for most software and other practical works are designed to take
away your freedom to share and change the works. By contrast, the GNU General
Public License is intended to guarantee your freedom to share and change all
versions of a program--to make sure it remains free software for all its users.
We, the Free Software Foundation, use the GNU General Public License for most
of our software; it applies also to any other work released this way by its
authors. You can apply it to your programs, too.
When we speak of free software, we are referring to freedom, not price. Our
General Public Licenses are designed to make sure that you have the freedom
to distribute copies of free software (and charge for them if you wish), that
you receive source code or can get it if you want it, that you can change
the software or use pieces of it in new free programs, and that you know you
can do these things.
To protect your rights, we need to prevent others from denying you these rights
or asking you to surrender the rights. Therefore, you have certain responsibilities
if you distribute copies of the software, or if you modify it: responsibilities
to respect the freedom of others.
For example, if you distribute copies of such a program, whether gratis or
for a fee, you must pass on to the recipients the same freedoms that you received.
You must make sure that they, too, receive or can get the source code. And
you must show them these terms so they know their rights.
Developers that use the GNU GPL protect your rights with two steps: (1) assert
copyright on the software, and (2) offer you this License giving you legal
permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains that
there is no warranty for this free software. For both users' and authors'
sake, the GPL requires that modified versions be marked as changed, so that
their problems will not be attributed erroneously to authors of previous versions.
Some devices are designed to deny users access to install or run modified
versions of the software inside them, although the manufacturer can do so.
This is fundamentally incompatible with the aim of protecting users' freedom
to change the software. The systematic pattern of such abuse occurs in the
area of products for individuals to use, which is precisely where it is most
unacceptable. Therefore, we have designed this version of the GPL to prohibit
the practice for those products. If such problems arise substantially in other
domains, we stand ready to extend this provision to those domains in future
versions of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents. States
should not allow patents to restrict development and use of software on general-purpose
computers, but in those that do, we wish to avoid the special danger that
patents applied to a free program could make it effectively proprietary. To
prevent this, the GPL assures that patents cannot be used to render the program
non-free.
The precise terms and conditions for copying, distribution and modification
follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of works,
such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this License.
Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals
or organizations.
To "modify" a work means to copy from or adapt all or part of the work in
a fashion requiring copyright permission, other than the making of an exact
copy. The resulting work is called a "modified version" of the earlier work
or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based on the
Program.
To "propagate" a work means to do anything with it that, without permission,
would make you directly or secondarily liable for infringement under applicable
copyright law, except executing it on a computer or modifying a private copy.
Propagation includes copying, distribution (with or without modification),
making available to the public, and in some countries other activities as
well.
To "convey" a work means any kind of propagation that enables other parties
to make or receive copies. Mere interaction with a user through a computer
network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices" to the
extent that it includes a convenient and prominently visible feature that
(1) displays an appropriate copyright notice, and (2) tells the user that
there is no warranty for the work (except to the extent that warranties are
provided), that licensees may convey the work under this License, and how
to view a copy of this License. If the interface presents a list of user commands
or options, such as a menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work for making
modifications to it. "Object code" means any non-source form of a work.
A "Standard Interface" means an interface that either is an official standard
defined by a recognized standards body, or, in the case of interfaces specified
for a particular programming language, one that is widely used among developers
working in that language.
The "System Libraries" of an executable work include anything, other than
the work as a whole, that (a) is included in the normal form of packaging
a Major Component, but which is not part of that Major Component, and (b)
serves only to enable use of the work with that Major Component, or to implement
a Standard Interface for which an implementation is available to the public
in source code form. A "Major Component", in this context, means a major essential
component (kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to produce
the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all the source
code needed to generate, install, and (for an executable work) run the object
code and to modify the work, including scripts to control those activities.
However, it does not include the work's System Libraries, or general-purpose
tools or generally available free programs which are used unmodified in performing
those activities but which are not part of the work. For example, Corresponding
Source includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically linked
subprograms that the work is specifically designed to require, such as by
intimate data communication or control flow between those subprograms and
other parts of the work.
The Corresponding Source need not include anything that users can regenerate
automatically from other parts of the Corresponding Source.
The Corresponding Source for a work in source code form is that same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of copyright
on the Program, and are irrevocable provided the stated conditions are met.
This License explicitly affirms your unlimited permission to run the unmodified
Program. The output from running a covered work is covered by this License
only if the output, given its content, constitutes a covered work. This License
acknowledges your rights of fair use or other equivalent, as provided by copyright
law.
You may make, run and propagate covered works that you do not convey, without
conditions so long as your license otherwise remains in force. You may convey
covered works to others for the sole purpose of having them make modifications
exclusively for you, or provide you with facilities for running those works,
provided that you comply with the terms of this License in conveying all material
for which you do not control copyright. Those thus making or running the covered
works for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of your copyrighted
material outside their relationship with you.
Conveying under any other circumstances is permitted solely under the conditions
stated below. Sublicensing is not allowed; section 10 makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological measure
under any applicable law fulfilling obligations under article 11 of the WIPO
copyright treaty adopted on 20 December 1996, or similar laws prohibiting
or restricting circumvention of such measures.
When you convey a covered work, you waive any legal power to forbid circumvention
of technological measures to the extent such circumvention is effected by
exercising rights under this License with respect to the covered work, and
you disclaim any intention to limit operation or modification of the work
as a means of enforcing, against the work's users, your or third parties'
legal rights to forbid circumvention of technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you receive
it, in any medium, provided that you conspicuously and appropriately publish
on each copy an appropriate copyright notice; keep intact all notices stating
that this License and any non-permissive terms added in accord with section
7 apply to the code; keep intact all notices of the absence of any warranty;
and give all recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey, and you
may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to produce
it from the Program, in the form of source code under the terms of section
4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified it, and
giving a relevant date.
b) The work must carry prominent notices stating that it is released under
this License and any conditions added under section 7. This requirement modifies
the requirement in section 4 to "keep intact all notices".
c) You must license the entire work, as a whole, under this License to anyone
who comes into possession of a copy. This License will therefore apply, along
with any applicable section 7 additional terms, to the whole of the work,
and all its parts, regardless of how they are packaged. This License gives
no permission to license the work in any other way, but it does not invalidate
such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display Appropriate
Legal Notices; however, if the Program has interactive interfaces that do
not display Appropriate Legal Notices, your work need not make them do so.
A compilation of a covered work with other separate and independent works,
which are not by their nature extensions of the covered work, and which are
not combined with it such as to form a larger program, in or on a volume of
a storage or distribution medium, is called an "aggregate" if the compilation
and its resulting copyright are not used to limit the access or legal rights
of the compilation's users beyond what the individual works permit. Inclusion
of a covered work in an aggregate does not cause this License to apply to
the other parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms of sections
4 and 5, provided that you also convey the machine-readable Corresponding
Source under the terms of this License, in one of these ways:
a) Convey the object code in, or embodied in, a physical product (including
a physical distribution medium), accompanied by the Corresponding Source fixed
on a durable physical medium customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product (including
a physical distribution medium), accompanied by a written offer, valid for
at least three years and valid for as long as you offer spare parts or customer
support for that product model, to give anyone who possesses the object code
either (1) a copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical medium customarily
used for software interchange, for a price no more than your reasonable cost
of physically performing this conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the written
offer to provide the Corresponding Source. This alternative is allowed only
occasionally and noncommercially, and only if you received the object code
with such an offer, in accord with subsection 6b.
d) Convey the object code by offering access from a designated place (gratis
or for a charge), and offer equivalent access to the Corresponding Source
in the same way through the same place at no further charge. You need not
require recipients to copy the Corresponding Source along with the object
code. If the place to copy the object code is a network server, the Corresponding
Source may be on a different server (operated by you or a third party) that
supports equivalent copying facilities, provided you maintain clear directions
next to the object code saying where to find the Corresponding Source. Regardless
of what server hosts the Corresponding Source, you remain obligated to ensure
that it is available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided you inform
other peers where the object code and Corresponding Source of the work are
being offered to the general public at no charge under subsection 6d.
A separable portion of the object code, whose source code is excluded from
the Corresponding Source as a System Library, need not be included in conveying
the object code work.
A "User Product" is either (1) a "consumer product", which means any tangible
personal property which is normally used for personal, family, or household
purposes, or (2) anything designed or sold for incorporation into a dwelling.
In determining whether a product is a consumer product, doubtful cases shall
be resolved in favor of coverage. For a particular product received by a particular
user, "normally used" refers to a typical or common use of that class of product,
regardless of the status of the particular user or of the way in which the
particular user actually uses, or expects or is expected to use, the product.
A product is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent the
only significant mode of use of the product.
"Installation Information" for a User Product means any methods, procedures,
authorization keys, or other information required to install and execute modified
versions of a covered work in that User Product from a modified version of
its Corresponding Source. The information must suffice to ensure that the
continued functioning of the modified object code is in no case prevented
or interfered with solely because modification has been made.
If you convey an object code work under this section in, or with, or specifically
for use in, a User Product, and the conveying occurs as part of a transaction
in which the right of possession and use of the User Product is transferred
to the recipient in perpetuity or for a fixed term (regardless of how the
transaction is characterized), the Corresponding Source conveyed under this
section must be accompanied by the Installation Information. But this requirement
does not apply if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has been installed
in ROM).
The requirement to provide Installation Information does not include a requirement
to continue to provide support service, warranty, or updates for a work that
has been modified or installed by the recipient, or for the User Product in
which it has been modified or installed. Access to a network may be denied
when the modification itself materially and adversely affects the operation
of the network or violates the rules and protocols for communication across
the network.
Corresponding Source conveyed, and Installation Information provided, in accord
with this section must be in a format that is publicly documented (and with
an implementation available to the public in source code form), and must require
no special password or key for unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this License
by making exceptions from one or more of its conditions. Additional permissions
that are applicable to the entire Program shall be treated as though they
were included in this License, to the extent that they are valid under applicable
law. If additional permissions apply only to part of the Program, that part
may be used separately under those permissions, but the entire Program remains
governed by this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option remove any
additional permissions from that copy, or from any part of it. (Additional
permissions may be written to require their own removal in certain cases when
you modify the work.) You may place additional permissions on material, added
by you to a covered work, for which you have or can give appropriate copyright
permission.
Notwithstanding any other provision of this License, for material you add
to a covered work, you may (if authorized by the copyright holders of that
material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the terms of
sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or author
attributions in that material or in the Appropriate Legal Notices displayed
by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or requiring
that modified versions of such material be marked in reasonable ways as different
from the original version; or
d) Limiting the use for publicity purposes of names of licensors or authors
of the material; or
e) Declining to grant rights under trademark law for use of some trade names,
trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that material by
anyone who conveys the material (or modified versions of it) with contractual
assumptions of liability to the recipient, for any liability that these contractual
assumptions directly impose on those licensors and authors.
All other non-permissive additional terms are considered "further restrictions"
within the meaning of section 10. If the Program as you received it, or any
part of it, contains a notice stating that it is governed by this License
along with a term that is a further restriction, you may remove that term.
If a license document contains a further restriction but permits relicensing
or conveying under this License, you may add to a covered work material governed
by the terms of that license document, provided that the further restriction
does not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you must place,
in the relevant source files, a statement of the additional terms that apply
to those files, or a notice indicating where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the form
of a separately written license, or stated as exceptions; the above requirements
apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly provided
under this License. Any attempt otherwise to propagate or modify it is void,
and will automatically terminate your rights under this License (including
any patent licenses granted under the third paragraph of section 11).
However, if you cease all violation of this License, then your license from
a particular copyright holder is reinstated (a) provisionally, unless and
until the copyright holder explicitly and finally terminates your license,
and (b) permanently, if the copyright holder fails to notify you of the violation
by some reasonable means prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is reinstated permanently
if the copyright holder notifies you of the violation by some reasonable means,
this is the first time you have received notice of violation of this License
(for any work) from that copyright holder, and you cure the violation prior
to 30 days after your receipt of the notice.
Termination of your rights under this section does not terminate the licenses
of parties who have received copies or rights from you under this License.
If your rights have been terminated and not permanently reinstated, you do
not qualify to receive new licenses for the same material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or run a copy
of the Program. Ancillary propagation of a covered work occurring solely as
a consequence of using peer-to-peer transmission to receive a copy likewise
does not require acceptance. However, nothing other than this License grants
you permission to propagate or modify any covered work. These actions infringe
copyright if you do not accept this License. Therefore, by modifying or propagating
a covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically receives
a license from the original licensors, to run, modify and propagate that work,
subject to this License. You are not responsible for enforcing compliance
by third parties with this License.
An "entity transaction" is a transaction transferring control of an organization,
or substantially all assets of one, or subdividing an organization, or merging
organizations. If propagation of a covered work results from an entity transaction,
each party to that transaction who receives a copy of the work also receives
whatever licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the Corresponding
Source of the work from the predecessor in interest, if the predecessor has
it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the rights
granted or affirmed under this License. For example, you may not impose a
license fee, royalty, or other charge for exercise of rights granted under
this License, and you may not initiate litigation (including a cross-claim
or counterclaim in a lawsuit) alleging that any patent claim is infringed
by making, using, selling, offering for sale, or importing the Program or
any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this License
of the Program or a work on which the Program is based. The work thus licensed
is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims owned or controlled
by the contributor, whether already acquired or hereafter acquired, that would
be infringed by some manner, permitted by this License, of making, using,
or selling its contributor version, but do not include claims that would be
infringed only as a consequence of further modification of the contributor
version. For purposes of this definition, "control" includes the right to
grant patent sublicenses in a manner consistent with the requirements of this
License.
Each contributor grants you a non-exclusive, worldwide, royalty-free patent
license under the contributor's essential patent claims, to make, use, sell,
offer for sale, import and otherwise run, modify and propagate the contents
of its contributor version.
In the following three paragraphs, a "patent license" is any express agreement
or commitment, however denominated, not to enforce a patent (such as an express
permission to practice a patent or covenant not to sue for patent infringement).
To "grant" such a patent license to a party means to make such an agreement
or commitment not to enforce a patent against the party.
If you convey a covered work, knowingly relying on a patent license, and the
Corresponding Source of the work is not available for anyone to copy, free
of charge and under the terms of this License, through a publicly available
network server or other readily accessible means, then you must either (1)
cause the Corresponding Source to be so available, or (2) arrange to deprive
yourself of the benefit of the patent license for this particular work, or
(3) arrange, in a manner consistent with the requirements of this License,
to extend the patent license to downstream recipients. "Knowingly relying"
means you have actual knowledge that, but for the patent license, your conveying
the covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that country
that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or arrangement,
you convey, or propagate by procuring conveyance of, a covered work, and grant
a patent license to some of the parties receiving the covered work authorizing
them to use, propagate, modify or convey a specific copy of the covered work,
then the patent license you grant is automatically extended to all recipients
of the covered work and works based on it.
A patent license is "discriminatory" if it does not include within the scope
of its coverage, prohibits the exercise of, or is conditioned on the non-exercise
of one or more of the rights that are specifically granted under this License.
You may not convey a covered work if you are a party to an arrangement with
a third party that is in the business of distributing software, under which
you make payment to the third party based on the extent of your activity of
conveying the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory patent
license (a) in connection with copies of the covered work conveyed by you
(or copies made from those copies), or (b) primarily for and in connection
with specific products or compilations that contain the covered work, unless
you entered into that arrangement, or that patent license was granted, prior
to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting any implied
license or other defenses to infringement that may otherwise be available
to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or otherwise)
that contradict the conditions of this License, they do not excuse you from
the conditions of this License. If you cannot convey a covered work so as
to satisfy simultaneously your obligations under this License and any other
pertinent obligations, then as a consequence you may not convey it at all.
For example, if you agree to terms that obligate you to collect a royalty
for further conveying from those to whom you convey the Program, the only
way you could satisfy both those terms and this License would be to refrain
entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have permission to
link or combine any covered work with a work licensed under version 3 of the
GNU Affero General Public License into a single combined work, and to convey
the resulting work. The terms of this License will continue to apply to the
part which is the covered work, but the special requirements of the GNU Affero
General Public License, section 13, concerning interaction through a network
will apply to the combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of the
GNU General Public License from time to time. Such new versions will be similar
in spirit to the present version, but may differ in detail to address new
problems or concerns.
Each version is given a distinguishing version number. If the Program specifies
that a certain numbered version of the GNU General Public License "or any
later version" applies to it, you have the option of following the terms and
conditions either of that numbered version or of any later version published
by the Free Software Foundation. If the Program does not specify a version
number of the GNU General Public License, you may choose any version ever
published by the Free Software Foundation.
If the Program specifies that a proxy can decide which future versions of
the GNU General Public License can be used, that proxy's public statement
of acceptance of a version permanently authorizes you to choose that version
for the Program.
Later license versions may give you additional or different permissions. However,
no additional obligations are imposed on any author or copyright holder as
a result of your choosing to follow a later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE
LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM
PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR
CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL
ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM
AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL,
INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO
USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED
INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE
PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER
PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided above cannot
be given local legal effect according to their terms, reviewing courts shall
apply local law that most closely approximates an absolute waiver of all civil
liability in connection with the Program, unless a warranty or assumption
of liability accompanies a copy of the Program in return for a fee. END OF
TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest possible
use to the public, the best way to achieve this is to make it free software
which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest to attach
them to the start of each source file to most effectively state the exclusion
of warranty; and each file should have at least the "copyright" line and a
pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
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 3 of the License, or (at your option) any later
version.
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 <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short notice like
this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it under certain
conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands might
be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary. For
more information on this, and how to apply and follow the GNU GPL, see <https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General Public
License instead of this License. But first, please read <https://www.gnu.org/
licenses /why-not-lgpl.html>.

View File

@@ -1,12 +0,0 @@
This library 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 3 of
the license or (at your option) at any later version that is
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 as 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.

View File

@@ -1,37 +0,0 @@
# KDE Connect - Android app
KDE Connect is a multi-platform app that allows your devices to communicate (eg: your phone and your computer).
## (Some) Features
- **Shared clipboard**: copy and paste between your phone and your computer (or any other device).
- **Notification sync**: Read and reply to your Android notifications from the desktop.
- **Share files and URLs** instantly from one device to another.
- **Multimedia remote control**: Use your phone as a remote for Linux media players.
- **Virtual touchpad**: Use your phone screen as your computer's touchpad and keyboard.
All this without wires, over the already existing Wi-Fi network, and using TLS encryption.
## About this app
This is a native Android port of the KDE Connect Qt app. You will find a more complete readme about KDE Connect [here](https://invent.kde.org/network/kdeconnect-kde/).
## How to install this app
You can install this app from the [Play Store](https://play.google.com/store/apps/details?id=org.kde.kdeconnect_tp) as well as [F-Droid](https://f-droid.org/repository/browse/?fdid=org.kde.kdeconnect_tp). Note you will also need to install the [desktop app](https://invent.kde.org/network/kdeconnect-kde) for it to work.
## Contributing
A lot of useful information, including how to get started working on KDE Connect and how to connect with the current developers, is on our [KDE Community Wiki page](https://community.kde.org/KDEConnect)
For bug reporting, please use [KDE's Bugzilla](https://bugs.kde.org). Please do not use the issue tracker in GitLab since we want to keep everything in one place.
To contribute patches, use [KDE Connect's Gitlab](https://invent.kde.org/network/kdeconnect-android/).
On Gitlab (as well as on our [old Phabricator](https://phabricator.kde.org/tag/kde_connect/)) you can find a task list with stuff to do and links to other relevant resources.
It is a good idea to also subscribe to the [KDE Connect mailing list](https://mail.kde.org/mailman/listinfo/kdeconnect).
Please know that all translations for all KDE apps are handled by the [localization team](https://l10n.kde.org/). If you would like to submit a translation, that should be done by working with the proper team for that language.
## License
[GNU GPL v2](https://www.gnu.org/licenses/gpl-2.0.html) and [GNU GPL v3](https://www.gnu.org/licenses/gpl-3.0.html)
If you are reading this from GitHub, you should know that this is just a mirror of the [KDE Project repo](https://invent.kde.org/network/kdeconnect-android/).

View File

@@ -1,51 +0,0 @@
#!/usr/bin/env bash
# The name of catalog we create (without the.pot extension), sourced from the scripty scripts
FILENAME="kdeconnect-android"
function export_pot_file # First parameter will be the path of the pot file we have to create, includes $FILENAME
{
potfile=$1
mkdir outdir
ANSI_COLORS_DISABLED=1 a2po export --android res/ --gettext outdir
mv outdir/template.pot $potfile
rm -rf outdir
}
function import_po_files # First parameter will be a path that will contain several .po files with the format LANG.po
{
podir=$1
# Android doesn't support languages with an @
find "$podir" -type f -name "*@*.po" -delete
# drop obsolete messages, as Babel cannot parse them -- see:
# https://github.com/python-babel/babel/issues/206
# https://github.com/python-babel/babel/issues/566
find "$podir" -name '*.po' -exec msgattrib --no-obsolete -o {} {} \;
ANSI_COLORS_DISABLED=1 a2po import --ignore-fuzzy --android res/ --gettext $podir
# Generate the locales_config.xml
pushd res
echo '<?xml version="1.0" encoding="utf-8"?>' > xml/locales_config.xml
echo '<locale-config xmlns:android="http://schemas.android.com/apk/res/android">' >> xml/locales_config.xml
transform_locale_regex='(\w+)-r(\w+)'
# Add en-US as the first locale so that is the fallback, and also because it won't be handled in the following loop
echo -e '\t<locale android:name="en-US"/>' >> xml/locales_config.xml
for i in values-*; do
if [ -d "${i}" ]; then
if [ -e "${i}/strings.xml" ]; then
locale="${i:7}"
if [[ "${locale}" =~ $transform_locale_regex ]]; then
# Special case to turn locales like "en-rUS", "en-rGB" into "en-US" and "en-GB"
transformed_locale="${BASH_REMATCH[1]}-${BASH_REMATCH[2]}"
echo -e "\t<locale android:name=\"${transformed_locale}\"/>" >> xml/locales_config.xml
else
echo -e "\t<locale android:name=\"${locale}\"/>" >> xml/locales_config.xml
fi
fi
fi
done
echo "</locale-config>" >> xml/locales_config.xml
popd
}

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 26 KiB

2
build.gradle Normal file
View File

@@ -0,0 +1,2 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.

View File

@@ -1,359 +0,0 @@
import com.android.build.api.instrumentation.AsmClassVisitorFactory
import com.android.build.api.instrumentation.ClassContext
import com.android.build.api.instrumentation.ClassData
import com.android.build.api.instrumentation.InstrumentationParameters
import com.android.build.api.instrumentation.InstrumentationScope
import com.github.jk1.license.LicenseReportExtension
import com.github.jk1.license.render.ReportRenderer
import com.github.jk1.license.render.TextReportRenderer
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.MethodVisitor
import org.objectweb.asm.Opcodes.CHECKCAST
import org.objectweb.asm.Opcodes.INVOKESTATIC
buildscript {
dependencies {
classpath(libs.android.gradlePlugin)
classpath(libs.kotlin.gradlePlugin)
}
}
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.ksp)
alias(libs.plugins.dependencyLicenseReport)
alias(libs.plugins.compose.compiler)
}
val licenseResDir = File("$projectDir/build/dependency-license-res")
val hashProvider = project.providers.exec {
workingDir = rootDir
commandLine("git", "rev-parse", "--short", "HEAD")
}.standardOutput.asText.map { it.trim() }
android {
namespace = "org.kde.kdeconnect_tp"
compileSdk = 35
defaultConfig {
applicationId = "org.kde.kdeconnect_tp"
minSdk = 21
targetSdk = 35
versionCode = 13304
versionName = "1.33.4"
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
}
buildFeatures {
viewBinding = true
compose = true
buildConfig = true
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_9
targetCompatibility = JavaVersion.VERSION_1_9
// Flag to enable support for the new language APIs
isCoreLibraryDesugaringEnabled = true
}
kotlinOptions {
jvmTarget = "9"
}
androidResources {
generateLocaleConfig = true
}
sourceSets {
getByName("main") {
setRoot(".") // By default AGP expects all directories under src/main/...
java.srcDir("src") // by default is "java"
res.setSrcDirs(listOf(licenseResDir, "res")) // add licenseResDir
}
getByName("debug") {
res.srcDir("dbg-res")
}
getByName("test") {
java.srcDir("tests")
}
}
packaging {
resources {
merges += listOf("META-INF/DEPENDENCIES", "META-INF/LICENSE", "META-INF/NOTICE")
}
}
signingConfigs {
getByName("debug") {
storeFile = file("debug.keystore")
storePassword = "android"
keyAlias = "androiddebugkey"
keyPassword = "android"
}
}
buildTypes {
getByName("debug") {
isMinifyEnabled = false
isShrinkResources = false
signingConfig = signingConfigs.getByName("debug")
applicationIdSuffix = ".debug"
versionNameSuffix = "-debug"
}
getByName("release") {
isMinifyEnabled = true
isShrinkResources = true
}
}
lint {
abortOnError = false
checkReleaseBuilds = false
}
applicationVariants.all {
val variant = this
logger.quiet("Found a variant called ${variant.name}")
if (variant.buildType.isDebuggable) {
variant.outputs.all {
val output = this as com.android.build.gradle.internal.api.BaseVariantOutputImpl
if (output.outputFile.name.endsWith(".apk")) {
// Default output filename is "${project.name}-${v.name}.apk". We want
// the Git commit short-hash to be added onto that default filename.
try {
val newName = "${project.name}-${variant.name}-${hashProvider.get()}.apk"
logger.quiet(" Found an output file ${output.outputFile.name}, renaming to $newName")
output.outputFileName = newName
} catch (ignored: Exception) {
logger.warn("Could not make use of the 'git' command-line tool. Output filenames will not be customized.")
}
}
}
}
}
}
/**
* Fix PosixFilePermission class type check issue.
*
* It fixed the class cast exception when lib desugar enabled and minSdk < 26.
*/
abstract class FixPosixFilePermissionClassVisitorFactory :
AsmClassVisitorFactory<FixPosixFilePermissionClassVisitorFactory.Params> {
override fun createClassVisitor(
classContext: ClassContext,
nextClassVisitor: ClassVisitor
): ClassVisitor {
return object : ClassVisitor(instrumentationContext.apiVersion.get(), nextClassVisitor) {
override fun visitMethod(
access: Int,
name: String?,
descriptor: String?,
signature: String?,
exceptions: Array<out String>?
): MethodVisitor {
if (name == "attributesToPermissions") { // org.apache.sshd.sftp.common.SftpHelper.attributesToPermissions
return object : MethodVisitor(
instrumentationContext.apiVersion.get(),
super.visitMethod(access, name, descriptor, signature, exceptions)
) {
override fun visitTypeInsn(opcode: Int, type: String?) {
// We need to prevent Android Desugar modifying the `PosixFilePermission` classname.
//
// Android Desugar will replace `CHECKCAST java/nio/file/attribute/PosixFilePermission`
// to `CHECKCAST j$/nio/file/attribute/PosixFilePermission`.
// We need to replace it with `CHECKCAST java/lang/Enum` to prevent Android Desugar from modifying it.
if (opcode == CHECKCAST && type == "java/nio/file/attribute/PosixFilePermission") {
println("Bypass PosixFilePermission type check success.")
// `Enum` is the superclass of `PosixFilePermission`.
// Due to `Object` is not the superclass of `Enum`, we need to use `Enum` instead of `Object`.
super.visitTypeInsn(opcode, "java/lang/Enum")
} else {
super.visitTypeInsn(opcode, type)
}
}
}
}
return super.visitMethod(access, name, descriptor, signature, exceptions)
}
}
}
override fun isInstrumentable(classData: ClassData): Boolean {
return (classData.className == "org.apache.sshd.sftp.common.SftpHelper").also {
if (it) println("SftpHelper Found! Instrumenting...")
}
}
interface Params : InstrumentationParameters
}
/**
* Collections.unmodifiableXXX is not exist when Android API level is lower than 26.
* So we replace the call to Collections.unmodifiableXXX with the original collection by removing the call.
*/
abstract class FixCollectionsClassVisitorFactory :
AsmClassVisitorFactory<FixCollectionsClassVisitorFactory.Params> {
override fun createClassVisitor(
classContext: ClassContext,
nextClassVisitor: ClassVisitor
): ClassVisitor {
return object : ClassVisitor(instrumentationContext.apiVersion.get(), nextClassVisitor) {
override fun visitMethod(
access: Int,
name: String?,
descriptor: String?,
signature: String?,
exceptions: Array<out String>?
): MethodVisitor {
return object : MethodVisitor(
instrumentationContext.apiVersion.get(),
super.visitMethod(access, name, descriptor, signature, exceptions)
) {
override fun visitMethodInsn(
opcode: Int,
type: String?,
name: String?,
descriptor: String?,
isInterface: Boolean
) {
val backportClass = "org/kde/kdeconnect/Helpers/CollectionsBackport"
if (opcode == INVOKESTATIC && type == "java/util/Collections") {
val replaceRules = mapOf(
"unmodifiableNavigableSet" to "(Ljava/util/NavigableSet;)Ljava/util/NavigableSet;",
"unmodifiableSet" to "(Ljava/util/Set;)Ljava/util/Set;",
"unmodifiableNavigableMap" to "(Ljava/util/NavigableMap;)Ljava/util/NavigableMap;",
"emptyNavigableMap" to "()Ljava/util/NavigableMap;")
if (name in replaceRules && descriptor == replaceRules[name]) {
super.visitMethodInsn(opcode, backportClass, name, descriptor, isInterface)
val calleeClass = classContext.currentClassData.className
println("Replace Collections.$name call with CollectionsBackport.$name from $calleeClass success.")
return
}
}
super.visitMethodInsn(opcode, type, name, descriptor, isInterface)
}
}
}
}
}
override fun isInstrumentable(classData: ClassData): Boolean {
return classData.className.startsWith("org.apache.sshd") // We only need to fix the Apache SSHD library
}
interface Params : InstrumentationParameters
}
ksp {
arg("com.albertvaka.classindexksp.annotations", "org.kde.kdeconnect.Plugins.PluginFactory.LoadablePlugin")
}
androidComponents {
onVariants { variant ->
variant.instrumentation.transformClassesWith(
FixPosixFilePermissionClassVisitorFactory::class.java,
InstrumentationScope.ALL
) { }
variant.instrumentation.transformClassesWith(
FixCollectionsClassVisitorFactory::class.java,
InstrumentationScope.ALL
) { }
}
}
dependencies {
// It has a bug that causes a crash when using PosixFilePermission and minSdk < 26.
// It has been used in SSHD Core.
// We have taken a workaround to fix it.
// See `FixPosixFilePermissionClassVisitorFactory` for more details.
coreLibraryDesugaring(libs.android.desugarJdkLibsNio)
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.constraintlayout.compose)
implementation(libs.androidx.compose.ui.tooling.preview)
debugImplementation(libs.androidx.compose.ui.tooling)
implementation(libs.androidx.media)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.preference.ktx)
implementation(libs.androidx.recyclerview)
implementation(libs.androidx.swiperefreshlayout)
implementation(libs.androidx.documentfile)
implementation(libs.androidx.lifecycle.viewmodel.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.lifecycle.extensions)
implementation(libs.androidx.lifecycle.common.java8)
implementation(libs.androidx.gridlayout)
implementation(libs.material)
implementation(libs.disklrucache) //For caching album art bitmaps
implementation(libs.slf4j.handroid)
implementation(libs.apache.sshd.core)
implementation(libs.apache.sshd.sftp)
implementation(libs.apache.sshd.scp)
implementation(libs.apache.sshd.mina)
implementation(libs.apache.mina.core)
implementation(libs.bcpkix.jdk15on) //For SSL certificate generation
ksp(libs.classindexksp)
// The android-smsmms library is the only way I know to handle MMS in Android
// (Shouldn't a phone OS make phone things easy?)
// This library was originally authored as com.klinkerapps at https://github.com/klinker41/android-smsmms.
// However, that version is under-loved. I have therefore made "some fixes" and published it.
// Please see https://invent.kde.org/sredman/android-smsmms/-/tree/master
implementation(libs.android.smsmms)
implementation(libs.logger)
implementation(libs.commons.io)
implementation(libs.commons.collections4)
implementation(libs.commons.lang3)
implementation(libs.univocity.parsers)
// Kotlin
implementation(libs.kotlin.stdlib.jdk8)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.coroutines.android)
// Testing
testImplementation(libs.junit)
testImplementation(libs.mockito.core)
testImplementation(libs.jsonassert)
// For device controls
implementation(libs.reactive.streams)
implementation(libs.rxjava)
}
licenseReport {
configurations = LicenseReportExtension.ALL
renderers = arrayOf<ReportRenderer>(TextReportRenderer())
}
tasks.named("generateLicenseReport") {
doLast {
val target = File(licenseResDir, "raw/license")
target.parentFile.mkdirs()
target.writeText(
files(
layout.projectDirectory.file("COPYING"),
layout.buildDirectory.file("reports/dependency-license/THIRD-PARTY-NOTICES.txt")
).joinToString(separator = "\n") {
it.readText()
}
)
}
}
tasks.named("preBuild") {
dependsOn("generateLicenseReport")
}

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<!-- <background android:drawable="@drawable/ic_launcher_background"/>-->
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_monochrome"/>
</adaptive-icon>

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<!-- <background android:drawable="@drawable/ic_launcher_banner_background"/>-->
<foreground android:drawable="@drawable/ic_launcher_banner_foreground"/>
</adaptive-icon>

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<!-- <background android:drawable="@drawable/ic_launcher_background"/>-->
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_monochrome"/>
</adaptive-icon>

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="kde_connect">Debug KDE Connect</string>
</resources>

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="kde_connect">Debug KDE Connect</string>
</resources>

Binary file not shown.

View File

@@ -1,2 +0,0 @@
json_key_file("/please/pass/the/key/using/--json-key/instead")
package_name("org.kde.kdeconnect_tp")

View File

@@ -1,7 +0,0 @@
To upload translations to the Play Store, run from the root of the repo:
```
fastlane supply --skip_upload_screenshots --skip_upload_images --skip_upload_changelogs --json-key <path to the json key file>
```
F-Droid reads them directly from this directory for each release tag, so no action is needed.

View File

@@ -1,40 +0,0 @@
EXPORTS_POT_DIR=1
FILE_PREFIX=kdeconnect-android-store
function export_pot_dir # First parameter will be the path of the directory where we have to store the pot files
{
potdir=$1
txt2po --no-segmentation --progress=names -P -i ./metadata/android/en-US/short_description.txt -o $potdir/kdeconnect-android-store-short.pot
txt2po --no-segmentation --progress=names -P -i ./metadata/android/en-US/full_description.txt -o $potdir/kdeconnect-android-store-full.pot
}
function import_po_dirs # First parameter will be a path that will be a directory to the dirs for each lang and then all the .po files inside
{
podir=$1
# Some languages don't exist in Google Play or have different codes
declare -a to_delete=( "bs" "ca@valencia" "sr@ijekavian" "sr@ijekavianlatin" "sr@latin" "ia" "eo" "tg" )
for lang in "${to_delete[@]}"; do
if [ -d $podir/$lang ]; then
rm $podir/$lang/*
rmdir $podir/$lang
fi
done
declare -A to_rename=( ["az"]="az-AZ" ["cs"]="cs-CZ" ["da"]="da-DK" ["de"]="de-DE" ["el"]="el-GR" ["es"]="es-ES" ["eu"]="eu-ES"
["fi"]="fi-FI" ["fr"]="fr-FR" ["gl"]="gl-ES" ["he"]="iw-IL" ["hu"]="hu-HU" ["is"]="is-IS" ["it"]="it-IT"
["ja"]="ja-JP" ["ka"]="ka-GE" ["ko"]="ko-KR" ["nl"]="nl-NL" ["nn"]="no-NO" ["pl"]="pl-PL" ["pt"]="pt-PT"
["ru"]="ru-RU" ["sv"]="sv-SE" ["ta"]="ta-IN" ["tr"]="tr-TR")
for lang in "${!to_rename[@]}"; do
if [ -d $podir/$lang ]; then
mv $podir/$lang $podir/${to_rename[$lang]}
fi
done
for lang in $(ls $podir); do
if [ -f $podir/$lang/kdeconnect-android-store-short.po -a -f $podir/$lang/kdeconnect-android-store-full.po ]; then
mkdir -p ./metadata/android/$lang/
cp ./metadata/android/en-US/title.txt ./metadata/android/$lang/title.txt # we do not translate the app name
po2txt --fuzzy --progress=names -i $podir/$lang/kdeconnect-android-store-short.po -o ./metadata/android/$lang/short_description.txt
po2txt --fuzzy --progress=names -i $podir/$lang/kdeconnect-android-store-full.po -o ./metadata/android/$lang/full_description.txt
fi
done
}

View File

@@ -1,21 +0,0 @@
يوفر كِيدِي المتّصل مجموعة من الميزات لدمج سير عملك عبر الأجهزة:
- نقل الملفات بين أجهزتك.
- الوصول إلى الملفات الموجودة على هاتفك من جهاز الكمبيوتر الخاص بك، دون أسلاك.
- الحافظة المشتركة: النسخ واللصق بين أجهزتك.
- الحصول على إشعارات للمكالمات والرسائل الواردة على جهاز الكمبيوتر الخاص بك.
- لوحة اللمس الافتراضية: استخدم شاشة هاتفك كلوحة لمس لجهاز الكمبيوتر الخاص بك.
- مزامنة الإشعارات: الوصول إلى إشعارات هاتفك من جهاز الكمبيوتر الخاص بك والرد على الرسائل.
- التحكم عن بعد في الوسائط المتعددة: استخدم هاتفك كجهاز تحكم عن بعد لمشغلات الوسائط لينكس.
- اتصال WiFi: لا حاجة إلى سلك USB أو بلوتوث.
- تشفير TLS من البداية إلى النهاية: معلوماتك آمنة.
يرجى ملاحظة أنك ستحتاج إلى تثبيت كِيدِي المتّصل على حاسوبك حتى يعمل هذا التطبيق، والحفاظ على تحديث إصدار سطح المكتب بإصدار أندوريد حتى تعمل أحدث الميزات.
معلومات الأذونات الحساسة:
* إذن إمكانية الوصول: مطلوب لتلقي إدخال من جهاز آخر للتحكم في هاتف أندرويد خاص بك، إذا كنت تستخدم ميزة الإدخال عن بُعد.
* إذن تحديد الموقع في الخلفية: مطلوب لمعرفة شبكة واي فاي التي تتصل بها، إذا كنت تستخدم ميزة الشبكات الموثوقة.
لا يرسل كِيدِي المتّصل أي معلومات إلى كيدي أو إلى أي طرف ثالث. يرسل كِيدِي المتّصل البيانات من جهاز إلى آخر مباشرةً باستخدام الشبكة المحلية، وليس عبر الإنترنت، وباستخدام التشفير من البداية إلى النهاية.
هذا التطبيق جزء من مشروع مفتوح المصدر وهو موجود بفضل جميع الأشخاص الذين ساهموا فيه. قم بزيارة الموقع الإلكتروني للحصول على الكود المصدر.

View File

@@ -1 +0,0 @@
يقوم كِيدِي المتّصل بدمج هاتفك الذكي والحاسوب

View File

@@ -1 +0,0 @@
KDE Connect

View File

@@ -1,22 +0,0 @@
KDE connect cihazlarınız arasında inteqrasiya üçün funksiyalar dəstini təqdim edir:
- Faylların cihazlar arasında köçürülməsi.
- Heç bir kabel qoşulmadan telefonunuzdakı fayllara compyuterinizdə baxmaq.
- Mübadilə yaddaşını paylaşmaq: cihazlarınız arasında kopyalama və yerləşdirmə.
- Daxil olan zəng və ismarıc bildirişlərini komputerdə göstərmək
- Virtual toxunma paneli: Telefonunuzun ektranını kompyuterin toxunma paneli kimi istifdə edin.
- Bildirişlərin eyniləşdirilməsi: Telefon bildirişlərini kompyuterinizdə göstərmək və ismarıclara cavab vermək.
- Multimedianın məsafədən idarə edilməsi: Linux media oxuducusunu telefonunuzdan idarə edin.
- WiFi bağlantısı: USB qoşulması və ya Bluetooth qoşulmasına ehtiyyac yoxdur.
- Ucdan-uca TLC şifrələmə: məlumatlarınızın təhlükəsizliyi qorunur.
Nəzərə alın ki, bu tətbiqin işləməsi üçün kompyuterinizə KDE Connect-i quraşdırmalısınız və sonuncu funksiyaların işləməsi üçün masaüstü və Android versiyalarını sonuncu versiyaya eyni şəkildə yeniləməlisiniz.
Həssas icazələr haqqında məlumat:
*Giriş imkanı icazələri: Əgər uzaqdan daxiletmə funksiyasından istifadə edirsinizsə Android cihazınızı idarə etmək üçün başqa cihazlardan daxil olanları qəbul etmək tələb olunur.
*Məkanın arxa fonda istifadəsinə icazə: Əgər etibarlı şəbəkə funksiyasından
istifadə edirsinizsə hansı WiFi şəbəkəsinə qoşulduğunuzu bilməniz tələb olunur
KDE Connect heç bir halda məlumatları KDE-yə və ya üçüncü tərəfə ötürmür. KDE Connect məlumatları bir cihazdan başqasına internet istifadə etmədən və ucdan-uca şifrələmədən istifadə edərək yerli şəbəkə üzərindən göndərir.
Bu tətbiq açıq qaynaq layihəsinin bir hissəsidir və ona töhvə verənlərin sayəsində mövcuddur. Mənbə kodunu əldə etmək üçün veb-səhifəyə daxil olun.

View File

@@ -1 +0,0 @@
KDE Connect kompyuteriniz ilə smartfonunuzu inteqrasiya edir

View File

@@ -1 +0,0 @@
KDE Connect

View File

@@ -1,21 +0,0 @@
KDE Connect предоставя набор от функции за интегриране на вашия работен процес на различни устройства:
- Прехвърляйте файлове между вашите устройства.
- Осъществявайте достъп до файлове на телефона си от компютъра си, без кабели.
- Споделен клипборд: копирайте и поставяйте между вашите устройства.
- Получавайте известия за входящи обаждания и съобщения на вашия компютър.
- Виртуален тъчпад: Използвайте екрана на телефона си като тъчпад на компютъра.
- Синхронизиране на известия: Достъп до известията на телефона ви от вашия компютър и отговаряне на съобщения.
- Мултимедийно дистанционно управление: Използвайте телефона си като дистанционно за Linux медийни плейъри.
- WiFi връзка: не е необходим USB кабел или bluetooth.
- TLS криптиране от край до край: информацията ви е в безопасност.
Моля, имайте предвид, че ще трябва да инсталирате KDE Connect на вашия компютър, за да работи това приложение, и поддържайте версията за настолен компютър актуална с версията за Android, за да работят най-новите функции.
Поверителна информация за разрешения:
* Разрешение за достъпност: Изисква се за получаване на вход от друго устройство за управление на вашия телефон с Android, ако използвате функцията за отдалечено въвеждане.
* Разрешение за местоположение във фонов режим: Изисква се, за да знаете към коя WiFi мрежа сте свързани, ако използвате функцията Trusted Networks.
KDE Connect никога не изпраща никаква информация на KDE или на трета страна. KDE Connect изпраща данни от едно устройство на друго директно чрез локалната мрежа, никога през интернет, и чрез криптиране от край до край.
Това приложение е част от проект с отворен код и съществува благодарение на всички хора, които са допринесли за него. Посетете уебсайта, за да вземете изходния код.

View File

@@ -1 +0,0 @@
KDE Connect интегрира вашия смартфон и компютър

View File

@@ -1 +0,0 @@
KDE Connect

View File

@@ -1,21 +0,0 @@
El KDE Connect proporciona un conjunt de característiques per a integrar el flux de treball entre dispositius:
- Transferiu fitxers entre els vostres dispositius.
- Accediu als fitxers del telèfon mòbil des de l'ordinador, sense cables.
- Porta-retalls compartit: copieu i enganxeu entre els dispositius.
- Obteniu les notificacions de trucades entrants i missatges a l'ordinador.
- Ratolí tàctil virtual: utilitzeu la pantalla del telèfon com a ratolí tàctil de l'ordinador.
- Sincronització de notificacions: accediu a les notificacions del telèfon des de l'ordinador i contesteu els missatges.
- Control remot multimèdia: utilitzeu el telèfon com a control remot dels reproductors multimèdia Linux.
- Connexió Wi-Fi: no cal connexió USB ni Bluetooth.
- Encriptatge TLS d'extrem a extrem: la informació és segura.
Tingueu en compte que haureu d'instal·lar el KDE Connect a l'ordinador perquè aquesta aplicació funcioni, i mantingueu actualitzada la versió d'escriptori amb la versió de l'Android perquè funcionin les últimes característiques.
Informació dels permisos confidencials:
* Permís d'accessibilitat: requerit per a rebre l'entrada des d'un altre dispositiu per a controlar el telèfon Android, si utilitzeu la característica d'entrada remota.
* Permís d'ubicació en segon pla: requerit per a saber a quina xarxa Wi-Fi esteu connectat, si utilitzeu la característica xarxes de confiança.
El KDE Connect mai envia cap informació a KDE ni a cap tercer. El KDE Connect envia dades des d'un dispositiu a l'altre directament utilitzant la xarxa local, mai a través d'Internet, i utilitzant l'encriptatge d'extrem a extrem.
Aquesta aplicació forma part d'un projecte de codi obert i existeix gràcies a totes les persones que hi han contribuït. Visiteu el lloc web per a aconseguir el codi font.

View File

@@ -1 +0,0 @@
El KDE Connect integra el vostre telèfon intel·ligent i l'ordinador

View File

@@ -1 +0,0 @@
KDE Connect

View File

@@ -1,21 +0,0 @@
KDE Connect poskytuje sadu vlastností pro vzájemnou integraci vašich zařízení:
- Přenášejte soubory mezi zařízeními.
- Přistupujte bezdrátově k souborům na vašem počítači
- Sdílená schránka: kopírujte a vkládejte z jednoho zařízení na druhé.
- Přijímejte upozornění na příchozí hovory nebo SMS na vašem PC.
- Virtuální touchpad: Používejte obrazovku svého telefonu jako touchpad počítače.
- Synchronizace upozornění: Čtěte upozornění z vašeho telefonu na počítači.
- Vzdálené ovládání multimédií: Používejte svůj telefon jako ovladač přehrávače na Linuxu.
- Připojení WiFi: Není potřeba kabel USB ani Bluetooth.
- Koncové šifrování TLS: Vaše informace jsou v bezpečí.
Prosím pamatujte, že pro správnou funkci této aplikace je nutné abyste na vašem počítači měli nainstalován KDE Connect a udržovali jej aktuální na počítači i na Androidu.
Informace o citlivých oprávněních:
* Oprávnění přístupnosti: Nutné pro příjem vstupu z jiného zařízení aby mohlo ovládat váš telefon Android pokud používáte vlastnost Vzdálený vstup.
* Oprávnění o poloze na pozadí: Je potřebné pro rozpoznání, ke které síti WiFi jste připojeni, pokud používáte funkci Důvěryhodné sítě.
KDE Connect nikdy neposílá KDE ani nikomu jinému jakékoliv informace. KDE Connect odesílá data z jednoho zařízení druhému přímo přes místní síť, nikdy přes internet, a výhradně s použitím koncového šifrování.
Tato aplikace je součástí Open Source projektu a existuje jenom díky přispěvatelům. Zdrojové soubory naleznete na webových stránkách.

View File

@@ -1 +0,0 @@
KDE Connect integruje váš chytrý telefon a počítač

View File

@@ -1 +0,0 @@
KDE Connect

View File

@@ -1,21 +0,0 @@
KDE Connect bietet eine Reihe von Funktionen, um Ihre Arbeitsabläufe über verschiedene Geräte zu vereinigen:
- Daten zwischen Ihren Geräten übertragen.
- Auf Daten auf Ihrem Telefon von Ihrem Computer aus zugreifen, ohne Kabel.
- Geteilte Zwischenablage: Kopieren und Einfügen zwischen Ihren Geräten.
- Erhalten Sie Benachrichtigungen über eingehende Anrufe und Nachrichten auf Ihren Computer.
- Virtuelles Touchpad: Verwenden Sie den Bildschirm Ihres Telefons als Touchpad für Ihren Computer.
- Abgleich der Benachrichtigungen: Greifen Sie über den Computer auf Ihre Telefonbenachrichtigungen zu und antworten Sie auf Nachrichten.
- Multimedia-Fernbedienung: Verwenden Sie Ihr Telefon als Fernbedienung für Linux-Medienspieler.
- WLAN-Verbindung: kein USB-Kabel oder Bluetooth erforderlich.
- Ende-zu-Ende-TLS-Verschlüsselung: Ihre Informationen sind sicher.
Bitte beachten Sie, dass Sie KDE Connect auf Ihrem Computer installieren müssen, damit diese App funktioniert und halten Sie die Desktop-Version mit der Android-Version auf dem aktuellen Stand, um die neuesten Funktionen nutzen zu können.
Informationen zu sensiblen Berechtigungen:
* Zugriffsberechtigung: Wird benötigt, um Eingaben zur Steuerung ihres Android-Telefons von einem anderen Gerät zu erhalten, wenn Sie die Ferneingabefunktion verwenden.
* Berechtigung den Standort im Hintergrund zu nutzen: Wird benötigt, um festzustellen, mit welchem WLAN-Netzwerk Sie verbunden sind, wenn Sie die Funktion „Vertrauenswürdige Netzwerke” verwenden.
KDE Connect sendet niemals irgendwelche Informationen an KDE oder an Dritte. KDE Connect sendet Daten, unter Verwendung einer Ende-zu-Ende-Verschlüsselung, über das lokale Netzwerk direkt von einem Gerät zum anderen, niemals über das Internet.
Diese App ist Teil eines Open-Scource-Projekts und besteht Dank all der Menschen die dazu beigetragen haben. Besuchen Sie die Internetseite, um sich den Quelltext zu holen.

View File

@@ -1 +0,0 @@
KDE Connect verbindet Ihr Smartphone mit Ihrem Computer

Some files were not shown because too many files have changed in this diff Show More