diff --git a/.gitignore b/.gitignore index 9068cd01..fadc061e 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,6 @@ proguard/ # Log Files *.log + +.idea +*.iml diff --git a/README.md b/README.md index 5aee1edb..d7be6776 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 00000000..2b4b5870 --- /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 00000000..dc8ca604 --- /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 00000000..1e1760d3 --- /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 00000000..9816dc3f --- /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 00000000..ac296ff5 --- /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 00000000..bad25cfa --- /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 00000000..aa4f3f99 --- /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 00000000..2395a1fd --- /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 00000000..96a442e5 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 00000000..359047df 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 00000000..71c6d760 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 00000000..4df18946 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 00000000..ff147381 --- /dev/null +++ b/app/src/main/res/layout/activity_controlcenter.xml @@ -0,0 +1,54 @@ + + + + + + +