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

Compare commits

..

1 Commits
v0.9g ... v0.1

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
261 changed files with 4410 additions and 16274 deletions

16
.gitignore vendored
View File

@@ -1,14 +1,4 @@
local.properties
.gradle/
.idea/
out/
gen/
bin/
build/
target/
classes/
gradle
gradlew
gradlew.bat
*.iml
*.keystore
.gradle
.idea
out

View File

@@ -1,3 +0,0 @@
REVIEWBOARD_URL = "https://git.reviewboard.kde.org"
REPOSITORY = 'git://anongit.kde.org/kdeconnect-android'
TARGET_GROUPS = 'kdeconnect'

View File

@@ -1,181 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.kde.kdeconnect_tp"
android:versionCode="910"
android:versionName="0.9g">
<uses-sdk android:minSdkVersion="9"
android:targetSdkVersion="22" />
<supports-screens
android:anyDensity="true"
android:largeScreens="true"
android:normalScreens="true"
android:smallScreens="true"
android:xlargeScreens="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" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
android:allowBackup="true"
android:icon="@drawable/icon"
android:label="KDE Connect"
android:theme="@style/KdeConnectTheme"
>
<service
android:name="org.kde.kdeconnect.BackgroundService"
android:enabled="true" >
</service>
<activity
android:name="org.kde.kdeconnect.UserInterface.MaterialActivity"
android:label="KDE Connect"
android:theme="@style/KdeConnectTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name="org.kde.kdeconnect.UserInterface.SettingsActivity"
android:label="@string/device_menu_plugins"
android:parentActivityName="org.kde.kdeconnect.UserInterface.MaterialActivity" >
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.kde.kdeconnect.UserInterface.MaterialActivity" />
</activity>
<activity
android:name="org.kde.kdeconnect.UserInterface.CustomDevicesActivity"
android:label="@string/custom_devices_settings"
android:parentActivityName="org.kde.kdeconnect.UserInterface.MaterialActivity" >
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.kde.kdeconnect.UserInterface.MaterialActivity" />
</activity>
<activity
android:name="org.kde.kdeconnect.Plugins.SharePlugin.SendFileActivity"
android:label="KDE Connect"
android:parentActivityName="org.kde.kdeconnect.UserInterface.MaterialActivity" >
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.kde.kdeconnect.UserInterface.MaterialActivity" />
</activity>
<activity
android:name="org.kde.kdeconnect.UserInterface.PluginSettingsActivity"
android:label="@string/device_menu_plugins"
android:parentActivityName="org.kde.kdeconnect.UserInterface.SettingsActivity" >
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.kde.kdeconnect.UserInterface.SettingsActivity" />
</activity>
<receiver android:name="org.kde.kdeconnect.KdeConnectBroadcastReceiver" >
<intent-filter>
<action android:name="android.intent.action.PACKAGE_REPLACED" />
<data
android:host="kdeconnect"
android:path="/"
android:scheme="package" />
</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>
<!-- Plugin-related activities and services -->
<activity
android:name="org.kde.kdeconnect.Plugins.MprisPlugin.MprisActivity"
android:label="@string/remote_control"
android:parentActivityName="org.kde.kdeconnect.UserInterface.MaterialActivity"
>
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.kde.kdeconnect.UserInterface.MaterialActivity" />
</activity>
<activity
android:name="org.kde.kdeconnect.Plugins.MousePadPlugin.MousePadActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
android:label="@string/remote_control"
android:parentActivityName="org.kde.kdeconnect.UserInterface.MaterialActivity"
android:windowSoftInputMode="stateHidden|adjustResize" >
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.kde.kdeconnect.UserInterface.MaterialActivity" />
</activity>
<activity
android:name="org.kde.kdeconnect.Plugins.SharePlugin.ShareActivity"
android:label="KDE Connect"
>
<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>
</activity>
<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>
<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>
</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

@@ -1,26 +1,5 @@
/*
* Copyright 2014 Albert Vaca Cintora <albertvaka@gmail.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of
* the License or (at your option) version 3 or any later version
* accepted by the membership of KDE e.V. (or its successor approved
* by the membership of KDE e.V.), which shall act as a proxy
* defined in Section 14 of version 3 of the license.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.kde.kdeconnect;
import android.app.Activity;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
@@ -32,117 +11,57 @@ import android.preference.PreferenceManager;
import android.util.Base64;
import android.util.Log;
import org.kde.kdeconnect.Backends.BaseLink;
import org.kde.kdeconnect.Backends.BaseLinkProvider;
import org.kde.kdeconnect.Backends.LanBackend.LanLinkProvider;
import org.kde.kdeconnect.Backends.LoopbackBackend.LoopbackLinkProvider;
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.HashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class BackgroundService extends Service {
public interface DeviceListChangedCallback {
void onDeviceListChanged();
}
private ArrayList<BaseLinkProvider> linkProviders = new ArrayList<BaseLinkProvider>();
private final ConcurrentHashMap<String, DeviceListChangedCallback> deviceListChangedCallbacks = new ConcurrentHashMap<>();
private HashMap<String, Device> devices = new HashMap<String, Device>();
private final ArrayList<BaseLinkProvider> linkProviders = new ArrayList<>();
private final ConcurrentHashMap<String, Device> devices = new ConcurrentHashMap<>();
private final HashSet<Object> discoveryModeAcquisitions = new HashSet<>();
public boolean acquireDiscoveryMode(Object key) {
boolean wasEmpty = discoveryModeAcquisitions.isEmpty();
discoveryModeAcquisitions.add(key);
if (wasEmpty) {
onNetworkChange();
}
return wasEmpty;
}
public void releaseDiscoveryMode(Object key) {
boolean removed = discoveryModeAcquisitions.remove(key);
if (removed && discoveryModeAcquisitions.isEmpty()) {
cleanDevices();
}
}
public static void addGuiInUseCounter(Context activity) {
addGuiInUseCounter(activity, false);
}
public static void addGuiInUseCounter(final Context activity, final boolean forceNetworkRefresh) {
BackgroundService.RunCommand(activity, new BackgroundService.InstanceCallback() {
@Override
public void onServiceStart(BackgroundService service) {
boolean refreshed = service.acquireDiscoveryMode(activity);
if (!refreshed && forceNetworkRefresh) {
service.onNetworkChange();
}
}
});
}
public static void removeGuiInUseCounter(final Context activity) {
BackgroundService.RunCommand(activity, new BackgroundService.InstanceCallback() {
@Override
public void onServiceStart(BackgroundService service) {
//If no user interface is open, close the connections open to other devices
service.releaseDiscoveryMode(activity);
}
});
}
private final Device.PairingCallback devicePairingCallback = new Device.PairingCallback() {
private Device.PairingCallback devicePairingCallback = new Device.PairingCallback() {
@Override
public void incomingRequest() {
onDeviceListChanged();
if (deviceListChangedCallback != null) deviceListChangedCallback.onDeviceListChanged();
}
@Override
public void pairingSuccessful() {
onDeviceListChanged();
if (deviceListChangedCallback != null) deviceListChangedCallback.onDeviceListChanged();
}
@Override
public void pairingFailed(String error) {
onDeviceListChanged();
if (deviceListChangedCallback != null) deviceListChangedCallback.onDeviceListChanged();
}
@Override
public void unpaired() {
onDeviceListChanged();
if (deviceListChangedCallback != null) deviceListChangedCallback.onDeviceListChanged();
}
};
private void onDeviceListChanged() {
for(DeviceListChangedCallback callback : deviceListChangedCallbacks.values()) {
callback.onDeviceListChanged();
}
}
private void loadRememberedDevicesFromSettings() {
//Log.e("BackgroundService", "Loading remembered trusted devices");
SharedPreferences preferences = getSharedPreferences("trusted_devices", Context.MODE_PRIVATE);
Set<String> trustedDevices = preferences.getAll().keySet();
for(String deviceId : trustedDevices) {
//Log.e("BackgroundService", "Loading device "+deviceId);
if (preferences.getBoolean(deviceId, false)) {
Device device = new Device(this, deviceId);
Device device = new Device(getBaseContext(), deviceId);
devices.put(deviceId,device);
device.addPairingCallback(devicePairingCallback);
}
}
}
private void registerLinkProviders() {
public void registerLinkProviders() {
SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(this);
@@ -160,71 +79,74 @@ public class BackgroundService extends Service {
return devices.get(id);
}
private void cleanDevices() {
for(Device d : devices.values()) {
if (!d.isPaired() && !d.isPairRequested() && !d.isPairRequestedByOtherEnd() && d.getConnectionSource() == BaseLink.ConnectionStarted.Remotely) {
d.disconnect();
}
}
}
private final BaseLinkProvider.ConnectionReceiver deviceListener = new BaseLinkProvider.ConnectionReceiver() {
private BaseLinkProvider.ConnectionReceiver deviceListener = new BaseLinkProvider.ConnectionReceiver() {
@Override
public void onConnectionReceived(final NetworkPackage identityPackage, final BaseLink link) {
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("KDE/BackgroundService", "addLink, known device: " + deviceId);
device.addLink(identityPackage, link);
Log.i("BackgroundService", "addLink, known device: " + deviceId);
device.addLink(link);
} else {
Log.i("KDE/BackgroundService", "addLink,unknown device: " + deviceId);
device = new Device(BackgroundService.this, identityPackage, link);
if (device.isPaired() || device.isPairRequested() || device.isPairRequestedByOtherEnd()
|| link.getConnectionSource() == BaseLink.ConnectionStarted.Locally
||!discoveryModeAcquisitions.isEmpty() )
{
devices.put(deviceId, device);
device.addPairingCallback(devicePairingCallback);
} else {
device.disconnect();
}
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);
}
onDeviceListChanged();
if (deviceListChangedCallback != null) deviceListChangedCallback.onDeviceListChanged();
}
@Override
public void onConnectionLost(BaseLink link) {
public void onConnectionLost(BaseComputerLink link) {
Device d = devices.get(link.getDeviceId());
Log.i("KDE/onConnectionLost", "removeLink, deviceId: " + link.getDeviceId());
Log.i("onConnectionLost", "removeLink, deviceId: " + link.getDeviceId());
if (d != null) {
d.removeLink(link);
if (!d.isReachable() && !d.isPaired()) {
//Log.e("onConnectionLost","Removing connection device because it was not paired");
devices.remove(link.getDeviceId());
d.removePairingCallback(devicePairingCallback);
}
} else {
//Log.d("KDE/onConnectionLost","Removing connection to unknown device");
Log.e("onConnectionLost","Removing connection to unknown device, this should not happen");
}
onDeviceListChanged();
if (deviceListChangedCallback != null) deviceListChangedCallback.onDeviceListChanged();
}
};
public ConcurrentHashMap<String, Device> getDevices() {
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);
}
@@ -236,13 +158,15 @@ public class BackgroundService extends Service {
}
}
public void addDeviceListChangedCallback(String key, DeviceListChangedCallback callback) {
deviceListChangedCallbacks.put(key, callback);
public interface DeviceListChangedCallback {
void onDeviceListChanged();
}
public void removeDeviceListChangedCallback(String key) {
deviceListChangedCallbacks.remove(key);
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() {
@@ -252,7 +176,7 @@ public class BackgroundService extends Service {
IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_ON);
registerReceiver(new KdeConnectBroadcastReceiver(), filter);
Log.i("KDE/BackgroundService", "Service not started yet, initializing...");
Log.i("BackgroundService","Service not started yet, initializing...");
initializeRsaKeys();
loadRememberedDevicesFromSettings();
@@ -260,10 +184,7 @@ public class BackgroundService extends Service {
//Link Providers need to be already registered
addConnectionListener(deviceListener);
for (BaseLinkProvider a : linkProviders) {
a.onStart();
}
startDiscovery();
}
@@ -279,7 +200,7 @@ public class BackgroundService extends Service {
keyPair = keyGen.genKeyPair();
} catch(Exception e) {
e.printStackTrace();
Log.e("KDE/initializeRsaKeys","Exception");
Log.e("initializeRsaKeys","Exception");
return;
}
@@ -289,7 +210,7 @@ public class BackgroundService extends Service {
SharedPreferences.Editor edit = settings.edit();
edit.putString("publicKey",Base64.encodeToString(publicKey, 0).trim()+"\n");
edit.putString("privateKey",Base64.encodeToString(privateKey, 0));
edit.apply();
edit.commit();
}
@@ -324,9 +245,8 @@ public class BackgroundService extends Service {
@Override
public void onDestroy() {
for (BaseLinkProvider a : linkProviders) {
a.onStop();
}
Log.i("BackgroundService", "Destroying");
stopDiscovery();
super.onDestroy();
}
@@ -342,13 +262,14 @@ public class BackgroundService extends Service {
void onServiceStart(BackgroundService service);
}
private final static ArrayList<InstanceCallback> callbacks = new ArrayList<>();
private static ArrayList<InstanceCallback> callbacks = new ArrayList<InstanceCallback>();
private final static Lock mutex = new ReentrantLock(true);
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);
@@ -362,19 +283,14 @@ public class BackgroundService extends Service {
RunCommand(c, null);
}
public static void RunCommand(final Context c, final InstanceCallback callback) {
new Thread(new Runnable() {
@Override
public void run() {
if (callback != null) {
mutex.lock();
callbacks.add(callback);
mutex.unlock();
}
Intent serviceIntent = new Intent(c, BackgroundService.class);
c.startService(serviceIntent);
}
}).start();
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

@@ -1,23 +1,3 @@
/*
* Copyright 2014 Albert Vaca Cintora <albertvaka@gmail.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of
* the License or (at your option) version 3 or any later version
* accepted by the membership of KDE e.V. (or its successor approved
* by the membership of KDE e.V.), which shall act as a proxy
* defined in Section 14 of version 3 of the license.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.kde.kdeconnect.Helpers;
import android.content.Context;
@@ -33,7 +13,7 @@ public class AppsHelper {
try {
PackageManager pm = context.getPackageManager();
ApplicationInfo ai = pm.getApplicationInfo(packageName, 0);
ApplicationInfo ai = pm.getApplicationInfo( packageName, 0);
return pm.getApplicationLabel(ai).toString();
@@ -69,4 +49,4 @@ public class AppsHelper {
}
}

View File

@@ -1,29 +1,11 @@
/*
* Copyright 2014 Albert Vaca Cintora <albertvaka@gmail.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of
* the License or (at your option) version 3 or any later version
* accepted by the membership of KDE e.V. (or its successor approved
* by the membership of KDE e.V.), which shall act as a proxy
* defined in Section 14 of version 3 of the license.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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 {
@@ -49,18 +31,15 @@ public class ContactsHelper {
// Take the first match only
if (cursor != null && cursor.moveToFirst()) {
int nameIndex = cursor.getColumnIndex(PhoneLookup.DISPLAY_NAME);
int nameIndex = cursor.getColumnIndex(ContactsContract.PhoneLookup.DISPLAY_NAME);
if (nameIndex != -1) {
String name = cursor.getString(nameIndex);
//Log.e("PhoneNumberLookup", "success: " + name);
cursor.close();
return name + " (" + number + ")";
return name;
}
}
if (cursor != null) cursor.close();
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

@@ -1,23 +1,3 @@
/*
* Copyright 2014 Albert Vaca Cintora <albertvaka@gmail.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of
* the License or (at your option) version 3 or any later version
* accepted by the membership of KDE e.V. (or its successor approved
* by the membership of KDE e.V.), which shall act as a proxy
* defined in Section 14 of version 3 of the license.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.kde.kdeconnect;
import android.content.Context;
@@ -30,12 +10,8 @@ import android.util.Log;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.kde.kdeconnect.Helpers.DeviceHelper;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.security.GeneralSecurityException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.ArrayList;
@@ -44,58 +20,41 @@ import javax.crypto.Cipher;
public class NetworkPackage {
public final static int ProtocolVersion = 5;
public final static int ProtocolVersion = 3;
//TODO: Move these to their respective plugins
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_SFTP = "kdeconnect.sftp";
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";
public final static String PACKAGE_TYPE_MOUSEPAD = "kdeconnect.mousepad";
public final static String PACKAGE_TYPE_SHARE = "kdeconnect.share";
private long mId;
private String mType;
private JSONObject mBody;
private InputStream mPayload;
private JSONObject mPayloadTransferInfo;
private long mPayloadSize;
private NetworkPackage() {
}
public NetworkPackage(String type) {
mId = System.currentTimeMillis();
mType = type;
mBody = new JSONObject();
mPayload = null;
mPayloadSize = 0;
mPayloadTransferInfo = new JSONObject();
}
public String getType() {
return mType;
}
public long getId() {
return mId;
}
//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 long getLong(String key) { return mBody.optLong(key,-1); }
public long getLong(String key,long defaultValue) { return mBody.optLong(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); }
@@ -103,13 +62,9 @@ public class NetworkPackage {
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 JSONArray getJSONArray(String key) { return mBody.optJSONArray(key); }
public void set(String key, JSONArray value) { try { mBody.put(key,value); } catch(Exception e) { } }
public ArrayList<String> getStringList(String key) {
JSONArray jsonArray = mBody.optJSONArray(key);
if (jsonArray == null) return null;
ArrayList<String> list = new ArrayList<>();
ArrayList<String> list = new ArrayList<String>();
int length = jsonArray.length();
for (int i = 0; i < length; i++) {
try {
@@ -136,64 +91,40 @@ public class NetworkPackage {
}
}
public boolean has(String key) { return mBody.has(key); }
public boolean isEncrypted() { return mType.equals(PACKAGE_TYPE_ENCRYPTED); }
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);
if (hasPayload()) {
jo.put("payloadSize", mPayloadSize);
jo.put("payloadTransferInfo", mPayloadTransferInfo);
}
jo.put("id",mId);
jo.put("type",mType);
jo.put("body",mBody);
} catch(Exception e) {
e.printStackTrace();
Log.e("NetworkPackage", "Serialization exception");
}
//QJSon does not escape slashes, but Java JSONObject does. Converting to QJson format.
String json = jo.toString().replace("\\/","/")+"\n";
if (!isEncrypted()) {
//Log.e("NetworkPackage.serialize", json);
}
//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");
if (jo.has("payloadSize")) {
np.mPayloadTransferInfo = jo.getJSONObject("payloadTransferInfo");
np.mPayloadSize = jo.getLong("payloadSize");
} else {
np.mPayloadTransferInfo = new JSONObject();
np.mPayloadSize = 0;
}
} catch (Exception e) {
e.printStackTrace();
Log.e("NetworkPackage", "Unserialization exception unserializing "+s);
return null;
}
if (!np.isEncrypted()) {
//Log.e("NetworkPackage.unserialize", s);
}
return np;
}
public NetworkPackage encrypt(PublicKey publicKey) throws GeneralSecurityException {
public void encrypt(PublicKey publicKey) throws Exception {
String serialized = serialize();
@@ -215,16 +146,21 @@ public class NetworkPackage {
chunks.put(Base64.encodeToString(encryptedChunk, Base64.NO_WRAP));
}
//Log.i("NetworkPackage", "Encrypted " + chunks.length()+" chunks");
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");
}
NetworkPackage encrypted = new NetworkPackage(NetworkPackage.PACKAGE_TYPE_ENCRYPTED);
encrypted.set("data", chunks);
encrypted.setPayload(mPayload, mPayloadSize);
return encrypted;
Log.i("NetworkPackage", "Encrypted " + chunks.length()+" chunks");
}
public NetworkPackage decrypt(PrivateKey privateKey) throws GeneralSecurityException, JSONException {
public NetworkPackage decrypt(PrivateKey privateKey) throws Exception {
JSONArray chunks = mBody.getJSONArray("data");
@@ -238,31 +174,26 @@ public class NetworkPackage {
decryptedJson += decryptedChunk;
}
NetworkPackage decrypted = unserialize(decryptedJson);
decrypted.setPayload(mPayload, mPayloadSize);
return decrypted;
return unserialize(decryptedJson);
}
static public NetworkPackage createIdentityPackage(Context context) {
NetworkPackage np = new NetworkPackage(NetworkPackage.PACKAGE_TYPE_IDENTITY);
String deviceId = DeviceHelper.getDeviceId(context);
String deviceId = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
try {
np.mBody.put("deviceId", deviceId);
np.mBody.put("deviceName", DeviceHelper.getDeviceName(context));
np.mBody.put("deviceName", HumanDeviceNames.getDeviceName());
np.mBody.put("protocolVersion", NetworkPackage.ProtocolVersion);
np.mBody.put("deviceType", DeviceHelper.isTablet()? "tablet" : "phone");
} catch (Exception e) {
e.printStackTrace();
Log.e("NetworkPacakge","Exception on createIdentityPackage");
} catch (JSONException e) {
}
return np;
}
static public NetworkPackage createPublicKeyPackage(Context context) {
NetworkPackage np = new NetworkPackage(NetworkPackage.PACKAGE_TYPE_PAIR);
@@ -277,40 +208,4 @@ public class NetworkPackage {
}
public void setPayload(byte[] data) {
setPayload(new ByteArrayInputStream(data), data.length);
}
public void setPayload(InputStream stream, long size) {
mPayload = stream;
mPayloadSize = size;
}
/*public void setPayload(InputStream stream) {
setPayload(stream, -1);
}*/
public InputStream getPayload() {
return mPayload;
}
public long getPayloadSize() {
return mPayloadSize;
}
public boolean hasPayload() {
return (mPayload != null);
}
public boolean hasPayloadTransferInfo() {
return (mPayloadTransferInfo.length() > 0);
}
public JSONObject getPayloadTransferInfo() {
return mPayloadTransferInfo;
}
public void setPayloadTransferInfo(JSONObject payloadTransferInfo) {
mPayloadTransferInfo = payloadTransferInfo;
}
}

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

@@ -1,29 +1,11 @@
/*
* Copyright 2014 Albert Vaca Cintora <albertvaka@gmail.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of
* the License or (at your option) version 3 or any later version
* accepted by the membership of KDE e.V. (or its successor approved
* by the membership of KDE e.V.), which shall act as a proxy
* defined in Section 14 of version 3 of the license.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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.support.v4.content.ContextCompat;
import android.os.Build;
import android.widget.Button;
import org.kde.kdeconnect.NetworkPackage;
@@ -32,6 +14,14 @@ 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);
@@ -42,27 +32,43 @@ public class ClipboardPlugin extends Plugin {
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() {
//Disabled by default due to just one direction sync(incoming clipboard change) in early version of android.
return (android.os.Build.VERSION.SDK_INT >= 11);
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;
}
@@ -70,6 +76,25 @@ public class ClipboardPlugin extends Plugin {
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,23 +1,3 @@
/*
* Copyright 2014 Albert Vaca Cintora <albertvaka@gmail.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of
* the License or (at your option) version 3 or any later version
* accepted by the membership of KDE e.V. (or its successor approved
* by the membership of KDE e.V.), which shall act as a proxy
* defined in Section 14 of version 3 of the license.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.kde.kdeconnect.Plugins.NotificationsPlugin;
import android.app.Service;
@@ -25,10 +5,9 @@ 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 {
@@ -37,7 +16,7 @@ public class NotificationReceiver extends NotificationListenerService {
void onNotificationRemoved(StatusBarNotification statusBarNotification);
}
private final ArrayList<NotificationListener> listeners = new ArrayList<>();
private ArrayList<NotificationListener> listeners = new ArrayList<NotificationListener>();
public void addListener(NotificationListener listener) {
listeners.add(listener);
@@ -48,7 +27,7 @@ public class NotificationReceiver extends NotificationListenerService {
@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);
}
@@ -67,38 +46,29 @@ public class NotificationReceiver extends NotificationListenerService {
//To use the service from the outer (name)space
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();
Log.i("NotificationReceiver", "onStartCommand");
for (InstanceCallback c : callbacks) {
c.onServiceStart(this);
}
callbacks.clear();
mutex.unlock();
return Service.START_STICKY;
}
public interface InstanceCallback {
void onServiceStart(NotificationReceiver service);
}
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();
callbacks.add(callback);
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

@@ -1,47 +1,33 @@
/*
* Copyright 2014 Albert Vaca Cintora <albertvaka@gmail.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of
* the License or (at your option) version 3 or any later version
* accepted by the membership of KDE e.V. (or its successor approved
* by the membership of KDE e.V.), which shall act as a proxy
* defined in Section 14 of version 3 of the license.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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.media.AudioManager;
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;
import java.util.Timer;
import java.util.TimerTask;
public class TelephonyPlugin extends Plugin {
private int lastState = TelephonyManager.CALL_STATE_IDLE;
private NetworkPackage lastPackage = null;
private boolean isMuted = false;
/*static {
PluginFactory.registerPlugin(TelephonyPlugin.class);
}*/
@Override
public String getPluginName() {
return "plugin_telephony";
}
@Override
public String getDisplayName() {
@@ -53,7 +39,17 @@ public class TelephonyPlugin extends Plugin {
return context.getResources().getString(R.string.pref_plugin_telephony_desc);
}
private final BroadcastReceiver receiver = new BroadcastReceiver() {
@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) {
@@ -89,12 +85,16 @@ public class TelephonyPlugin extends Plugin {
callBroadcastReceived(finalIntState, finalNumber);
}
}
};
private void callBroadcastReceived(int state, String phoneNumber) {
//Log.e("TelephonyPlugin", "callBroadcastReceived");
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) {
@@ -104,11 +104,6 @@ public class TelephonyPlugin extends Plugin {
switch (state) {
case TelephonyManager.CALL_STATE_RINGING:
if (isMuted) {
AudioManager am = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
am.setStreamMute(AudioManager.STREAM_RING, false);
isMuted = false;
}
np.set("event", "ringing");
device.sendPackage(np);
break;
@@ -126,20 +121,6 @@ public class TelephonyPlugin extends Plugin {
lastPackage.set("isCancel","true");
device.sendPackage(lastPackage);
if (isMuted) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
if (isMuted) {
AudioManager am = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
am.setStreamMute(AudioManager.STREAM_RING, false);
isMuted = false;
}
}
}, 500);
}
//Emit a missed call notification if needed
if (lastState == TelephonyManager.CALL_STATE_RINGING) {
np.set("event","missedCall");
@@ -157,7 +138,7 @@ public class TelephonyPlugin extends Plugin {
lastState = state;
}
private void smsBroadcastReceived(SmsMessage message) {
public void smsBroadcastReceived(SmsMessage message) {
//Log.e("SmsBroadcastReceived", message.toString());
@@ -181,9 +162,7 @@ public class TelephonyPlugin extends Plugin {
@Override
public boolean onCreate() {
//Log.e("TelephonyPlugin", "onCreate");
IntentFilter filter = new IntentFilter("android.provider.Telephony.SMS_RECEIVED");
filter.setPriority(500);
filter.addAction(TelephonyManager.ACTION_PHONE_STATE_CHANGED);
context.registerReceiver(receiver, filter);
return true;
@@ -196,19 +175,17 @@ public class TelephonyPlugin extends Plugin {
@Override
public boolean onPackageReceived(NetworkPackage np) {
if (!np.getType().equals(NetworkPackage.PACKAGE_TYPE_TELEPHONY)) {
return false;
}
if (np.getString("action").equals("mute")) {
if (!isMuted) {
AudioManager am = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
am.setStreamMute(AudioManager.STREAM_RING, true);
isMuted = true;
}
//Log.e("TelephonyPlugin", "mute");
}
//Do nothing
return true;
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

@@ -2,10 +2,13 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="16dip"
android:paddingRight="16dip"
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

@@ -5,20 +5,9 @@
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:drawableBottom="@drawable/text_section_header"
android:textStyle="bold"
android:layout_marginTop="8dp"
android:textColor="?android:textColorSecondary"
android:drawablePadding="4dp"
android:textSize="14sp"
android:textAllCaps="true"
android:paddingLeft="8dip"
android:paddingRight="8dip"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:id="@+id/list_item_category_text"
/>
<include
android:id="@+id/list_item_category_text"
layout="@android:layout/preference_category" />
<TextView
android:layout_width="wrap_content"

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

@@ -1,20 +1,21 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:kdeconnect="http://schemas.android.com/apk/res-auto/android">
xmlns:kdeconnect="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/menu_refresh"
android:icon="@drawable/ic_action_refresh"
android:icon="@drawable/navigation_refresh"
android:orderInCategory="200"
kdeconnect:showAsAction="ifRoom"
android:title="@string/refresh"
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="ifRoom"
kdeconnect:showAsAction="always"
kdeconnect:actionViewClass="android.widget.ProgressBar"
/>
</menu>
</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,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,25 +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 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.
All this without wires, over the already existing WiFi network, and using a secure, encrypted protocol.
## 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://github.com/albertvaka/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://github.com/albertvaka/kdeconnect-kde) for it to work.
## 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://projects.kde.org/projects/playground/base/kdeconnect-android/repository/).

View File

@@ -1,23 +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
ANSI_COLORS_DISABLED=1 a2po import --ignore-fuzzy --android res/ --gettext $podir
#Android doesn't support languages with an @
rm -r res/values-*@*
}

View File

@@ -1,67 +1,2 @@
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:1.3.0'
}
}
// Top-level build file where you can add configuration options common to all sub-projects/modules.
apply plugin: 'com.android.application'
android {
compileSdkVersion 22
buildToolsVersion '22.0.1'
defaultConfig {
minSdkVersion 9
targetSdkVersion 22
}
sourceSets {
main {
manifest.srcFile 'AndroidManifest.xml'
java.srcDirs = ['src']
resources.srcDirs = ['resources']
res.srcDirs = ['res']
assets.srcDirs = ['assets']
}
androidTest {
java.srcDirs = ['tests']
}
}
packagingOptions {
pickFirst "META-INF/DEPENDENCIES"
pickFirst "META-INF/LICENSE"
pickFirst "META-INF/NOTICE"
}
lintOptions {
abortOnError false
checkReleaseBuilds false
}
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'),'proguard-rules.pro'
}
}
}
dependencies {
repositories {
mavenCentral()
}
compile 'com.android.support:support-v4:22.2.1'
compile 'com.android.support:appcompat-v7:22.2.1'
compile 'com.android.support:design:22.2.1'
compile 'org.apache.mina:mina-core:2.0.9'
compile 'org.apache.sshd:sshd-core:0.8.0'
compile 'org.bouncycastle:bcprov-jdk16:1.46'
androidTestCompile 'org.mockito:mockito-core:1.10.19'
// Because mockito has some problems with dex environment
androidTestCompile 'com.google.dexmaker:dexmaker:1.1'
androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.1'
//compile fileTree(dir: 'libs', include: '*.jar')
}

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,6 @@
#Wed Apr 10 15:27:10 PDT 2013
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=http\://services.gradle.org/distributions/gradle-1.6-bin.zip

164
gradlew vendored Executable file
View File

@@ -0,0 +1,164 @@
#!/usr/bin/env bash
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn ( ) {
echo "$*"
}
die ( ) {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
esac
# For Cygwin, ensure paths are in UNIX format before anything is touched.
if $cygwin ; then
[ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
fi
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >&-
APP_HOME="`pwd -P`"
cd "$SAVED" >&-
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
function splitJvmOpts() {
JVM_OPTS=("$@")
}
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"

90
gradlew.bat vendored Normal file
View File

@@ -0,0 +1,90 @@
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windowz variants
if not "%OS%" == "Windows_NT" goto win9xME_args
if "%@eval[2+2]" == "4" goto 4NT_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
goto execute
:4NT_args
@rem Get arguments from the 4NT Shell from JP Software
set CMD_LINE_ARGS=%$
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

880
icon.svg
View File

@@ -1,880 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="274.43201"
height="274.43201"
id="svg2"
version="1.1"
inkscape:version="0.91 r"
sodipodi:docname="icon.svg"
inkscape:export-filename="/home/vaka/kdeconnect/kdeconnect-android/res/drawable-mdpi/icon.png"
inkscape:export-xdpi="15.741604"
inkscape:export-ydpi="15.741604">
<defs
id="defs4">
<linearGradient
inkscape:collect="always"
id="linearGradient4123">
<stop
style="stop-color:#b3b3b3;stop-opacity:1;"
offset="0"
id="stop4125" />
<stop
style="stop-color:#b3b3b3;stop-opacity:0;"
offset="1"
id="stop4127" />
</linearGradient>
<linearGradient
id="linearGradient4022">
<stop
style="stop-color:#e64f3e;stop-opacity:1;"
offset="0"
id="stop4024" />
<stop
style="stop-color:#e63e47;stop-opacity:1;"
offset="1"
id="stop4026" />
</linearGradient>
<linearGradient
inkscape:collect="always"
id="linearGradient4303">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop4305" />
<stop
style="stop-color:#ffffff;stop-opacity:0;"
offset="1"
id="stop4307" />
</linearGradient>
<linearGradient
inkscape:collect="always"
id="linearGradient4276">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop4278" />
<stop
style="stop-color:#ffffff;stop-opacity:0;"
offset="1"
id="stop4280" />
</linearGradient>
<linearGradient
id="linearGradient3994">
<stop
style="stop-color:#00d1c9;stop-opacity:1;"
offset="0"
id="stop3996" />
<stop
style="stop-color:#0081e3;stop-opacity:1;"
offset="1"
id="stop3998" />
</linearGradient>
<linearGradient
inkscape:collect="always"
id="linearGradient3939">
<stop
style="stop-color:#006eff;stop-opacity:1;"
offset="0"
id="stop3941" />
<stop
style="stop-color:#006eff;stop-opacity:0;"
offset="1"
id="stop3943" />
</linearGradient>
<linearGradient
id="linearGradient3872">
<stop
style="stop-color:#09e0ff;stop-opacity:1;"
offset="0"
id="stop3874" />
<stop
style="stop-color:#3d8fc2;stop-opacity:1;"
offset="1"
id="stop3876" />
</linearGradient>
<linearGradient
id="linearGradient3931">
<stop
style="stop-color:#202020;stop-opacity:1;"
offset="0"
id="stop3933" />
<stop
style="stop-color:#1a1a1a;stop-opacity:1;"
offset="1"
id="stop3935" />
</linearGradient>
<linearGradient
id="linearGradient3896">
<stop
style="stop-color:#cd602b;stop-opacity:1;"
offset="0"
id="stop3898" />
<stop
style="stop-color:#cd2b2b;stop-opacity:1;"
offset="1"
id="stop3900" />
</linearGradient>
<linearGradient
id="linearGradient4134">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop4136" />
<stop
style="stop-color:#ececec;stop-opacity:1;"
offset="1"
id="stop4138" />
</linearGradient>
<linearGradient
id="linearGradient3916">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop3918" />
<stop
style="stop-color:#e5e5e5;stop-opacity:1;"
offset="1"
id="stop3920" />
</linearGradient>
<linearGradient
id="linearGradient3838">
<stop
style="stop-color:#00ccff;stop-opacity:1;"
offset="0"
id="stop3840" />
<stop
id="stop3842"
offset="0.50000006"
style="stop-color:#db1cd8;stop-opacity:0.29374999;" />
<stop
style="stop-color:#00ccff;stop-opacity:0;"
offset="1"
id="stop3844" />
</linearGradient>
<linearGradient
id="linearGradient4622">
<stop
id="stop4624"
offset="0"
style="stop-color:#00ccff;stop-opacity:1;" />
<stop
style="stop-color:#db1cd8;stop-opacity:0.29374999;"
offset="0.62261778"
id="stop4626" />
<stop
id="stop4628"
offset="1"
style="stop-color:#00ccff;stop-opacity:0;" />
</linearGradient>
<linearGradient
id="linearGradient3883">
<stop
style="stop-color:#00ff6d;stop-opacity:1;"
offset="0"
id="stop3885" />
<stop
style="stop-color:#00ff9d;stop-opacity:0;"
offset="1"
id="stop3887" />
</linearGradient>
<linearGradient
id="linearGradient3875">
<stop
style="stop-color:#ff0061;stop-opacity:1;"
offset="0"
id="stop3877" />
<stop
style="stop-color:#ff0061;stop-opacity:0;"
offset="1"
id="stop3879" />
</linearGradient>
<linearGradient
id="linearGradient3814">
<stop
style="stop-color:#008d9c;stop-opacity:1;"
offset="0"
id="stop3816" />
<stop
style="stop-color:#008d9c;stop-opacity:0;"
offset="1"
id="stop3818" />
</linearGradient>
<linearGradient
id="linearGradient3769">
<stop
style="stop-color:#00ccff;stop-opacity:1;"
offset="0"
id="stop3771" />
<stop
id="stop3784"
offset="0.5"
style="stop-color:#db1cd8;stop-opacity:0.29374999;" />
<stop
style="stop-color:#00ccff;stop-opacity:0;"
offset="1"
id="stop3773" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3769-5"
id="linearGradient3775-0"
x1="145.67661"
y1="39.017067"
x2="145.67661"
y2="204.43738"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.0057861,0,0,1.0057861,-8.6127751,811.84018)" />
<linearGradient
id="linearGradient3769-5">
<stop
style="stop-color:#00ccff;stop-opacity:1;"
offset="0"
id="stop3771-1" />
<stop
id="stop3784-9"
offset="0.5"
style="stop-color:#db1cd8;stop-opacity:0.29374999;" />
<stop
style="stop-color:#00ccff;stop-opacity:0;"
offset="1"
id="stop3773-8" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3814-6"
id="linearGradient3820-1"
x1="140"
y1="922.36218"
x2="140"
y2="962.36218"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.92431917,0,0,0.92431917,33.590696,71.811258)" />
<linearGradient
id="linearGradient3814-6">
<stop
style="stop-color:#ff941f;stop-opacity:1;"
offset="0"
id="stop3816-1" />
<stop
style="stop-color:#ff6447;stop-opacity:0;"
offset="1"
id="stop3818-9" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3769-2"
id="linearGradient3775-2"
x1="145.67661"
y1="39.017067"
x2="145.67661"
y2="204.43738"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.87570243,0,0,0.87570243,-361.65303,924.58758)" />
<linearGradient
id="linearGradient3769-2">
<stop
style="stop-color:#00ccff;stop-opacity:1;"
offset="0"
id="stop3771-7" />
<stop
id="stop3784-4"
offset="0.5"
style="stop-color:#db1cd8;stop-opacity:0.29374999;" />
<stop
style="stop-color:#00ccff;stop-opacity:0;"
offset="1"
id="stop3773-6" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3814-3"
id="linearGradient3820-5"
x1="140"
y1="922.36218"
x2="140"
y2="962.36218"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.80477205,0,0,0.80477205,-324.90796,280.27054)" />
<linearGradient
id="linearGradient3814-3">
<stop
style="stop-color:#008d9c;stop-opacity:1;"
offset="0"
id="stop3816-3" />
<stop
style="stop-color:#008d9c;stop-opacity:0;"
offset="1"
id="stop3818-5" />
</linearGradient>
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3769-5-6"
id="radialGradient3858-6"
cx="154.9223"
cy="930.70764"
fx="154.9223"
fy="930.70764"
r="110.12753"
gradientTransform="matrix(-1.9976843,2.4215517,-2.2797722,-2.3221033,2557.9371,2642.3697)"
gradientUnits="userSpaceOnUse" />
<linearGradient
id="linearGradient3769-5-6">
<stop
style="stop-color:#00ccff;stop-opacity:1;"
offset="0"
id="stop3771-1-3" />
<stop
id="stop3784-9-7"
offset="0.5"
style="stop-color:#db1cd8;stop-opacity:0.29374999;" />
<stop
style="stop-color:#00ccff;stop-opacity:0;"
offset="1"
id="stop3773-8-2" />
</linearGradient>
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3769-5-5"
id="radialGradient3858-4"
cx="154.9223"
cy="930.70764"
fx="154.9223"
fy="930.70764"
r="110.12753"
gradientTransform="matrix(-1.9597692,2.4215517,-2.2365032,-2.3221033,2514.0386,2642.3697)"
gradientUnits="userSpaceOnUse" />
<linearGradient
id="linearGradient3769-5-5">
<stop
style="stop-color:#00ccff;stop-opacity:1;"
offset="0"
id="stop3771-1-4" />
<stop
id="stop3784-9-8"
offset="0.5"
style="stop-color:#db1cd8;stop-opacity:0.29374999;" />
<stop
style="stop-color:#00ccff;stop-opacity:0;"
offset="1"
id="stop3773-8-7" />
</linearGradient>
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3838-9"
id="radialGradient3836-9"
cx="133.65482"
cy="98.044632"
fx="133.65482"
fy="98.044632"
r="110.17627"
gradientTransform="matrix(-2.536372,2.8181903,-2.7229092,-2.4506185,735.96498,-60.394722)"
gradientUnits="userSpaceOnUse" />
<linearGradient
id="linearGradient3838-9">
<stop
style="stop-color:#00ccff;stop-opacity:1;"
offset="0"
id="stop3840-7" />
<stop
id="stop3842-4"
offset="0.5928458"
style="stop-color:#db1cd8;stop-opacity:0.29374999;" />
<stop
style="stop-color:#00ccff;stop-opacity:0;"
offset="1"
id="stop3844-9" />
</linearGradient>
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3916"
id="radialGradient3922"
cx="-145.65875"
cy="80.361481"
fx="-145.65875"
fy="80.361481"
r="24.046875"
gradientTransform="matrix(1,0,0,1.1708902,0,-13.732987)"
gradientUnits="userSpaceOnUse" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3838-8"
id="radialGradient3836-1"
cx="133.65482"
cy="98.044632"
fx="133.65482"
fy="98.044632"
r="110.17627"
gradientTransform="matrix(-2.536372,2.8181903,-2.7229092,-2.4506185,735.96498,-60.394722)"
gradientUnits="userSpaceOnUse" />
<linearGradient
id="linearGradient3838-8">
<stop
style="stop-color:#00ccff;stop-opacity:1;"
offset="0"
id="stop3840-8" />
<stop
id="stop3842-9"
offset="0.5928458"
style="stop-color:#db1cd8;stop-opacity:0.29374999;" />
<stop
style="stop-color:#00ccff;stop-opacity:0;"
offset="1"
id="stop3844-3" />
</linearGradient>
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3896-9"
id="radialGradient3902-4"
cx="68.047195"
cy="9.7423315"
fx="68.047195"
fy="9.7423315"
r="128"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.9375,3.046875,-1.28745,0.39613841,38.748519,-115.19061)" />
<linearGradient
id="linearGradient3896-9">
<stop
style="stop-color:#cd602b;stop-opacity:1;"
offset="0"
id="stop3898-5" />
<stop
style="stop-color:#cd2b2b;stop-opacity:1;"
offset="1"
id="stop3900-5" />
</linearGradient>
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3896-6"
id="radialGradient3902-1"
cx="68.047195"
cy="9.7423315"
fx="68.047195"
fy="9.7423315"
r="128"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.9375,3.046875,-1.28745,0.39613841,38.748519,-1167.5528)" />
<linearGradient
id="linearGradient3896-6">
<stop
style="stop-color:#cd602b;stop-opacity:1;"
offset="0"
id="stop3898-7" />
<stop
style="stop-color:#cd2b2b;stop-opacity:1;"
offset="1"
id="stop3900-0" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3978-1"
id="linearGradient3984-5"
x1="-20"
y1="712.36218"
x2="150"
y2="932.36218"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(0.18296994,0.08188383)" />
<linearGradient
inkscape:collect="always"
id="linearGradient3978-1">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop3980-3" />
<stop
style="stop-color:#ffffff;stop-opacity:0;"
offset="1"
id="stop3982-0" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3939"
id="linearGradient3945"
x1="146"
y1="968.36218"
x2="145"
y2="967.36218"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-296.98485,-295.9747)" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3994-1"
id="linearGradient4000-8"
x1="-61.213001"
y1="-85.49823"
x2="178.787"
y2="324.50177"
gradientUnits="userSpaceOnUse" />
<linearGradient
id="linearGradient3994-1">
<stop
style="stop-color:#00d1c9;stop-opacity:1;"
offset="0"
id="stop3996-8" />
<stop
style="stop-color:#3e6aad;stop-opacity:1;"
offset="1"
id="stop3998-1" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3994-0"
id="linearGradient4000-1"
x1="-61.213001"
y1="-85.49823"
x2="178.787"
y2="324.50177"
gradientUnits="userSpaceOnUse" />
<linearGradient
id="linearGradient3994-0">
<stop
style="stop-color:#00d1c9;stop-opacity:1;"
offset="0"
id="stop3996-9" />
<stop
style="stop-color:#3e6aad;stop-opacity:1;"
offset="1"
id="stop3998-8" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3994-3"
id="linearGradient4000-9"
x1="-61.213001"
y1="-85.49823"
x2="178.787"
y2="324.50177"
gradientUnits="userSpaceOnUse" />
<linearGradient
id="linearGradient3994-3">
<stop
style="stop-color:#00d1c9;stop-opacity:1;"
offset="0"
id="stop3996-88" />
<stop
style="stop-color:#3e6aad;stop-opacity:1;"
offset="1"
id="stop3998-2" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4276"
id="linearGradient4282"
x1="-110"
y1="632.36218"
x2="110"
y2="922.36218"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(415.17269,-239.40615)" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4303"
id="linearGradient4309"
x1="-5"
y1="622.36218"
x2="230"
y2="982.36218"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(415.17269,-239.40615)" />
<linearGradient
gradientTransform="matrix(1.838244,0,0,1.760204,-248.41104,-733.67334)"
inkscape:collect="always"
xlink:href="#linearGradient4303-8"
id="linearGradient4309-4"
x1="-5"
y1="622.36218"
x2="209.46437"
y2="986.59003"
gradientUnits="userSpaceOnUse" />
<linearGradient
inkscape:collect="always"
id="linearGradient4303-8">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop4305-2" />
<stop
style="stop-color:#ffffff;stop-opacity:0;"
offset="1"
id="stop4307-0" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4123"
id="linearGradient4129"
x1="111.1097"
y1="212.86099"
x2="111.1097"
y2="297.41571"
gradientUnits="userSpaceOnUse" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4123"
id="linearGradient4131"
gradientUnits="userSpaceOnUse"
x1="111.1097"
y1="212.86099"
x2="111.1097"
y2="297.41571" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4123"
id="linearGradient4133"
gradientUnits="userSpaceOnUse"
x1="111.1097"
y1="212.86099"
x2="111.1097"
y2="297.41571" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4123"
id="linearGradient4135"
gradientUnits="userSpaceOnUse"
x1="111.1097"
y1="212.86099"
x2="111.1097"
y2="297.41571" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4123"
id="linearGradient4137"
gradientUnits="userSpaceOnUse"
x1="111.1097"
y1="212.86099"
x2="111.1097"
y2="297.41571" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4123"
id="linearGradient4139"
gradientUnits="userSpaceOnUse"
x1="111.1097"
y1="212.86099"
x2="111.1097"
y2="297.41571" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4123"
id="linearGradient4141"
gradientUnits="userSpaceOnUse"
x1="111.1097"
y1="212.86099"
x2="111.1097"
y2="297.41571" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4123"
id="linearGradient4143"
gradientUnits="userSpaceOnUse"
x1="111.1097"
y1="212.86099"
x2="111.1097"
y2="297.41571" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4123"
id="linearGradient4145"
gradientUnits="userSpaceOnUse"
x1="111.1097"
y1="212.86099"
x2="111.1097"
y2="297.41571" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4123"
id="linearGradient4147"
gradientUnits="userSpaceOnUse"
x1="111.1097"
y1="212.86099"
x2="111.1097"
y2="297.41571" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4123"
id="linearGradient4149"
gradientUnits="userSpaceOnUse"
x1="111.1097"
y1="212.86099"
x2="111.1097"
y2="297.41571" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3994-6"
id="linearGradient4000-7"
x1="-61.213001"
y1="-85.49823"
x2="238.787"
y2="424.50177"
gradientUnits="userSpaceOnUse" />
<linearGradient
id="linearGradient3994-6">
<stop
style="stop-color:#00d1c9;stop-opacity:1;"
offset="0"
id="stop3996-6" />
<stop
style="stop-color:#0081e3;stop-opacity:1;"
offset="1"
id="stop3998-0" />
</linearGradient>
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#9d4f2f"
borderopacity="1"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:zoom="2"
inkscape:cx="118.88075"
inkscape:cy="143.06055"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="true"
showguides="true"
inkscape:guide-bbox="true"
inkscape:window-width="1920"
inkscape:window-height="1017"
inkscape:window-x="-4"
inkscape:window-y="0"
inkscape:window-maximized="1"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
showborder="true">
<inkscape:grid
type="xygrid"
id="grid2985"
empspacing="5"
visible="true"
enabled="false"
snapvisiblegridlinesonly="true"
originx="8.7869998px"
originy="9.9302358px" />
<sodipodi:guide
orientation="1,0"
position="8.7869998,239.93024"
id="guide2989" />
<sodipodi:guide
orientation="1,0"
position="263.787,-0.0697642"
id="guide2993" />
<sodipodi:guide
orientation="0,1"
position="261.787,9.9302358"
id="guide2995" />
<sodipodi:guide
orientation="1,0"
position="263.787,296.98485"
id="guide4514" />
<sodipodi:guide
orientation="0,1"
position="68.787,189.93024"
id="guide3939" />
<sodipodi:guide
orientation="0,1"
position="-434.64286,137.14286"
id="guide3941" />
<sodipodi:guide
orientation="1,0"
position="128.787,19.930236"
id="guide4113" />
</sodipodi:namedview>
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
<dc:creator>
<cc:Agent>
<dc:title>Malcer</dc:title>
</cc:Agent>
</dc:creator>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Capa 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(8.7869998,-787.86042)">
<path
style="fill:#f5f5f5;fill-opacity:1;stroke:none"
d="m 64.090487,801.86214 128.677053,0 c 5.09194,0 9.19121,3.92526 9.19121,8.80103 l 0,44.00509 -11.02945,0 0,-40.48469 -125.000587,0 0,214.74493 125.000587,0 0,-174.26024 11.02945,0 0,184.82144 c 0,4.8757 -4.09927,8.801 -9.19121,8.801 l -128.677053,0 c -5.091955,0 -9.191239,-3.9253 -9.191239,-8.801 l 0,-228.82653 c 0,-4.87577 4.099284,-8.80103 9.191239,-8.80103 z"
id="rect3099-1"
inkscape:connector-curvature="0"
sodipodi:nodetypes="sssccccccccssssss" />
<rect
style="fill:#2d2d2d;fill-opacity:1;stroke:none"
id="rect3907-1"
width="125.0006"
height="214.7449"
x="65.928711"
y="814.18335"
rx="0"
ry="0" />
<rect
style="fill:#1a1a1a;fill-opacity:1;stroke:none"
id="rect3946-5"
width="35.204079"
height="1.760206"
x="110.82697"
y="807.14258"
ry="0.88010299" />
<g
transform="matrix(0.99703783,0,0,0.99703783,60.422321,868.29896)"
id="g3764-6-1"
style="fill:#f2f2f2;fill-opacity:1">
<g
id="g15-3-2"
style="fill:#f2f2f2;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="translate(59.2336,11.2066)" />
<g
id="g30-7-2"
style="fill:#f2f2f2;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="translate(19.32,28.4763)">
<path
inkscape:connector-curvature="0"
id="path32-1-7"
style="fill:#f2f2f2;fill-opacity:1;stroke:none"
d="m 18.0991,0.0201 c -0.1947,0.0201 -0.4053,0.0738 -0.5517,0.2207 0,0 -6.7689,6.7688 -6.7689,6.7688 -0.2843,0.2851 -0.319,0.7216 -0.1103,1.0667 0,0 7.9092,13.0594 7.9092,13.0594 -1.4044,2.3608 -2.5448,4.8933 -3.3477,7.5781 0,0 -14.530751,3.0165 -14.530751,3.0165 C 0.29459,31.8139 0,32.1988 0,32.6132 c 0,0 0,9.5645 0,9.5645 0,0.4044 0.307536,0.7507 0.698949,0.8461 0,0 14.089351,3.4212 14.089351,3.4212 0.7534,3.1084 1.9174,6.0828 3.4579,8.792 0,0 -8.1666,12.4339 -8.1666,12.4339 -0.2278,0.3473 -0.18337,0.8106 0.1103,1.1038 0,0 6.7689,6.7687 6.7689,6.7687 0.2851,0.2842 0.7224,0.3199 1.0667,0.1103 0,0 12.8018,-7.7619 12.8018,-7.7619 2.5109,1.4495 5.2294,2.6109 8.0931,3.3844 0,0 2.9798,14.3471 2.9798,14.3471 0.0835,0.405 0.432,0.699 0.846,0.699 0,0 9.5646,0 9.5646,0 0.4014,0 0.7508,-0.27 0.846,-0.663 0,0 3.5316,-14.42 3.5316,-14.42 2.9535,-0.7964 5.7382,-1.9786 8.3139,-3.4947 0,0 12.6178,8.277 12.6178,8.277 0.3479,0.2266 0.8087,0.1828 1.1036,-0.1103 0,0 6.7321,-6.7689 6.7321,-6.7689 0.2859,-0.2853 0.3565,-0.7224 0.1471,-1.0668 0,0 -4.5984,-7.578 -4.5984,-7.578 0,0 -1.5083,0.4783 -1.5083,0.4783 -0.2178,0.0671 -0.4591,-0.0314 -0.5886,-0.2209 0,0 -2.9047,-4.2728 -6.6952,-9.822 -4.5332,8.8706 -13.7783,14.9354 -24.4264,14.9354 -15.1308,0 -27.4061,-12.2753 -27.4061,-27.4061 0,-11.1298 6.6555,-20.6892 16.1862,-24.9782 0,0 0,-7.0631 0,-7.0631 -1.7346,0.6071 -3.4103,1.3396 -5.003,2.2072 -0.003,-0.0018 0.0029,-0.0351 0,-0.0367 0,0 -12.9121,-8.4611 -12.9121,-8.4611 C 18.4773,0.0169 18.2938,0 18.0991,0.0201 c 0,0 0,0 0,0 z m 39.0307,-17.2898 c 0,0 -17.2162,1.65541 -17.2162,1.65541 0,0 0,70.92489 0,70.92489 0,0 17.0323,-2.575 17.0323,-2.575 0,0 0,-30.2388 0,-30.2388 0,0 22.9183,33.5495 22.9183,33.5495 0,0 17.9518,-5.7019 17.9518,-5.7019 0,0 -23.47,-32.2621 -23.47,-32.2621 0,0 23.654,-30.42257 23.654,-30.42257 0,0 -18.3198,-4.193694 -18.3198,-4.193694 0,0 -22.7343,30.422764 -22.7343,30.422764 0,0 0.1839,-31.1585 0.1839,-31.1585 0,0 0,0 0,0 z" />
</g>
</g>
<path
style="opacity:0.5;fill:url(#linearGradient4309-4);fill-opacity:1;stroke:none"
d="m 65.928713,814.18357 90.073947,0 15.61309,-1.06296 -88.937821,217.56809 -18.58746,0 z"
id="rect3907-9-4"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccccc" />
<path
style="opacity:0.4;fill:#ffffff;fill-opacity:1;stroke:none"
d="m 72.876953,14.001953 c -5.091955,0 -9.191406,3.925011 -9.191406,8.800781 l 0,2 c 0,-4.87577 4.099451,-8.800781 9.191406,-8.800781 l 128.677737,0 c 5.09194,0 9.1914,3.925011 9.1914,8.800781 l 0,-2 c 0,-4.87577 -4.09946,-8.800781 -9.1914,-8.800781 z"
transform="translate(-8.7869998,787.86042)"
id="path4295"
inkscape:connector-curvature="0"
sodipodi:nodetypes="sscsscsss" />
<path
style="fill:#000000;fill-opacity:1;stroke:none;opacity:0.1"
d="m 63.685547,249.62891 0,2 c 0,4.8757 4.099451,8.80078 9.191406,8.80078 l 128.677737,0 c 5.09194,0 9.1914,-3.92508 9.1914,-8.80078 l 0,-2 c 0,4.8757 -4.09946,8.80078 -9.1914,8.80078 l -128.677737,0 c -5.091955,0 -9.191406,-3.92508 -9.191406,-8.80078 z"
id="path4300"
inkscape:connector-curvature="0"
sodipodi:nodetypes="csssscssc"
transform="translate(-8.7869998,787.86042)" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 28 KiB

13
kdeconnect-android.iml Normal file
View File

@@ -0,0 +1,13 @@
<?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="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.gradle" />
<excludeFolder url="file://$MODULE_DIR$/build" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

30
proguard-rules.pro vendored
View File

@@ -1,30 +0,0 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in {SDKHOME}/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Allow obfuscation of android.support.v7.internal.view.menu.**
# to avoid problem on Samsung 4.2.2 devices with appcompat v21
# see https://code.google.com/p/android/issues/detail?id=78377
-keep class !android.support.v7.internal.view.menu.**,** {*;}
-dontwarn org.apache.sshd.**
-dontwarn org.apache.mina.**
-dontwarn org.bouncycastle.**
-dontwarn org.slf4j.**
-keepattributes SourceFile,LineNumberTable

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="#FFFFFF" android:state_checked="true" />
<item android:color="#000000" android:state_checked="false" />
</selector>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 469 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 161 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 532 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 351 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 557 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 590 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 627 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 560 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 444 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 231 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 686 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 823 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 477 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 441 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 313 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 645 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 431 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 750 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 365 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 365 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 377 B

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