diff --git a/.gitignore b/.gitignore
index 9068cd011..fadc061eb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,3 +24,6 @@ proguard/
# Log Files
*.log
+
+.idea
+*.iml
diff --git a/README.md b/README.md
index 5aee1edbd..d7be67769 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,27 @@
Gadgetbridge
============
+
+Gadgetbridge is a Android (4.4+) Application which will allow you to use your
+Gadget (Smartwatches/Fitness Bands etc) without the vendors closed source
+application and without the need to create an account and sync your data to the
+vendors servers.
+
+Right now this is in very early testing stages and only supports the Pebble.
+
+USE IT AT YOUR OWN RISK. It will problably not work. And if it works it will
+annoy you more than it helps you ;)
+
+Known Visible Issues:
+
+* No special notifications, EVERYTHING will be send as a Chat/SMS message
+* Notifications are not properly queued, if two arrive at about the same time,
+ one of them will get lost
+* Connection to Pebble will be reopened and closed for every message (dont know
+ if this saves or consumes more energy)
+* Android 4.4+ only, we can only change this by implementing an
+ AccessibiltyService. Don't know if it is worth the hassle.
+* This will open the dialog to allow capturing notifications every time the
+ Activity gets restarted.
+
+Apart from that there are many internal design flaws which we will discuss using
+the issue tracker.
diff --git a/app/build.gradle b/app/build.gradle
new file mode 100644
index 000000000..2b4b58700
--- /dev/null
+++ b/app/build.gradle
@@ -0,0 +1,26 @@
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion 21
+ buildToolsVersion "21.1.2"
+
+ defaultConfig {
+ applicationId "nodomain.freeyourgadget.gadgetbridge"
+ minSdkVersion 19
+ targetSdkVersion 21
+ versionCode 1
+ versionName "1.0"
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+}
+
+dependencies {
+ compile fileTree(dir: 'libs', include: ['*.jar'])
+ compile 'com.android.support:appcompat-v7:21.0.3'
+ compile 'com.android.support:support-v4:21.0.3'
+}
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
new file mode 100644
index 000000000..dc8ca6043
--- /dev/null
+++ b/app/proguard-rules.pro
@@ -0,0 +1,17 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /home/andi/Android/Sdk/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 *;
+#}
diff --git a/app/src/androidTest/java/nodomain/freeyourgadget/gadgetbridge/ApplicationTest.java b/app/src/androidTest/java/nodomain/freeyourgadget/gadgetbridge/ApplicationTest.java
new file mode 100644
index 000000000..1e1760d36
--- /dev/null
+++ b/app/src/androidTest/java/nodomain/freeyourgadget/gadgetbridge/ApplicationTest.java
@@ -0,0 +1,13 @@
+package nodomain.freeyourgadget.gadgetbridge;
+
+import android.app.Application;
+import android.test.ApplicationTestCase;
+
+/**
+ * Testing Fundamentals
+ */
+public class ApplicationTest extends ApplicationTestCase {
+ public ApplicationTest() {
+ super(Application.class);
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..9816dc3f3
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/ControlCenter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/ControlCenter.java
new file mode 100644
index 000000000..ac296ff5a
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/ControlCenter.java
@@ -0,0 +1,236 @@
+package nodomain.freeyourgadget.gadgetbridge;
+
+import android.app.NotificationManager;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothSocket;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.support.v4.app.NotificationCompat;
+import android.support.v7.app.ActionBarActivity;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.Toast;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Set;
+import java.util.UUID;
+
+public class ControlCenter extends ActionBarActivity {
+ // SPP Serial Device UUID
+ private static final UUID SERIAL_UUID = UUID.fromString("00001101-0000-1000-8000-00805f9b34fb");
+
+ BluetoothAdapter mBtAdapter;
+ BluetoothDevice mBtDevice;
+ BluetoothSocket mBtSocket;
+ Button sendButton;
+ Button testNotificationButton;
+ EditText editTitle;
+ EditText editContent;
+ private NotificationReceiver nReceiver;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_controlcenter);
+
+ //Check the system status
+ mBtAdapter = BluetoothAdapter.getDefaultAdapter();
+ if (mBtAdapter == null) {
+ Toast.makeText(this, "Bluetooth is not supported.", Toast.LENGTH_SHORT).show();
+ finish();
+ return;
+ }
+ if (!mBtAdapter.isEnabled()) {
+ Toast.makeText(this, "Bluetooth is disabled.", Toast.LENGTH_SHORT).show();
+ finish();
+ return;
+ }
+
+ Set pairedDevices = mBtAdapter.getBondedDevices();
+ for (BluetoothDevice device : pairedDevices) {
+ if (device.getName().indexOf("Pebble") == 0) {
+ // Matching device found
+ mBtDevice = device;
+ }
+ }
+
+ editTitle = (EditText) findViewById(R.id.editTitle);
+ editContent = (EditText) findViewById(R.id.editContent);
+ sendButton = (Button) findViewById(R.id.sendButton);
+ sendButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+
+ if (!mBtAdapter.isEnabled() || mBtDevice == null)
+ return;
+ String title = editTitle.getText().toString();
+ String content = editContent.getText().toString();
+ try {
+ if (mBtSocket == null || !mBtSocket.isConnected()) {
+ mBtSocket = mBtDevice.createRfcommSocketToServiceRecord(SERIAL_UUID);
+ mBtSocket.connect();
+ }
+ ConnectedTask task = new ConnectedTask();
+ task.execute(mBtSocket, title, content);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ });
+ testNotificationButton = (Button) findViewById(R.id.testNotificationButton);
+ testNotificationButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ testNotification();
+ }
+ });
+
+ Intent enableIntent = new Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS");
+
+ startActivity(enableIntent);
+ nReceiver = new NotificationReceiver();
+
+ IntentFilter filter = new IntentFilter();
+ filter.addAction("nodomain.freeyourgadget.gadgetbridge.NOTIFICATION_LISTENER");
+ registerReceiver(nReceiver, filter);
+ }
+
+ private void testNotification() {
+ NotificationManager nManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
+ NotificationCompat.Builder ncomp = new NotificationCompat.Builder(this);
+ ncomp.setContentTitle("Test Notification");
+ ncomp.setContentText("This is a Test Notification from Gadgetbridge");
+ ncomp.setTicker("This is a Test Notificytion from Gadgetbridge");
+ ncomp.setSmallIcon(R.drawable.ic_launcher);
+ ncomp.setAutoCancel(true);
+ nManager.notify((int) System.currentTimeMillis(), ncomp.build());
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ // Inflate the menu; this adds items to the action bar if it is present.
+ getMenuInflater().inflate(R.menu.menu_main, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ // Handle action bar item clicks here. The action bar will
+ // automatically handle clicks on the Home/Up button, so long
+ // as you specify a parent activity in AndroidManifest.xml.
+ int id = item.getItemId();
+
+ //noinspection SimplifiableIfStatement
+ if (id == R.id.action_settings) {
+ //Intent intent = new Intent(this, SettingsActivity.class);
+ //startActivity(intent);
+ return true;
+ }
+
+ return super.onOptionsItemSelected(item);
+ }
+
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ try {
+ if (mBtSocket != null) {
+ mBtSocket.close();
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ if (nReceiver != null) {
+ unregisterReceiver(nReceiver);
+ }
+ }
+
+
+ //AsyncTask to receive a single line of data and post
+ private class ConnectedTask extends
+ AsyncTask {
+ @Override
+ protected String doInBackground(
+ Object... params) {
+ InputStream in = null;
+ OutputStream out = null;
+ BluetoothSocket socket = (BluetoothSocket) params[0];
+ String title = (String) params[1];
+ String content = (String) params[2];
+ try {
+ byte[] buffer = new byte[1024];
+ String result;
+ in = socket.getInputStream();
+
+ //in.read(buffer);
+ //result = PebbleProtocol.decodeResponse(buffer);
+
+ out = socket.getOutputStream();
+
+
+ byte[] msg;
+ msg = PebbleProtocol.encodeSMS(title, content);
+
+ //msg = PebbleProtocol.encodeSetTime();
+ //msg = PebbleProtocol.encodeIncomingCall("03012323", title);
+ //msg = PebbleProtocol.encodeEmail(title, "subject", content);
+
+ out.write(msg);
+ SystemClock.sleep(500);
+ //in.read(buffer);
+ //result = PebbleProtocol.decodeResponse(buffer);
+ result = "ok";
+ //Close the connection
+ return result.trim();
+ } catch (Exception exc) {
+ return "error";
+ }
+ }
+
+ @Override
+ protected void onPostExecute(String result) {
+ Toast.makeText(ControlCenter.this, result,
+ Toast.LENGTH_SHORT).show();
+ try {
+ mBtSocket.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ }
+
+ class NotificationReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (!mBtAdapter.isEnabled() || mBtDevice == null)
+ return;
+
+ String title = intent.getStringExtra("notification_title");
+ String content = intent.getStringExtra("notification_content");
+ try {
+ if (mBtSocket == null || !mBtSocket.isConnected()) {
+ mBtSocket = mBtDevice.createRfcommSocketToServiceRecord(SERIAL_UUID);
+ mBtSocket.connect();
+ }
+ ConnectedTask task = new ConnectedTask();
+ task.execute(mBtSocket, title, content);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/NotificationListener.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/NotificationListener.java
new file mode 100644
index 000000000..bad25cfa5
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/NotificationListener.java
@@ -0,0 +1,39 @@
+package nodomain.freeyourgadget.gadgetbridge;
+
+import android.app.Notification;
+import android.content.Intent;
+import android.os.Bundle;
+import android.service.notification.NotificationListenerService;
+import android.service.notification.StatusBarNotification;
+
+public class NotificationListener extends NotificationListenerService {
+
+ private String TAG = this.getClass().getSimpleName();
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ }
+
+ @Override
+ public void onNotificationPosted(StatusBarNotification sbn) {
+ Intent i = new Intent("nodomain.freeyourgadget.gadgetbridge.NOTIFICATION_LISTENER");
+ Notification notification = sbn.getNotification();
+ Bundle extras = notification.extras;
+ String title = extras.getCharSequence(Notification.EXTRA_TITLE).toString();
+ String content = extras.getCharSequence(Notification.EXTRA_TEXT).toString();
+ i.putExtra("notification_title", title);
+ i.putExtra("notification_content", content);
+ sendBroadcast(i);
+ }
+
+ @Override
+ public void onNotificationRemoved(StatusBarNotification sbn) {
+
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/PebbleProtocol.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/PebbleProtocol.java
new file mode 100644
index 000000000..aa4f3f995
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/PebbleProtocol.java
@@ -0,0 +1,133 @@
+package nodomain.freeyourgadget.gadgetbridge;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.SimpleTimeZone;
+
+public class PebbleProtocol {
+ static final short ENDPOINT_FIRMWARE = 1;
+ static final short ENDPOINT_TIME = 11;
+ static final short ENDPOINT_FIRMWAREVERSION = 16;
+ static final short ENDPOINT_PHONEVERSION = 17;
+ static final short ENDPOINT_SYSTEMMESSAGE = 18;
+ static final short ENDPOINT_MUSICCONTROL = 32;
+ static final short ENDPOINT_PHONECONTROL = 33;
+ static final short ENDPOINT_APPLICATIONMESSAGE = 48;
+ static final short ENDPOINT_LAUNCHER = 49;
+ static final short ENDPOINT_LOGS = 2000;
+ static final short ENDPOINT_PING = 2001;
+ static final short ENDPOINT_LOGDUMP = 2002;
+ static final short ENDPOINT_RESET = 2003;
+ static final short ENDPOINT_APP = 2004;
+ static final short ENDPOINT_APPLOGS = 2006;
+ static final short ENDPOINT_NOTIFICATION = 3000;
+ static final short ENDPOINT_RESOURCE = 4000;
+ static final short ENDPOINT_SYSREG = 5000;
+ static final short ENDPOINT_FCTREG = 5001;
+ static final short ENDPOINT_APPMANAGER = 6000;
+ static final short ENDPOINT_RUNKEEPER = 7000;
+ static final short ENDPOINT_SCREENSHOT = 8000;
+ static final short ENDPOINT_PUTBYTES = (short) 48879;
+
+ static final byte NOTIFICATION_EMAIL = 0;
+ static final byte NOTIFICATION_SMS = 1;
+ static final byte NOTIFICATION_TWITTER = 2;
+ static final byte NOTIFICATION_FACEBOOK = 3;
+
+ static final byte PHONECONTROL_ANSWER = 1;
+ static final byte PHONECONTROL_HANGUP = 2;
+ static final byte PHONECONTROL_GETSTATE = 3;
+ static final byte PHONECONTROL_INCOMINGCALL = 4;
+ static final byte PHONECONTROL_OUTGOINGCALL = 5;
+ static final byte PHONECONTROL_MISSEDCALL = 6;
+ static final byte PHONECONTROL_RING = 7;
+ static final byte PHONECONTROL_START = 8;
+ static final byte PHONECONTROL_END = 9;
+
+ static final byte TIME_GETTIME = 0;
+ static final byte TIME_SETTIME = 2;
+
+ static final byte LENGTH_PREFIX = 4;
+ static final byte LENGTH_SETTIME = 9;
+
+ static byte[] encodeMessage(short endpoint, byte type, String[] parts) {
+ // Calculate length first
+ int length = LENGTH_PREFIX + 1;
+ for (String s : parts) {
+ length += (1 + s.getBytes().length);
+ }
+
+ // Encode Prefix
+ ByteBuffer buf = ByteBuffer.allocate(length);
+ buf.order(ByteOrder.BIG_ENDIAN);
+ buf.putShort((short) (length - LENGTH_PREFIX));
+ buf.putShort(endpoint);
+ buf.put(type);
+
+ // Encode Pascal-Style Strings
+ for (String s : parts) {
+
+ int partlength = s.getBytes().length;
+ if (partlength > 255) partlength = 255;
+ buf.put((byte) partlength);
+ buf.put(s.getBytes(), 0, partlength);
+ }
+
+ return buf.array();
+ }
+
+ public static byte[] encodeSMS(String from, String body) {
+ Long ts = System.currentTimeMillis() / 1000;
+ String tsstring = ts.toString(); // SIC
+ String[] parts = {from, body, tsstring};
+
+ return encodeMessage(ENDPOINT_NOTIFICATION, NOTIFICATION_SMS, parts);
+ }
+
+ public static byte[] encodeEmail(String from, String subject, String body) {
+ Long ts = System.currentTimeMillis() / 1000;
+ String tsstring = ts.toString(); // SIC
+ String[] parts = {from, body, tsstring, subject};
+
+ return encodeMessage(ENDPOINT_NOTIFICATION, NOTIFICATION_EMAIL, parts);
+ }
+
+ public static byte[] encodeSetTime() {
+ long ts = System.currentTimeMillis() / 1000;
+ ts += SimpleTimeZone.getDefault().getOffset(ts) / 1000;
+ ByteBuffer buf = ByteBuffer.allocate(LENGTH_SETTIME);
+ buf.order(ByteOrder.BIG_ENDIAN);
+ buf.putShort((short) (LENGTH_SETTIME - LENGTH_PREFIX));
+ buf.putShort(ENDPOINT_TIME);
+ buf.put(TIME_SETTIME);
+ buf.putInt((int) ts);
+
+ return buf.array();
+ }
+
+ public static byte[] encodeIncomingCall(String number, String name) {
+ String cookie = "000"; // That's a dirty trick to make the cookie part 4 bytes long :P
+ String[] parts = {cookie, number, name};
+ return encodeMessage(ENDPOINT_PHONECONTROL, PHONECONTROL_INCOMINGCALL, parts);
+ }
+
+ // FIXME: that should return data into some unified struct/Class
+ public static String decodeResponse(byte[] responseData) {
+ ByteBuffer buf = ByteBuffer.wrap(responseData);
+ buf.order(ByteOrder.BIG_ENDIAN);
+ short length = buf.getShort();
+ short endpoint = buf.getShort();
+ byte extra = 0;
+
+ switch (endpoint) {
+ case ENDPOINT_PHONECONTROL:
+ extra = buf.get();
+ break;
+ default:
+ break;
+ }
+ String ret = Short.toString(length) + "/" + Short.toString(endpoint) + "/" + Byte.toString(extra);
+
+ return ret;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/SettingsActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/SettingsActivity.java
new file mode 100644
index 000000000..2395a1fdd
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/SettingsActivity.java
@@ -0,0 +1,258 @@
+package nodomain.freeyourgadget.gadgetbridge;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.media.Ringtone;
+import android.media.RingtoneManager;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.preference.ListPreference;
+import android.preference.Preference;
+import android.preference.PreferenceActivity;
+import android.preference.PreferenceCategory;
+import android.preference.PreferenceFragment;
+import android.preference.PreferenceManager;
+import android.preference.RingtonePreference;
+import android.text.TextUtils;
+
+import java.util.List;
+
+/**
+ * A {@link PreferenceActivity} that presents a set of application settings. On
+ * handset devices, settings are presented as a single list. On tablets,
+ * settings are split by category, with category headers shown to the left of
+ * the list of settings.
+ *
+ * See
+ * Android Design: Settings for design guidelines and the Settings
+ * API Guide for more information on developing a Settings UI.
+ */
+public class SettingsActivity extends PreferenceActivity {
+ /**
+ * Determines whether to always show the simplified settings UI, where
+ * settings are presented in a single list. When false, settings are shown
+ * as a master/detail two-pane view on tablets. When true, a single pane is
+ * shown on tablets.
+ */
+ private static final boolean ALWAYS_SIMPLE_PREFS = false;
+
+
+ @Override
+ protected void onPostCreate(Bundle savedInstanceState) {
+ super.onPostCreate(savedInstanceState);
+
+ setupSimplePreferencesScreen();
+ }
+
+ /**
+ * Shows the simplified settings UI if the device configuration if the
+ * device configuration dictates that a simplified, single-pane UI should be
+ * shown.
+ */
+ private void setupSimplePreferencesScreen() {
+ if (!isSimplePreferences(this)) {
+ return;
+ }
+
+ // In the simplified UI, fragments are not used at all and we instead
+ // use the older PreferenceActivity APIs.
+
+ // Add 'general' preferences.
+ addPreferencesFromResource(R.xml.pref_general);
+
+ // Add 'notifications' preferences, and a corresponding header.
+ PreferenceCategory fakeHeader = new PreferenceCategory(this);
+ fakeHeader.setTitle(R.string.pref_header_notifications);
+ getPreferenceScreen().addPreference(fakeHeader);
+ addPreferencesFromResource(R.xml.pref_notification);
+
+ // Add 'data and sync' preferences, and a corresponding header.
+ fakeHeader = new PreferenceCategory(this);
+ fakeHeader.setTitle(R.string.pref_header_data_sync);
+ getPreferenceScreen().addPreference(fakeHeader);
+ addPreferencesFromResource(R.xml.pref_data_sync);
+
+ // Bind the summaries of EditText/List/Dialog/Ringtone preferences to
+ // their values. When their values change, their summaries are updated
+ // to reflect the new value, per the Android Design guidelines.
+ bindPreferenceSummaryToValue(findPreference("example_text"));
+ bindPreferenceSummaryToValue(findPreference("example_list"));
+ bindPreferenceSummaryToValue(findPreference("notifications_new_message_ringtone"));
+ bindPreferenceSummaryToValue(findPreference("sync_frequency"));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onIsMultiPane() {
+ return isXLargeTablet(this) && !isSimplePreferences(this);
+ }
+
+ /**
+ * Helper method to determine if the device has an extra-large screen. For
+ * example, 10" tablets are extra-large.
+ */
+ private static boolean isXLargeTablet(Context context) {
+ return (context.getResources().getConfiguration().screenLayout
+ & Configuration.SCREENLAYOUT_SIZE_MASK) >= Configuration.SCREENLAYOUT_SIZE_XLARGE;
+ }
+
+ /**
+ * Determines whether the simplified settings UI should be shown. This is
+ * true if this is forced via {@link #ALWAYS_SIMPLE_PREFS}, or the device
+ * doesn't have newer APIs like {@link PreferenceFragment}, or the device
+ * doesn't have an extra-large screen. In these cases, a single-pane
+ * "simplified" settings UI should be shown.
+ */
+ private static boolean isSimplePreferences(Context context) {
+ return ALWAYS_SIMPLE_PREFS
+ || Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB
+ || !isXLargeTablet(context);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+ public void onBuildHeaders(List target) {
+ if (!isSimplePreferences(this)) {
+ loadHeadersFromResource(R.xml.pref_headers, target);
+ }
+ }
+
+ /**
+ * A preference value change listener that updates the preference's summary
+ * to reflect its new value.
+ */
+ private static Preference.OnPreferenceChangeListener sBindPreferenceSummaryToValueListener = new Preference.OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object value) {
+ String stringValue = value.toString();
+
+ if (preference instanceof ListPreference) {
+ // For list preferences, look up the correct display value in
+ // the preference's 'entries' list.
+ ListPreference listPreference = (ListPreference) preference;
+ int index = listPreference.findIndexOfValue(stringValue);
+
+ // Set the summary to reflect the new value.
+ preference.setSummary(
+ index >= 0
+ ? listPreference.getEntries()[index]
+ : null);
+
+ } else if (preference instanceof RingtonePreference) {
+ // For ringtone preferences, look up the correct display value
+ // using RingtoneManager.
+ if (TextUtils.isEmpty(stringValue)) {
+ // Empty values correspond to 'silent' (no ringtone).
+ preference.setSummary(R.string.pref_ringtone_silent);
+
+ } else {
+ Ringtone ringtone = RingtoneManager.getRingtone(
+ preference.getContext(), Uri.parse(stringValue));
+
+ if (ringtone == null) {
+ // Clear the summary if there was a lookup error.
+ preference.setSummary(null);
+ } else {
+ // Set the summary to reflect the new ringtone display
+ // name.
+ String name = ringtone.getTitle(preference.getContext());
+ preference.setSummary(name);
+ }
+ }
+
+ } else {
+ // For all other preferences, set the summary to the value's
+ // simple string representation.
+ preference.setSummary(stringValue);
+ }
+ return true;
+ }
+ };
+
+ /**
+ * Binds a preference's summary to its value. More specifically, when the
+ * preference's value is changed, its summary (line of text below the
+ * preference title) is updated to reflect the value. The summary is also
+ * immediately updated upon calling this method. The exact display format is
+ * dependent on the type of preference.
+ *
+ * @see #sBindPreferenceSummaryToValueListener
+ */
+ private static void bindPreferenceSummaryToValue(Preference preference) {
+ // Set the listener to watch for value changes.
+ preference.setOnPreferenceChangeListener(sBindPreferenceSummaryToValueListener);
+
+ // Trigger the listener immediately with the preference's
+ // current value.
+ sBindPreferenceSummaryToValueListener.onPreferenceChange(preference,
+ PreferenceManager
+ .getDefaultSharedPreferences(preference.getContext())
+ .getString(preference.getKey(), ""));
+ }
+
+ /**
+ * This fragment shows general preferences only. It is used when the
+ * activity is showing a two-pane settings UI.
+ */
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+ public static class GeneralPreferenceFragment extends PreferenceFragment {
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ addPreferencesFromResource(R.xml.pref_general);
+
+ // Bind the summaries of EditText/List/Dialog/Ringtone preferences
+ // to their values. When their values change, their summaries are
+ // updated to reflect the new value, per the Android Design
+ // guidelines.
+ bindPreferenceSummaryToValue(findPreference("example_text"));
+ bindPreferenceSummaryToValue(findPreference("example_list"));
+ }
+ }
+
+ /**
+ * This fragment shows notification preferences only. It is used when the
+ * activity is showing a two-pane settings UI.
+ */
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+ public static class NotificationPreferenceFragment extends PreferenceFragment {
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ addPreferencesFromResource(R.xml.pref_notification);
+
+ // Bind the summaries of EditText/List/Dialog/Ringtone preferences
+ // to their values. When their values change, their summaries are
+ // updated to reflect the new value, per the Android Design
+ // guidelines.
+ bindPreferenceSummaryToValue(findPreference("notifications_new_message_ringtone"));
+ }
+ }
+
+ /**
+ * This fragment shows data and sync preferences only. It is used when the
+ * activity is showing a two-pane settings UI.
+ */
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+ public static class DataSyncPreferenceFragment extends PreferenceFragment {
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ addPreferencesFromResource(R.xml.pref_data_sync);
+
+ // Bind the summaries of EditText/List/Dialog/Ringtone preferences
+ // to their values. When their values change, their summaries are
+ // updated to reflect the new value, per the Android Design
+ // guidelines.
+ bindPreferenceSummaryToValue(findPreference("sync_frequency"));
+ }
+ }
+}
diff --git a/app/src/main/res/drawable-hdpi/ic_launcher.png b/app/src/main/res/drawable-hdpi/ic_launcher.png
new file mode 100644
index 000000000..96a442e5b
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_launcher.png differ
diff --git a/app/src/main/res/drawable-mdpi/ic_launcher.png b/app/src/main/res/drawable-mdpi/ic_launcher.png
new file mode 100644
index 000000000..359047dfa
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_launcher.png differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_launcher.png b/app/src/main/res/drawable-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..71c6d760f
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_launcher.png b/app/src/main/res/drawable-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..4df189464
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/layout/activity_controlcenter.xml b/app/src/main/res/layout/activity_controlcenter.xml
new file mode 100644
index 000000000..ff1473819
--- /dev/null
+++ b/app/src/main/res/layout/activity_controlcenter.xml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/menu/menu_main.xml b/app/src/main/res/menu/menu_main.xml
new file mode 100644
index 000000000..45d2280b5
--- /dev/null
+++ b/app/src/main/res/menu/menu_main.xml
@@ -0,0 +1,7 @@
+
+
+
diff --git a/app/src/main/res/values-w820dp/dimens.xml b/app/src/main/res/values-w820dp/dimens.xml
new file mode 100644
index 000000000..63fc81644
--- /dev/null
+++ b/app/src/main/res/values-w820dp/dimens.xml
@@ -0,0 +1,6 @@
+
+
+ 64dp
+
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
new file mode 100644
index 000000000..47c822467
--- /dev/null
+++ b/app/src/main/res/values/dimens.xml
@@ -0,0 +1,5 @@
+
+
+ 16dp
+ 16dp
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 000000000..fb1ed07b2
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,9 @@
+
+
+
+ Gadgetbridge
+ Gadgetbridge Control Center
+ Hello world!
+ Settings
+
+
diff --git a/app/src/main/res/values/strings_activity_settings.xml b/app/src/main/res/values/strings_activity_settings.xml
new file mode 100644
index 000000000..2686b04f7
--- /dev/null
+++ b/app/src/main/res/values/strings_activity_settings.xml
@@ -0,0 +1,60 @@
+
+
+
+
+
+ General
+
+ Enable social recommendations
+ Recommendations for people to contact
+ based on your message history
+
+
+ Display name
+ John Smith
+
+ Add friends to messages
+
+ - Always
+ - When possible
+ - Never
+
+
+ - 1
+ - 0
+ - -1
+
+
+
+ Data & sync
+
+ Sync frequency
+
+ - 15 minutes
+ - 30 minutes
+ - 1 hour
+ - 3 hours
+ - 6 hours
+ - Never
+
+
+ - 15
+ - 30
+ - 60
+ - 180
+ - 360
+ - -1
+
+
+ System sync settings
+
+
+ Notifications
+
+ New message notifications
+
+ Ringtone
+ Silent
+
+ Vibrate
+
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
new file mode 100644
index 000000000..766ab9930
--- /dev/null
+++ b/app/src/main/res/values/styles.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/xml/pref_data_sync.xml b/app/src/main/res/xml/pref_data_sync.xml
new file mode 100644
index 000000000..ffda831c4
--- /dev/null
+++ b/app/src/main/res/xml/pref_data_sync.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/xml/pref_general.xml b/app/src/main/res/xml/pref_general.xml
new file mode 100644
index 000000000..c49cbed20
--- /dev/null
+++ b/app/src/main/res/xml/pref_general.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/xml/pref_headers.xml b/app/src/main/res/xml/pref_headers.xml
new file mode 100644
index 000000000..2996b9999
--- /dev/null
+++ b/app/src/main/res/xml/pref_headers.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/xml/pref_notification.xml b/app/src/main/res/xml/pref_notification.xml
new file mode 100644
index 000000000..b4b8cae99
--- /dev/null
+++ b/app/src/main/res/xml/pref_notification.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 000000000..6356aabdc
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,19 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+ repositories {
+ jcenter()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:1.0.0'
+
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+allprojects {
+ repositories {
+ jcenter()
+ }
+}
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 000000000..1d3591c8a
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,18 @@
+# Project-wide Gradle settings.
+
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+# Default value: -Xmx10248m -XX:MaxPermSize=256m
+# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
+
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000..8c0fb64a8
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..0c71e760d
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -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=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
diff --git a/gradlew b/gradlew
new file mode 100755
index 000000000..91a7e269e
--- /dev/null
+++ b/gradlew
@@ -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 "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 000000000..aec99730b
--- /dev/null
+++ b/gradlew.bat
@@ -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
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 000000000..e7b4def49
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1 @@
+include ':app'