2
0
mirror of https://github.com/KDE/kdeconnect-android synced 2025-08-22 01:51:47 +00:00

Catch up with the KDE client

Implemented symmetric pairing
Implemented public key exchange
Implemented encryption
Implemented NetworkPackage protocol version 3
New GUI, less dependant on Android 3.0+ features
Plugins can now place a button in the user interface
Fixed not clickable plugins preferences page
Some refactoring
This commit is contained in:
Albert Vaca 2013-09-03 17:58:59 +02:00
parent 6b978f334b
commit d5bc6ebcfa
43 changed files with 1872 additions and 684 deletions

View File

@ -10,6 +10,7 @@
<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>

View File

@ -24,7 +24,7 @@
<activity
android:theme="@style/Theme.AppCompat"
android:name="org.kde.connect.MainActivity"
android:name="org.kde.connect.UserInterface.MainActivity"
android:label="KDE Connect" >
<intent-filter>
@ -36,11 +36,12 @@
<activity
android:theme="@style/Theme.AppCompat"
android:name="org.kde.connect.DeviceActivity"
android:name="org.kde.connect.UserInterface.DeviceActivity"
android:label="Device"
android:parentActivityName=".MainActivity">
android:parentActivityName="org.kde.connect.UserInterface.MainActivity"
>
<meta-data android:name="android.support.PARENT_ACTIVITY"
android:value=".MainActivity" />
android:value="org.kde.connect.UserInterface.MainActivity" />
</activity>
@ -48,17 +49,30 @@
<activity
android:theme="@style/Theme.AppCompat"
android:name="org.kde.connect.MprisActivity"
android:name="org.kde.connect.UserInterface.PairActivity"
android:label="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.connect.Plugins.MprisPlugin.MprisActivity"
android:label="MPRIS controls"
android:parentActivityName=".DeviceActivity">
>
<meta-data android:name="android.support.PARENT_ACTIVITY"
android:value=".DeviceActivity" />
</activity>
<activity
android:name="org.kde.connect.SettingsActivity"
android:name="org.kde.connect.UserInterface.SettingsActivity"
android:label="KDE Connect Settings"
android:parentActivityName=".DeviceActivity">
>
<meta-data android:name="android.support.PARENT_ACTIVITY"
android:value=".DeviceActivity" />
</activity>
@ -68,7 +82,7 @@
android:name="org.kde.connect.BackgroundService">
</service>
<service android:name="org.kde.connect.NotificationReceiver"
<service android:name="org.kde.connect.Plugins.NotificationsPlugin.NotificationReceiver"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
<intent-filter>
<action android:name="android.service.notification.NotificationListenerService" />

View File

@ -8,17 +8,23 @@ import android.content.SharedPreferences;
import android.os.Binder;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.util.Base64;
import android.util.Log;
import org.kde.connect.ComputerLinks.BaseComputerLink;
import org.kde.connect.LinkProviders.BaseLinkProvider;
import org.kde.connect.LinkProviders.BroadcastTcpLinkProvider;
import java.math.BigInteger;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
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.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@ -29,12 +35,33 @@ public class BackgroundService extends Service {
private HashMap<String, Device> devices = new HashMap<String, Device>();
Device.PairingCallback devicePairingCallback = new Device.PairingCallback() {
@Override
public void incomingRequest() {
if (deviceListChangedCallback != null) deviceListChangedCallback.onDeviceListChanged();
}
@Override
public void pairingSuccessful() {
if (deviceListChangedCallback != null) deviceListChangedCallback.onDeviceListChanged();
}
@Override
public void pairingFailed(String error) {
if (deviceListChangedCallback != null) deviceListChangedCallback.onDeviceListChanged();
}
@Override
public void unpaired() {
if (deviceListChangedCallback != null) deviceListChangedCallback.onDeviceListChanged();
}
};
private void loadRememberedDevicesFromSettings() {
SharedPreferences preferences = getSharedPreferences("trusted_devices", Context.MODE_PRIVATE);
Set<String> trustedDevices = preferences.getAll().keySet();
for(String deviceId : trustedDevices) {
if (preferences.getBoolean(deviceId, false)) {
devices.put(deviceId,new Device(getBaseContext(), deviceId));
Device device = new Device(getBaseContext(), deviceId);
devices.put(deviceId,device);
device.addPairingCallback(devicePairingCallback);
}
}
}
@ -69,14 +96,16 @@ public class BackgroundService extends Service {
if (device != null) {
Log.e("BackgroundService", "addLink, known device: "+deviceId);
if (!device.hasName()) device.setName(identityPackage.getString("deviceName"));
device.addLink(link);
} else {
Log.e("BackgroundService", "addLink,unknown device: "+deviceId);
String name = identityPackage.getString("deviceName");
device = new Device(getBaseContext(), deviceId, name, link);
devices.put(deviceId, device);
device.addPairingCallback(devicePairingCallback);
}
if (deviceListChangedCallback != null) deviceListChangedCallback.onDeviceListChanged();
}
@Override
@ -85,13 +114,14 @@ public class BackgroundService extends Service {
Log.e("onConnectionLost","removeLink, deviceId: "+link.getDeviceId());
if (d != null) {
d.removeLink(link);
if (!d.isReachable() && !d.isTrusted()) {
if (!d.isReachable() && !d.isPaired()) {
devices.remove(link.getDeviceId());
d.removePairingCallback(devicePairingCallback);
}
} else {
Log.e("onConnectionLost","Removing connection to unknown device, this should not happen");
}
if (deviceListChangedCallback != null) deviceListChangedCallback.onDeviceListChanged();
}
};
@ -133,6 +163,15 @@ public class BackgroundService extends Service {
}
}
public interface DeviceListChangedCallback {
void onDeviceListChanged();
}
private DeviceListChangedCallback deviceListChangedCallback = null;
public void setDeviceListChangedCallback(DeviceListChangedCallback callback) {
this.deviceListChangedCallback = callback;
}
//This will called only once, even if we launch the service intent several times
@Override
public void onCreate() {
@ -144,6 +183,7 @@ public class BackgroundService extends Service {
Log.i("BackgroundService","Service not started yet, initializing...");
initializeRsaKeys();
loadRememberedDevicesFromSettings();
registerLinkProviders();
@ -153,6 +193,61 @@ public class BackgroundService extends Service {
}
private void initializeRsaKeys() {
SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(this);
if (!settings.contains("publicKey") || !settings.contains("privateKey")) {
KeyPair keyPair;
try {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
keyGen.initialize(2048);
keyPair = keyGen.genKeyPair();
} catch(Exception e) {
e.printStackTrace();
Log.e("initializeRsaKeys","Exception");
return;
}
byte[] publicKey = keyPair.getPublic().getEncoded();
byte[] privateKey = keyPair.getPrivate().getEncoded();
SharedPreferences.Editor edit = settings.edit();
edit.putString("publicKey",Base64.encodeToString(publicKey, 0).trim()+"\n");
edit.putString("privateKey",Base64.encodeToString(privateKey, 0));
edit.commit();
}
/*
// Encryption and decryption test
//================================
try {
NetworkPackage np = NetworkPackage.createIdentityPackage(this);
SharedPreferences globalSettings = PreferenceManager.getDefaultSharedPreferences(this);
byte[] publicKeyBytes = Base64.decode(globalSettings.getString("publicKey",""), 0);
PublicKey publicKey = KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(publicKeyBytes));
np.encrypt(publicKey);
byte[] privateKeyBytes = Base64.decode(globalSettings.getString("privateKey",""), 0);
PrivateKey privateKey = KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(privateKeyBytes));
NetworkPackage decrypted = np.decrypt(privateKey);
Log.e("ENCRYPTION AND DECRYPTION TEST", decrypted.serialize());
} catch (Exception e) {
e.printStackTrace();
Log.e("ENCRYPTION AND DECRYPTION TEST","Exception: "+e);
}
*/
}
@Override
public void onDestroy() {
Log.i("BackgroundService", "Destroying");

View File

@ -0,0 +1,38 @@
package org.kde.connect.ComputerLinks;
import android.util.Log;
import org.apache.mina.core.session.IoSession;
import org.kde.connect.LinkProviders.BaseLinkProvider;
import org.kde.connect.NetworkPackage;
public class LanComputerLink extends BaseComputerLink {
private IoSession session = null;
public void disconnect() {
Log.e("NioSessionComputerLink","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) {
Log.e("TcpComputerLink", "sendPackage");
if (session == null) {
Log.e("TcpComputerLink","not yet connected");
return false;
} else {
session.write(np.serialize());
return true;
}
}
public void injectNetworkPackage(NetworkPackage np) {
packageReceived(np);
}
}

View File

@ -78,7 +78,7 @@ public class TcpComputerLink extends BaseComputerLink {
session = future.getSession();
if (callback != null) callback.dispatchMessage(new Message());
}
}).run();
}).start();
}

View File

@ -1,34 +1,70 @@
package org.kde.connect;
import android.R;
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.os.AsyncTask;
import android.os.Build;
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.connect.ComputerLinks.BaseComputerLink;
import org.kde.connect.Plugins.Plugin;
import org.kde.connect.Plugins.PluginFactory;
import org.kde.connect.UserInterface.PairActivity;
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.HashSet;
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>();
private Context context;
private String deviceId;
private String name;
private boolean trusted;
SharedPreferences settings;
//Remembered trusted device, we need to wait for a incoming devicelink to communicate
@ -39,8 +75,16 @@ public class Device implements BaseComputerLink.PackageReceiver {
this.context = context;
this.deviceId = deviceId;
this.name = settings.getString("deviceName", null);
this.trusted = true;
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();
}
@ -53,8 +97,9 @@ public class Device implements BaseComputerLink.PackageReceiver {
this.context = context;
this.deviceId = deviceId;
setName(name);
setTrusted(false);
this.name = name;
this.pairStatus = PairStatus.NotPaired;
this.publicKey = null;
addLink(dl);
}
@ -65,50 +110,154 @@ public class Device implements BaseComputerLink.PackageReceiver {
}
public String getName() {
return name != null? name : "unknown device";
}
public void setName(String name) {
this.name = name;
settings.edit().putString("deviceName",name).commit();
return name != null? name : "unknown device"; //TODO: i18n
}
public String getDeviceId() {
return deviceId;
}
public boolean isReachable() {
return !links.isEmpty();
//
// Pairing-related functions
//
public boolean isPaired() {
return pairStatus == PairStatus.Paired;
}
public boolean isTrusted() {
return trusted;
public boolean isPairRequested() {
return pairStatus == PairStatus.Requested;
}
public void setTrusted(boolean b) {
trusted = b;
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() {
if (pairStatus == PairStatus.Paired) {
for (PairingCallback cb : pairingCallback) cb.pairingFailed("Device already paired"); //TODO: i18n
return;
}
if (pairStatus == PairStatus.Requested) {
for (PairingCallback cb : pairingCallback) cb.pairingFailed("Pairing already requested"); //TODO: i18n
return;
}
if (!isReachable()) {
for (PairingCallback cb : pairingCallback) cb.pairingFailed("Device not reachable"); //TODO: i18n
return;
}
//Send our own public key
NetworkPackage np = NetworkPackage.createPublicKeyPackage(context);
boolean success = sendPackage(np);
if (!success) {
for (PairingCallback cb : pairingCallback) cb.pairingFailed("Could not send package");
return;
}
pairingTimer = new Timer();
pairingTimer.schedule(new TimerTask() {
@Override
public void run() {
for (PairingCallback cb : pairingCallback) cb.pairingFailed("Timed out"); //TODO: i18n
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();
boolean wasTrusted = preferences.getBoolean(deviceId, false);
NetworkPackage np = new NetworkPackage(NetworkPackage.PACKAGE_TYPE_PAIR);
np.set("pair", false);
sendPackage(np);
if (trusted && !wasTrusted) {
preferences.edit().putBoolean(deviceId, true).commit();
} else if(!trusted && wasTrusted) {
preferences.edit().remove(deviceId).commit();
}
for (PairingCallback cb : pairingCallback) cb.unpaired();
reloadPluginsFromSettings();
}
public void acceptPairing() {
Log.e("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.e("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("Canceled by the user"); //TODO: i18n
}
//
// Computer link-related functions
// ComputerLink-related functions
//
public boolean isReachable() {
return !links.isEmpty();
}
public void addLink(BaseComputerLink link) {
links.add(link);
@ -140,15 +289,154 @@ public class Device implements BaseComputerLink.PackageReceiver {
@Override
public void onPackageReceived(NetworkPackage np) {
for (Plugin plugin : plugins.values()) {
//Log.e("onPackageReceived",plugin.toString());
plugin.onPackageReceived(np);
if (np.getType().equals(NetworkPackage.PACKAGE_TYPE_PAIR)) {
Log.e("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("Canceled by other peer"); //TODO: i18n
}
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);
Log.e("asdasd","key bytes: " + publicKeyBytes);
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("Incorrect key received"); //TODO: i18n
return;
}
if (pairStatus == PairStatus.Requested) { //We started pairing
Log.e("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.e("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);
Notification noti = new NotificationCompat.Builder(context)
.setContentTitle("Pairing request from" + getName()) //TODO: i18n
.setContentText("Tap to answer") //TODO: i18n
.setContentIntent(pendingIntent)
.setTicker("Pair requested") //TODO: i18n
.setSmallIcon(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.e("Pairing","Unpair request");
if (pairStatus == PairStatus.Requested) {
pairingTimer.cancel();
for (PairingCallback cb : pairingCallback) cb.pairingFailed("Canceled by other peer"); //TODO: i18n
} 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) {
Log.e("Device", "sendPackage "+np.getType()+". "+links.size()+" links available");
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) {
@ -160,12 +448,14 @@ public class Device implements BaseComputerLink.PackageReceiver {
return null;
}
}
Log.e("sendPackage","Error: Package could not be sent");
Log.e("sendPackage","Error: Package could not be sent ("+links.size()+" links available)");
return null;
}
}.execute();
return true; //FIXME: Detect when unable to send a package and try again somehow
//TODO: Detect when unable to send a package and try again somehow
return !links.isEmpty();
}
@ -216,6 +506,11 @@ public class Device implements BaseComputerLink.PackageReceiver {
failedPlugins.remove(name);
plugins.put(name, plugin);
for (PluginsChangedListener listener : pluginsChangedListeners) {
listener.onPluginsChanged(Device.this);
}
}
});
@ -243,16 +538,18 @@ public class Device implements BaseComputerLink.PackageReceiver {
//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);
for (PluginsChangedListener listener : pluginsChangedListeners) {
listener.onPluginsChanged(this);
}
}
public boolean isPluginEnabled(String pluginName) {
@ -270,7 +567,7 @@ public class Device implements BaseComputerLink.PackageReceiver {
for(String pluginName : availablePlugins) {
boolean enabled = false;
if (isTrusted() && isReachable()) {
if (isPaired() && isReachable()) {
enabled = isPluginEnabled(pluginName);
}
//Log.e("reloadPluginsFromSettings",pluginName+"->"+enabled);
@ -286,15 +583,19 @@ public class Device implements BaseComputerLink.PackageReceiver {
}
}
public HashMap<String,Plugin> getLoadedPlugins() {
return plugins;
}
public HashMap<String,Plugin> getFailedPlugins() {
return failedPlugins;
}
interface PluginsChangedListener {
public interface PluginsChangedListener {
void onPluginsChanged(Device device);
}
ArrayList<PluginsChangedListener> pluginsChangedListeners = new ArrayList<PluginsChangedListener>();
private ArrayList<PluginsChangedListener> pluginsChangedListeners = new ArrayList<PluginsChangedListener>();
public void addPluginsChangedListener(PluginsChangedListener listener) {
pluginsChangedListeners.add(listener);

View File

@ -1,174 +0,0 @@
package org.kde.connect;
import android.app.Activity;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.CheckBoxPreference;
import android.preference.Preference;
import android.preference.PreferenceManager;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.CompoundButton;
import android.widget.ListView;
import android.widget.Switch;
import org.kde.connect.Plugins.PingPlugin;
import org.kde.connect.Plugins.Plugin;
import org.kde.kdeconnect.R;
import java.util.HashMap;
import java.util.Map;
public class DeviceActivity extends Activity {
private String deviceId;
private Device.PluginsChangedListener pluginsChangedListener = new Device.PluginsChangedListener() {
@Override
public void onPluginsChanged(final Device device) {
runOnUiThread(new Runnable() {
@Override
public void run() {
Log.e("MainActivity", "updateComputerList");
final HashMap<String, Plugin> plugins = device.getFailedPlugins();
final String[] ids = plugins.keySet().toArray(new String[plugins.size()]);
String[] names = new String[plugins.size()];
for(int i = 0; i < ids.length; i++) {
Plugin p = plugins.get(ids[i]);
names[i] = p.getDisplayName();
}
ListView list = (ListView)findViewById(R.id.listView1);
list.setAdapter(new ArrayAdapter<String>(DeviceActivity.this, android.R.layout.simple_list_item_1, names));
list.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) {
Plugin p = plugins.get(ids[position]);
p.getErrorDialog(DeviceActivity.this).show();
}
});
findViewById(R.id.textView).setVisibility(plugins.size() > 0? View.VISIBLE : View.GONE);
}
});
}
};
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.device, menu);
MenuItem item = menu.findItem(R.id.menu_trusted);
final Switch toggle = (Switch)item.getActionView();
BackgroundService.RunCommand(DeviceActivity.this, new BackgroundService.InstanceCallback() {
@Override
public void onServiceStart(BackgroundService service) {
final Device device = service.getDevice(deviceId);
toggle.setChecked(device.isTrusted());
toggle.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton compoundButton, final boolean b) {
device.setTrusted(b);
}
});
}
});
return true;
}
@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.DISPLAY_SHOW_CUSTOM);
actionBar.setDisplayHomeAsUpEnabled(true);
//actionBar.setIcon()
*/
deviceId = getIntent().getStringExtra("deviceId");
BackgroundService.RunCommand(DeviceActivity.this, new BackgroundService.InstanceCallback() {
@Override
public void onServiceStart(BackgroundService service) {
Device device = service.getDevice(deviceId);
setTitle(device.getName());
device.addPluginsChangedListener(pluginsChangedListener);
pluginsChangedListener.onPluginsChanged(device);
}
});
findViewById(R.id.button1).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
Intent intent = new Intent(DeviceActivity.this, SettingsActivity.class);
intent.putExtra("deviceId", deviceId);
startActivity(intent);
}
});
findViewById(R.id.button2).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
BackgroundService.RunCommand(DeviceActivity.this, new BackgroundService.InstanceCallback() {
@Override
public void onServiceStart(BackgroundService service) {
Device device = service.getDevice(deviceId);
device.sendPackage(new NetworkPackage(NetworkPackage.PACKAGE_TYPE_PING));
}
});
}
});
findViewById(R.id.button3).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
BackgroundService.RunCommand(DeviceActivity.this, new BackgroundService.InstanceCallback() {
@Override
public void onServiceStart(BackgroundService service) {
Intent intent = new Intent(DeviceActivity.this, MprisActivity.class);
intent.putExtra("deviceId", deviceId);
startActivity(intent);
}
});
}
});
}
@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();
}
}

View File

@ -0,0 +1,285 @@
package org.kde.connect.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.connect.ComputerLinks.BaseComputerLink;
import org.kde.connect.ComputerLinks.NioSessionComputerLink;
import org.kde.connect.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, NioSessionComputerLink> visibleComputers = new HashMap<String, NioSessionComputerLink>();
private HashMap<Long, NioSessionComputerLink> nioSessions = new HashMap<Long, NioSessionComputerLink>();
private NioSocketAcceptor tcpAcceptor = null;
private NioDatagramAcceptor udpAcceptor = null;
private final IoHandler tcpHandler = new IoHandlerAdapter() {
@Override
public void sessionClosed(IoSession session) throws Exception {
NioSessionComputerLink brokenLink = nioSessions.remove(session.getId());
if (brokenLink != null) {
connectionLost(brokenLink);
String deviceId = brokenLink.getDeviceId();
if (visibleComputers.get(deviceId) == brokenLink) {
visibleComputers.remove(deviceId);
connectionLost(brokenLink);
}
}
}
@Override
public void messageReceived(IoSession session, Object message) throws Exception {
super.messageReceived(session, message);
//Log.e("BroadcastTcpLinkProvider","Incoming package, address: "+session.getRemoteAddress()).toString());
String theMessage = (String) message;
NetworkPackage np = NetworkPackage.unserialize(theMessage);
NioSessionComputerLink 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;
}
NioSessionComputerLink link = new NioSessionComputerLink(session, np.getString("deviceId"), LanLinkProvider.this);
nioSessions.put(session.getId(),link);
addLink(np, link);
} else {
if (prevLink == null) {
Log.e("BroadcastTcpLinkProvider","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("BroadcastTcpLinkProvider", "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("BroadcastTcpLinkProvider", "Could not unserialize package");
}
if (np != null) {
final NetworkPackage identityPackage = np;
if (!np.getType().equals(NetworkPackage.PACKAGE_TYPE_IDENTITY)) {
Log.e("BroadcastTcpLinkProvider", "1 Expecting an identity package");
return;
} else {
String myId = NetworkPackage.createIdentityPackage(context).getString("deviceId");
if (np.getString("deviceId").equals(myId)) {
return;
}
}
Log.e("BroadcastTcpLinkProvider", "It is an identity package, 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.e("BroadcastTcpLinkProvider", "Connection successful: " + session.isConnected());
NioSessionComputerLink link = new NioSessionComputerLink(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("BroadcastTcpLinkProvider","Exception!!");
e.printStackTrace();
}
}
}
};
private void addLink(NetworkPackage identityPackage, NioSessionComputerLink link) {
String deviceId = identityPackage.getString("deviceId");
Log.e("BroadcastTcpLinkProvider","addLink to "+deviceId);
BaseComputerLink oldLink = visibleComputers.get(deviceId);
visibleComputers.put(deviceId, link);
connectionAccepted(identityPackage, link);
if (oldLink != null) {
Log.e("BroadcastTcpLinkProvider","Removing old connection to same device");
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("BroadcastTcpLinkProvider", "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.e("BroadcastTcpLinkProvider","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("BroadcastTcpLinkProvider","Udp identity package sent");
} catch(Exception e) {
e.printStackTrace();
Log.e("BroadcastTcpLinkProvider","Sending udp identity package failed");
}
return null;
}
}.execute();
}
@Override
public void onNetworkChange() {
Log.e("BroadcastTcpLinkProvider","OnNetworkChange: " + (udpAcceptor != null));
onStop();
onStart();
}
@Override
public void onStop() {
udpAcceptor.unbind();
tcpAcceptor.unbind();
}
@Override
public int getPriority() {
return 1000;
}
@Override
public String getName() {
return "BroadcastTcpLinkProvider";
}
}

View File

@ -1,159 +0,0 @@
package org.kde.connect;
import android.app.Activity;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
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.ArrayAdapter;
import android.widget.ListView;
import org.kde.connect.ComputerLinks.BaseComputerLink;
import org.kde.connect.LinkProviders.BaseLinkProvider;
import org.kde.kdeconnect.R;
import java.util.HashMap;
public class MainActivity extends Activity {
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.main, menu);
return true;
}
private MenuItem menuItem;
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_load:
BackgroundService.RunCommand(MainActivity.this, new BackgroundService.InstanceCallback() {
@Override
public void onServiceStart(BackgroundService service) {
service.onNetworkChange();
}
});
if (Build.VERSION.SDK_INT >= 11) {
menuItem = item;
menuItem.setActionView(R.layout.progressbar);
if (Build.VERSION.SDK_INT >= 14) {
menuItem.expandActionView();
}
TestTask task = new TestTask();
task.execute();
}
break;
default:
break;
}
return true;
}
private class TestTask extends AsyncTask<Void, Void, Void> {
@Override
protected Void doInBackground(Void... params) {
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
}
return null;
}
@Override
protected void onPostExecute(Void result) {
if (Build.VERSION.SDK_INT >= 14)
menuItem.collapseActionView();
menuItem.setActionView(null);
}
};
void updateComputerList() {
Log.e("MainActivity","updateComputerList");
BackgroundService.RunCommand(MainActivity.this, new BackgroundService.InstanceCallback() {
@Override
public void onServiceStart(BackgroundService service) {
HashMap<String, Device> devices = service.getDevices();
final String[] ids = devices.keySet().toArray(new String[devices.size()]);
final String[] names = new String[devices.size()];
for(int i = 0; i < ids.length; i++) {
Device d = devices.get(ids[i]);
names[i] = d.getName() + " " + d.isTrusted() + " " + d.isReachable();
}
runOnUiThread(new Runnable() {
@Override
public void run() {
ListView list = (ListView)findViewById(R.id.listView1);
list.setAdapter(new ArrayAdapter<String>(MainActivity.this, android.R.layout.simple_list_item_1, names));
list.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) {
Intent intent = new Intent(MainActivity.this, DeviceActivity.class);
intent.putExtra("deviceId", ids[position]);
startActivity(intent);
}
});
}
});
}
});
};
BaseLinkProvider.ConnectionReceiver connectionReceiver = new BaseLinkProvider.ConnectionReceiver() {
@Override
public void onConnectionReceived(NetworkPackage identityPackage, BaseComputerLink link) {
updateComputerList();
}
@Override
public void onConnectionLost(BaseComputerLink link) {
updateComputerList();
}
};
@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);
*/
BackgroundService.RunCommand(MainActivity.this, new BackgroundService.InstanceCallback() {
@Override
public void onServiceStart(BackgroundService service) {
service.onNetworkChange();
service.addConnectionListener(connectionReceiver);
}
});
updateComputerList();
}
@Override
protected void onDestroy() {
super.onDestroy();
BackgroundService.RunCommand(MainActivity.this, new BackgroundService.InstanceCallback() {
@Override
public void onServiceStart(BackgroundService service) {
service.removeConnectionListener(connectionReceiver);
}
});
}
}

View File

@ -1,20 +1,32 @@
package org.kde.connect;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.provider.Settings;
import android.util.Base64;
import android.util.Log;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.nio.charset.Charset;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.ArrayList;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
public class NetworkPackage {
private final static int CURRENT_PACKAGE_VERSION = 1;
public final static int ProtocolVersion = 3;
public final static String PACKAGE_TYPE_IDENTITY = "kdeconnect.identity";
public final static String PACKAGE_TYPE_PAIR = "kdeconnect.pair";
public final static String PACKAGE_TYPE_ENCRYPTED = "kdeconnect.encrypted";
public final static String PACKAGE_TYPE_PING = "kdeconnect.ping";
public final static String PACKAGE_TYPE_TELEPHONY = "kdeconnect.telephony";
public final static String PACKAGE_TYPE_BATTERY = "kdeconnect.battery";
@ -25,7 +37,6 @@ public class NetworkPackage {
private long mId;
private String mType;
private JSONObject mBody;
private int mVersion;
private NetworkPackage() {
}
@ -34,17 +45,12 @@ public class NetworkPackage {
mId = System.currentTimeMillis();
mType = type;
mBody = new JSONObject();
mVersion = CURRENT_PACKAGE_VERSION;
}
public String getType() {
return mType;
}
public int getVersion() {
return mVersion;
}
//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); }
@ -96,10 +102,10 @@ public class NetworkPackage {
jo.put("id",mId);
jo.put("type",mType);
jo.put("body",mBody);
jo.put("version",mVersion);
} catch(Exception e) {
}
String json = jo.toString()+"\n";
//QJSon does not escape slashes, but Java JSONObject does. Converting to QJson format.
String json = jo.toString().replace("\\/","/")+"\n";
Log.e("NetworkPackage.serialize",json);
return json;
}
@ -112,16 +118,68 @@ public class NetworkPackage {
np.mId = jo.getLong("id");
np.mType = jo.getString("type");
np.mBody = jo.getJSONObject("body");
np.mVersion = jo.getInt("version");
} catch (Exception e) {
return null;
}
if (np.mVersion > CURRENT_PACKAGE_VERSION) {
Log.e("NetworkPackage.unserialize","Version "+np.mVersion+" greater than supported version "+CURRENT_PACKAGE_VERSION);
}
return np;
}
public void encrypt(PublicKey publicKey) throws Exception {
String serialized = serialize();
int chunkSize = 128;
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1PADDING");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
JSONArray chunks = new JSONArray();
while (serialized.length() > 0) {
if (serialized.length() < chunkSize) {
chunkSize = serialized.length();
}
String chunk = serialized.substring(0, chunkSize);
serialized = serialized.substring(chunkSize);
byte[] chunkBytes = chunk.getBytes(Charset.defaultCharset());
byte[] encryptedChunk;
encryptedChunk = cipher.doFinal(chunkBytes);
chunks.put(Base64.encodeToString(encryptedChunk, Base64.NO_WRAP));
}
mId = System.currentTimeMillis();
mType = NetworkPackage.PACKAGE_TYPE_ENCRYPTED;
mBody = new JSONObject();
try {
mBody.put("data", chunks);
}catch(Exception e){
e.printStackTrace();
Log.e("NetworkPackage","Exception");
}
Log.e("NetworkPackage", "Encrypted " + chunks.length()+" chunks");
}
public NetworkPackage decrypt(PrivateKey privateKey) throws Exception {
JSONArray chunks = mBody.getJSONArray("data");
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1PADDING");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
String decryptedJson = "";
for (int i = 0; i < chunks.length(); i++) {
byte[] encryptedChunk = Base64.decode(chunks.getString(i), Base64.NO_WRAP);
String decryptedChunk = new String(cipher.doFinal(encryptedChunk));
decryptedJson += decryptedChunk;
}
return unserialize(decryptedJson);
}
static public NetworkPackage createIdentityPackage(Context context) {
NetworkPackage np = new NetworkPackage(NetworkPackage.PACKAGE_TYPE_IDENTITY);
@ -130,6 +188,7 @@ public class NetworkPackage {
try {
np.mBody.put("deviceId", deviceId);
np.mBody.put("deviceName", HumanDeviceNames.getDeviceName());
np.mBody.put("protocolVersion", NetworkPackage.ProtocolVersion);
} catch (JSONException e) {
}
@ -137,5 +196,18 @@ public class NetworkPackage {
}
static public NetworkPackage createPublicKeyPackage(Context context) {
NetworkPackage np = new NetworkPackage(NetworkPackage.PACKAGE_TYPE_PAIR);
np.set("pair", true);
SharedPreferences globalSettings = PreferenceManager.getDefaultSharedPreferences(context);
String publicKey = "-----BEGIN PUBLIC KEY-----\n" + globalSettings.getString("publicKey", "").trim()+ "\n-----END PUBLIC KEY-----\n";
np.set("publicKey", publicKey);
return np;
}
}

View File

@ -1,5 +1,6 @@
package org.kde.connect.Plugins;
package org.kde.connect.Plugins.BatteryPlugin;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.BroadcastReceiver;
import android.content.Context;
@ -8,9 +9,10 @@ import android.content.IntentFilter;
import android.graphics.drawable.Drawable;
import android.os.BatteryManager;
import android.util.Log;
import android.widget.Button;
import org.kde.connect.NetworkPackage;
import org.kde.connect.PluginFactory;
import org.kde.connect.Plugins.Plugin;
import org.kde.kdeconnect.R;
public class BatteryPlugin extends Plugin {
@ -112,4 +114,8 @@ public class BatteryPlugin extends Plugin {
return null;
}
@Override
public Button getInterfaceButton(Activity activity) {
return null;
}
}

View File

@ -1,17 +1,17 @@
package org.kde.connect.Plugins;
package org.kde.connect.Plugins.ClibpoardPlugin;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.util.Log;
import android.widget.Button;
import org.kde.connect.NetworkPackage;
import org.kde.connect.PluginFactory;
import org.kde.connect.Plugins.Plugin;
import org.kde.kdeconnect.R;
public class ClipboardPlugin extends Plugin {
@ -117,4 +117,8 @@ public class ClipboardPlugin extends Plugin {
.create();
}
@Override
public Button getInterfaceButton(Activity activity) {
return null;
}
}

View File

@ -1,4 +1,4 @@
package org.kde.connect;
package org.kde.connect.Plugins.MprisPlugin;
import android.app.Activity;
import android.os.Bundle;
@ -13,9 +13,11 @@ import android.widget.SeekBar;
import android.widget.Spinner;
import android.widget.TextView;
import org.kde.connect.BackgroundService;
import org.kde.connect.ComputerLinks.BaseComputerLink;
import org.kde.connect.Device;
import org.kde.connect.LinkProviders.BaseLinkProvider;
import org.kde.connect.Plugins.MprisPlugin;
import org.kde.connect.NetworkPackage;
import org.kde.kdeconnect.R;
import java.util.ArrayList;

View File

@ -1,14 +1,18 @@
package org.kde.connect.Plugins;
package org.kde.connect.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.connect.NetworkPackage;
import org.kde.connect.PluginFactory;
import org.kde.connect.Plugins.Plugin;
import org.kde.kdeconnect.R;
import java.util.ArrayList;
@ -196,4 +200,18 @@ public class MprisPlugin extends Plugin {
return null;
}
@Override
public Button getInterfaceButton(final Activity activity) {
Button b = new Button(activity);
b.setText("Open remote control"); //TODO: i18n
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,4 +1,4 @@
package org.kde.connect;
package org.kde.connect.Plugins.NotificationsPlugin;
import android.app.Service;
import android.content.Context;

View File

@ -1,5 +1,6 @@
package org.kde.connect.Plugins;
package org.kde.connect.Plugins.NotificationsPlugin;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Notification;
import android.content.Context;
@ -13,11 +14,12 @@ import android.provider.Settings;
import android.service.notification.StatusBarNotification;
import android.util.Base64;
import android.util.Log;
import android.widget.Button;
import org.kde.connect.Helpers.AppsHelper;
import org.kde.connect.Helpers.ImagesHelper;
import org.kde.connect.NetworkPackage;
import org.kde.connect.NotificationReceiver;
import org.kde.connect.Plugins.Plugin;
import org.kde.kdeconnect.R;
import java.io.ByteArrayOutputStream;
@ -248,7 +250,7 @@ public class NotificationsPlugin extends Plugin implements NotificationReceiver.
e.printStackTrace();
}
}
}).run();
}).start();
}
}
});
@ -279,9 +281,9 @@ public class NotificationsPlugin extends Plugin implements NotificationReceiver.
if (Build.VERSION.SDK_INT < 18) {
return new AlertDialog.Builder(baseContext)
.setTitle("Notifications Plugin")
.setMessage("This plugin is not compatible with Android prior 4.3")
.setPositiveButton("Ok",new DialogInterface.OnClickListener() {
.setTitle("Notifications Plugin") //TODO: i18n
.setMessage("This plugin is not compatible with Android prior 4.3") //TODO: i18n
.setPositiveButton("Ok",new DialogInterface.OnClickListener() { //TODO: i18n
@Override
public void onClick(DialogInterface dialogInterface, int i) {
@ -290,18 +292,18 @@ public class NotificationsPlugin extends Plugin implements NotificationReceiver.
.create();
} else {
return new AlertDialog.Builder(baseContext)
.setTitle("Notifications Plugin")
.setMessage("You need to grant permission to access notifications")
.setTitle("Notifications Plugin") //TODO: i18n
.setMessage("You need to grant permission to access notifications") //TODO: i18n
.setPositiveButton("Open settings",new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
public void onClick(DialogInterface dialogInterface, int i) { //TODO: i18n
Intent intent=new Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS");
baseContext.startActivity(intent);
}
})
.setNegativeButton("Cancel",new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
public void onClick(DialogInterface dialogInterface, int i) { //TODO: i18n
//Do nothing
}
})
@ -309,5 +311,9 @@ public class NotificationsPlugin extends Plugin implements NotificationReceiver.
}
}
@Override
public Button getInterfaceButton(Activity activity) {
return null;
}
}

View File

@ -1,15 +1,19 @@
package org.kde.connect.Plugins;
package org.kde.connect.Plugins.PingPlugin;
import android.R;
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.util.Log;
import android.support.v4.app.NotificationCompat;
import android.view.View;
import android.widget.Button;
import org.kde.connect.Device;
import org.kde.connect.NetworkPackage;
import org.kde.connect.PluginFactory;
import org.kde.connect.Plugins.Plugin;
public class PingPlugin extends Plugin {
@ -60,7 +64,7 @@ public class PingPlugin extends Plugin {
if (np.getType().equals(NetworkPackage.PACKAGE_TYPE_PING)) {
//Log.e("PingPackageReceiver", "was a ping!");
Notification noti = new Notification.Builder(context)
Notification noti = new NotificationCompat.Builder(context)
.setContentTitle(device.getName())
.setContentText("Ping!")
.setTicker("Ping!")
@ -82,4 +86,17 @@ public class PingPlugin extends Plugin {
return null;
}
@Override
public Button getInterfaceButton(Activity activity) {
Button b = new Button(activity);
b.setText("Send ping"); //TODO: i18n
b.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
device.sendPackage(new NetworkPackage(NetworkPackage.PACKAGE_TYPE_PING));
}
});
return b;
}
}

View File

@ -6,6 +6,7 @@ import android.app.Application;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.widget.Button;
import org.kde.connect.Device;
import org.kde.connect.NetworkPackage;
@ -60,22 +61,29 @@ public abstract class Plugin {
/**
* Finish any ongoing operations, remove listeners... so
* this object could be garbage collected
* 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
* 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
* the problem (and how to fix it, if possible) to the user.
*/
public abstract AlertDialog getErrorDialog(Context baseContext);
//TODO: Add a getInterfaceButton to show in the device activity
/**
* 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

@ -1,19 +1,17 @@
package org.kde.connect;
package org.kde.connect.Plugins;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.preference.CheckBoxPreference;
import android.util.Log;
import org.kde.connect.Plugins.BatteryPlugin;
import org.kde.connect.Plugins.ClipboardPlugin;
import org.kde.connect.Plugins.MprisPlugin;
import org.kde.connect.Plugins.NotificationsPlugin;
import org.kde.connect.Plugins.PingPlugin;
import org.kde.connect.Plugins.Plugin;
import org.kde.connect.Plugins.TelephonyPlugin;
import org.kde.connect.Device;
import org.kde.connect.Plugins.BatteryPlugin.BatteryPlugin;
import org.kde.connect.Plugins.ClibpoardPlugin.ClipboardPlugin;
import org.kde.connect.Plugins.MprisPlugin.MprisPlugin;
import org.kde.connect.Plugins.NotificationsPlugin.NotificationsPlugin;
import org.kde.connect.Plugins.PingPlugin.PingPlugin;
import org.kde.connect.Plugins.TelephonyPlugin.TelephonyPlugin;
import java.util.Map;
import java.util.Set;

View File

@ -1,5 +1,6 @@
package org.kde.connect.Plugins;
package org.kde.connect.Plugins.TelephonyPlugin;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.BroadcastReceiver;
import android.content.Context;
@ -10,10 +11,11 @@ import android.os.Bundle;
import android.telephony.SmsMessage;
import android.telephony.TelephonyManager;
import android.util.Log;
import android.widget.Button;
import org.kde.connect.Helpers.ContactsHelper;
import org.kde.connect.NetworkPackage;
import org.kde.connect.PluginFactory;
import org.kde.connect.Plugins.Plugin;
import org.kde.kdeconnect.R;
public class TelephonyPlugin extends Plugin {
@ -182,4 +184,8 @@ public class TelephonyPlugin extends Plugin {
return null;
}
@Override
public Button getInterfaceButton(Activity activity) {
return null;
}
}

View File

@ -1,90 +0,0 @@
package org.kde.connect;
import android.content.Context;
import android.database.DataSetObserver;
import android.graphics.drawable.Drawable;
import android.preference.Preference;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.ListAdapter;
import android.widget.TextView;
import org.kde.kdeconnect.R;
import java.util.ArrayList;
public class PreferenceListAdapter implements ListAdapter {
private ArrayList<Preference> localList;
public PreferenceListAdapter(ArrayList<Preference> list) {
super();
Log.e("PreferenceListAdapter", ""+list.size());
localList = list;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View v = localList.get(position).getView(convertView, parent);
v.setEnabled(true);
v.setFocusable(true);
return v;
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public void unregisterDataSetObserver(DataSetObserver observer) {
} // Empty
@Override
public void registerDataSetObserver(DataSetObserver observer) {
} // Empty
@Override
public boolean isEmpty() {
return localList.size() == 0;
}
@Override
public boolean hasStableIds() {
return false;
}
@Override
public int getViewTypeCount() {
return localList.size();
}
@Override
public int getItemViewType(int position) {
return 0;
} // Empty
@Override
public Object getItem(int position) {
return localList.get(position);
} // Empty
@Override
public int getCount() {
return localList.size();
}
@Override
public boolean isEnabled(int i) {
return false;
}
@Override
public boolean areAllItemsEnabled() {
return false;
}
}

View File

@ -1,86 +0,0 @@
package org.kde.connect;
import android.app.ListActivity;
import android.content.SharedPreferences;
import android.os.Build;
import android.os.Bundle;
import android.preference.CheckBoxPreference;
import android.preference.Preference;
import android.preference.PreferenceActivity;
import android.preference.PreferenceManager;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import org.kde.kdeconnect.R;
import java.util.AbstractCollection;
import java.util.ArrayList;
import java.util.Set;
public class SettingsActivity extends ListActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
/*
ActionBar actionBar = getActionBar();
actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_HOME
| ActionBar.DISPLAY_SHOW_TITLE);
actionBar.setDisplayHomeAsUpEnabled(true);*/
getListView().setItemsCanFocus(true);
getListView().setFocusable(false);
getListView().setEnabled(true);
getListView().setChoiceMode(AbsListView.CHOICE_MODE_SINGLE);
getListView().setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
getListView().setScrollBarStyle(View.SCROLLBARS_INSIDE_OVERLAY);
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();
ArrayList<Preference> preferences = new ArrayList<Preference>();
for (final String pluginName : plugins) {
Log.e("SettingsActivity", pluginName);
CheckBoxPreference pref = new CheckBoxPreference(getBaseContext());
PluginFactory.PluginInfo info = PluginFactory.getPluginInfo(getBaseContext(), pluginName);
pref.setKey(pluginName);
pref.setTitle(info.getDisplayName());
pref.setSummary(info.getDescription());
pref.setSelectable(true);
pref.setEnabled(true);
pref.setChecked(device.isPluginEnabled(pluginName));
pref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
Log.e("CLICK","CLICK");
return false;
}
});
pref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
device.setPluginEnabled(pluginName, (Boolean)newValue);
return true;
}
});
preferences.add(pref);
}
setListAdapter(new PreferenceListAdapter(preferences));
}
});
}
}

View File

@ -0,0 +1,151 @@
package org.kde.connect.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.connect.BackgroundService;
import org.kde.connect.Device;
import org.kde.connect.Plugins.Plugin;
import org.kde.connect.UserInterface.List.ButtonItem;
import org.kde.connect.UserInterface.List.ListAdapter;
import org.kde.connect.UserInterface.List.SectionItem;
import org.kde.kdeconnect.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() {
Log.e("MainActivity", "updateComputerList");
//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("Plugins failed to load (tap for more info):"); //TODO: i18n
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()) {
//TODO: i18n
menu.add("Select 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;
}
});
//TODO: i18n
menu.add("Unpair").setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem menuItem) {
device.unpair();
finish();
return true;
}
});
return true;
} else {
return false;
}
}
}

View File

@ -1,4 +1,4 @@
package org.kde.connect;
package org.kde.connect.UserInterface;
import android.content.Context;
import android.database.DataSetObserver;

View File

@ -0,0 +1,21 @@
package org.kde.connect.UserInterface.List;
import android.app.Activity;
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.connect.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.connect.Device;
import org.kde.connect.UserInterface.DeviceActivity;
import org.kde.connect.UserInterface.PairActivity;
import org.kde.kdeconnect.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,34 @@
package org.kde.connect.UserInterface.List;
import java.util.ArrayList;
import android.app.Activity;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
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,38 @@
package org.kde.connect.UserInterface.List;
import android.app.Activity;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.TextView;
import org.kde.kdeconnect.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,190 @@
package org.kde.connect.UserInterface;
import android.os.AsyncTask;
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.connect.BackgroundService;
import org.kde.connect.ComputerLinks.BaseComputerLink;
import org.kde.connect.Device;
import org.kde.connect.LinkProviders.BaseLinkProvider;
import org.kde.connect.NetworkPackage;
import org.kde.connect.UserInterface.List.ListAdapter;
import org.kde.connect.UserInterface.List.DeviceItem;
import org.kde.connect.UserInterface.List.SectionItem;
import org.kde.kdeconnect.R;
import java.util.ArrayList;
import java.util.Collection;
public class MainActivity extends ActionBarActivity {
//
// Action bar
//
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:
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() {
Log.e("MainActivity","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;
section = new SectionItem("Connected devices"); //TODO: i18n
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("Not paired devices"); //TODO: i18n
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("Remembered devices"); //TODO: i18n
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 this 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.connect.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.connect.BackgroundService;
import org.kde.connect.Device;
import org.kde.kdeconnect.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("Pairing requested"); //TODO: i18n
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,37 @@
package org.kde.connect.UserInterface;
import android.content.Context;
import android.database.DataSetObserver;
import android.graphics.drawable.Drawable;
import android.preference.Preference;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.ListAdapter;
import android.widget.TextView;
import org.kde.kdeconnect.R;
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);
Log.e("PreferenceListAdapter", ""+items.size());
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,74 @@
package org.kde.connect.UserInterface;
import android.app.ListActivity;
import android.os.Bundle;
import android.preference.CheckBoxPreference;
import android.preference.Preference;
import android.support.v7.appcompat.R;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.AdapterView;
import org.kde.connect.BackgroundService;
import org.kde.connect.Device;
import org.kde.connect.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) {
Log.e("SettingsActivity", pluginName);
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);
}
});
}
}

View File

@ -9,23 +9,19 @@
tools:context=".MainActivity"
android:orientation="vertical">
<Button android:id="@+id/button1" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Select plugins"/>
<Button android:id="@+id/button2" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Send Ping"/>
<Button android:id="@+id/button3" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Mpris controls"/>
<ListView
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:id="@+id/buttons_list"
android:layout_weight="1"
/>
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:text="Plugins failed to load (tap for more info):"
android:visibility="gone"
android:id="@+id/textView"/>
<ListView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:id="@+id/listView1"
android:layout_weight="1"/>
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:id="@+id/errors_list"
android:layout_weight="1"
/>
</LinearLayout>

View File

@ -1,4 +1,4 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<ListView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
@ -7,12 +7,8 @@
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
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:id="@+id/listView1"
android:layout_weight="1"/>
</LinearLayout>
</ListView>

View File

@ -0,0 +1,44 @@
<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="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="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="Accept"
android:layout_weight="1" />
<Button android:id="@+id/reject_button" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="Reject"
android:layout_weight="1" />
</LinearLayout>
</LinearLayout>

View File

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Switch
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Trust"
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/switch1"></Switch>

View File

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

View File

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android: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="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="10dip"
android:layout_marginRight="6dip"
android:layout_marginTop="3dip"
android:layout_marginBottom="3dip"
android:layout_weight="1">
<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

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ProgressBar xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/progressBar2"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
</ProgressBar>

View File

@ -1,19 +0,0 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
<item
android:id="@+id/menu_trusted"
android:orderInCategory="200"
android:actionLayout="@layout/barswitch"
android:showAsAction="always"
android:title="Trusted"/>
<item
android:id="@+id/menu_plugins"
android:icon="@drawable/action_settings"
android:orderInCategory="250"
android:showAsAction="always"
android:title="Select features"/>
</menu>

View File

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

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="action_settings" >Settings</string>
<string name="action_settings">Settings</string>
<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>
@ -15,6 +15,6 @@
<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>
</resources>

0
gradlew vendored Normal file → Executable file
View File