diff --git a/app/build.gradle b/app/build.gradle
index 72091c9..6a307f5 100755
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -21,9 +21,11 @@ android {
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
+ implementation 'com.android.support:support-v4:27.0.2'
testImplementation 'junit:junit:4.12'
def supportLibVersion = "27.0.2"
implementation "com.android.support:appcompat-v7:$supportLibVersion"
implementation "com.android.support:support-v4:$supportLibVersion"
+ implementation 'com.android.support:design:27.0.2'
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index e343f86..1f26448 100755
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,6 +1,8 @@
+ package="de.rumpold.androiddsky">
+
+
-
+
-
+
+
+
-
\ No newline at end of file
diff --git a/app/src/main/java/de/rumpold/androiddsky/DSKY.java b/app/src/main/java/de/rumpold/androiddsky/DSKY.java
index 991c8d4..3cc62c8 100755
--- a/app/src/main/java/de/rumpold/androiddsky/DSKY.java
+++ b/app/src/main/java/de/rumpold/androiddsky/DSKY.java
@@ -1,9 +1,13 @@
package de.rumpold.androiddsky;
+import android.content.SharedPreferences;
import android.os.AsyncTask;
+import android.preference.PreferenceManager;
import android.util.Log;
import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
@@ -72,7 +76,18 @@ public class DSKY {
public DSKY(DSKYActivity activity) {
this.activity = activity;
- this.client = new YaAgcClient(this);
+
+ final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity.getApplicationContext());
+ final String agcHost = preferences.getString("agc_host", null);
+ final int agcPort = Integer.parseInt(preferences.getString("agc_port", "-1"));
+
+ if (agcHost == null || agcPort <= 0) {
+ this.client = null;
+ return;
+ } else {
+ final SocketAddress agcAddress = new InetSocketAddress(agcHost, agcPort);
+ this.client = new YaAgcClient(this, agcAddress);
+ }
Arrays.fill(register1, ' ');
Arrays.fill(register2, ' ');
@@ -88,9 +103,11 @@ public class DSKY {
@Override
public void run() {
try {
- client.connect();
+ if (client != null) {
+ client.connect();
+ }
} catch (IOException e) {
- throw new RuntimeException("Cannot connect to yaAGC", e);
+// throw new RuntimeException("Cannot connect to yaAGC", e);
}
}
});
@@ -134,7 +151,9 @@ public class DSKY {
@Override
public void run() {
try {
- client.disconnect();
+ if (client != null) {
+ client.disconnect();
+ }
} catch (IOException e) {
throw new RuntimeException("Cannot connect to yaAGC", e);
}
@@ -253,6 +272,9 @@ public class DSKY {
public void buttonPressed(final String button) {
Log.d(TAG, "buttonPressed: " + button);
+ if (client == null) {
+ return;
+ }
AsyncTask.execute(new Runnable() {
@Override
public void run() {
@@ -265,4 +287,8 @@ public class DSKY {
}
});
}
+
+ public boolean isConnected() {
+ return client != null && client.isConnected();
+ }
}
diff --git a/app/src/main/java/de/rumpold/androiddsky/network/YaAgcClient.java b/app/src/main/java/de/rumpold/androiddsky/network/YaAgcClient.java
index 38e31dc..4f736b6 100755
--- a/app/src/main/java/de/rumpold/androiddsky/network/YaAgcClient.java
+++ b/app/src/main/java/de/rumpold/androiddsky/network/YaAgcClient.java
@@ -1,11 +1,14 @@
package de.rumpold.androiddsky.network;
+import android.os.Handler;
+import android.os.Looper;
import android.util.Log;
import android.widget.Toast;
import java.io.IOException;
-import java.net.InetSocketAddress;
+import java.net.SocketAddress;
import java.nio.ByteBuffer;
+import java.nio.channels.ClosedByInterruptException;
import java.nio.channels.SocketChannel;
import de.rumpold.androiddsky.DSKY;
@@ -15,13 +18,12 @@ import de.rumpold.androiddsky.DSKY;
*/
@SuppressWarnings("OctalInteger")
public class YaAgcClient {
- private static final String YAAGC_HOST = "192.168.178.29";
- private static final int YAAGC_SERVER_PORT = 19697;
private static final String TAG = YaAgcClient.class.getSimpleName();
private static final int KEYBOARD_CHANNEL = 015;
private final DSKY dsky;
+ private final SocketAddress agcAddress;
private SocketChannel agcChannel;
private Thread handler;
@@ -54,23 +56,31 @@ public class YaAgcClient {
buf.position(buf.position() + 4);
}
- } catch (IOException | InterruptedException e) {
- Toast.makeText(dsky.getActivity().getApplicationContext(),
- "Connection failure",
- Toast.LENGTH_SHORT).show();
+ } catch (ClosedByInterruptException | InterruptedException ignored) {
+ } catch (IOException e) {
Log.w(TAG, "Connection error", e);
+ new Handler(Looper.getMainLooper()).post(new Runnable() {
+ @Override
+ public void run() {
+ Toast.makeText(dsky.getActivity().getApplicationContext(),
+ "Connection failure",
+ Toast.LENGTH_SHORT).show();
+ }
+ });
}
}
}
}
- public YaAgcClient(DSKY dsky) {
+ public YaAgcClient(DSKY dsky, SocketAddress agcAddress) {
this.dsky = dsky;
+ this.agcAddress = agcAddress;
}
public void connect() throws IOException {
+ Log.i(TAG, "Connecting to yaAGC at " + agcAddress);
agcChannel = SocketChannel.open();
- boolean success = agcChannel.connect(new InetSocketAddress(YAAGC_HOST, YAAGC_SERVER_PORT));
+ boolean success = agcChannel.connect(agcAddress);
if (success) {
Log.i(TAG, "connect: Successfully connected, starting DSKY I/O handler");
@@ -80,6 +90,7 @@ public class YaAgcClient {
}
public void disconnect() throws IOException {
+ Log.i(TAG, "Disconnecting from AGC");
if (handler != null) {
handler.interrupt();
try {
@@ -91,6 +102,10 @@ public class YaAgcClient {
agcChannel.close();
}
+ public boolean isConnected() {
+ return agcChannel.isConnected();
+ }
+
public void sendKeyCode(int keyCode) throws IOException {
sendChannelOutput(KEYBOARD_CHANNEL, keyCode);
}
diff --git a/app/src/main/java/de/rumpold/androiddsky/ui/AppCompatPreferenceActivity.java b/app/src/main/java/de/rumpold/androiddsky/ui/AppCompatPreferenceActivity.java
new file mode 100644
index 0000000..58e9789
--- /dev/null
+++ b/app/src/main/java/de/rumpold/androiddsky/ui/AppCompatPreferenceActivity.java
@@ -0,0 +1,109 @@
+package de.rumpold.androiddsky.ui;
+
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.preference.PreferenceActivity;
+import android.support.annotation.LayoutRes;
+import android.support.annotation.Nullable;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatDelegate;
+import android.support.v7.widget.Toolbar;
+import android.view.MenuInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * A {@link android.preference.PreferenceActivity} which implements and proxies the necessary calls
+ * to be used with AppCompat.
+ */
+public abstract class AppCompatPreferenceActivity extends PreferenceActivity {
+
+ private AppCompatDelegate mDelegate;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ getDelegate().installViewFactory();
+ getDelegate().onCreate(savedInstanceState);
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ protected void onPostCreate(Bundle savedInstanceState) {
+ super.onPostCreate(savedInstanceState);
+ getDelegate().onPostCreate(savedInstanceState);
+ }
+
+ public ActionBar getSupportActionBar() {
+ return getDelegate().getSupportActionBar();
+ }
+
+ public void setSupportActionBar(@Nullable Toolbar toolbar) {
+ getDelegate().setSupportActionBar(toolbar);
+ }
+
+ @Override
+ public MenuInflater getMenuInflater() {
+ return getDelegate().getMenuInflater();
+ }
+
+ @Override
+ public void setContentView(@LayoutRes int layoutResID) {
+ getDelegate().setContentView(layoutResID);
+ }
+
+ @Override
+ public void setContentView(View view) {
+ getDelegate().setContentView(view);
+ }
+
+ @Override
+ public void setContentView(View view, ViewGroup.LayoutParams params) {
+ getDelegate().setContentView(view, params);
+ }
+
+ @Override
+ public void addContentView(View view, ViewGroup.LayoutParams params) {
+ getDelegate().addContentView(view, params);
+ }
+
+ @Override
+ protected void onPostResume() {
+ super.onPostResume();
+ getDelegate().onPostResume();
+ }
+
+ @Override
+ protected void onTitleChanged(CharSequence title, int color) {
+ super.onTitleChanged(title, color);
+ getDelegate().setTitle(title);
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ getDelegate().onConfigurationChanged(newConfig);
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ getDelegate().onStop();
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ getDelegate().onDestroy();
+ }
+
+ public void invalidateOptionsMenu() {
+ getDelegate().invalidateOptionsMenu();
+ }
+
+ private AppCompatDelegate getDelegate() {
+ if (mDelegate == null) {
+ mDelegate = AppCompatDelegate.create(this, null);
+ }
+ return mDelegate;
+ }
+}
diff --git a/app/src/main/java/de/rumpold/androiddsky/ui/DSKYActivity.java b/app/src/main/java/de/rumpold/androiddsky/ui/DSKYActivity.java
index 2415b87..082e378 100755
--- a/app/src/main/java/de/rumpold/androiddsky/ui/DSKYActivity.java
+++ b/app/src/main/java/de/rumpold/androiddsky/ui/DSKYActivity.java
@@ -1,10 +1,14 @@
package de.rumpold.androiddsky.ui;
+import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
+import android.preference.PreferenceManager;
import android.support.annotation.IdRes;
import android.support.v7.app.AppCompatActivity;
import android.view.HapticFeedbackConstants;
+import android.view.Menu;
+import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
@@ -25,7 +29,7 @@ public class DSKYActivity extends AppCompatActivity {
private final Handler mViewHandler = new Handler();
- private final DSKY dsky = new DSKY(this);
+ private DSKY dsky;
private int rightActiveColor;
private int activeColor;
private int passiveColor;
@@ -73,6 +77,8 @@ public class DSKYActivity extends AppCompatActivity {
setContentView(R.layout.activity_dsky);
+ dsky = new DSKY(this);
+
// Colors
rightActiveColor = getResources().getColor(R.color.indicatorRightActive);
activeColor = getResources().getColor(R.color.indicatorActive);
@@ -82,6 +88,33 @@ public class DSKYActivity extends AppCompatActivity {
createButtonListeners();
}
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.action_connect:
+ if (!dsky.isConnected()) {
+ dsky.connect();
+ } else {
+ dsky.disconnect();
+ }
+ return true;
+
+ case R.id.action_settings:
+ final Intent intent = new Intent(this, SettingsActivity.class);
+ startActivity(intent);
+ return true;
+
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.dsky_menu, menu);
+ return super.onCreateOptionsMenu(menu);
+ }
+
private void createButtonListeners() {
final View.OnClickListener buttonListener = new View.OnClickListener() {
@Override
@@ -117,12 +150,17 @@ public class DSKYActivity extends AppCompatActivity {
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
- dsky.connect();
+ final boolean autoConnect = PreferenceManager.getDefaultSharedPreferences(this).getBoolean("auto_connect", false);
+ if (autoConnect) {
+ dsky.connect();
+ }
}
@Override
protected void onDestroy() {
+ if (dsky.isConnected()) {
+ dsky.disconnect();
+ }
super.onDestroy();
- dsky.disconnect();
}
}
diff --git a/app/src/main/java/de/rumpold/androiddsky/ui/SettingsActivity.java b/app/src/main/java/de/rumpold/androiddsky/ui/SettingsActivity.java
new file mode 100644
index 0000000..39748ad
--- /dev/null
+++ b/app/src/main/java/de/rumpold/androiddsky/ui/SettingsActivity.java
@@ -0,0 +1,198 @@
+package de.rumpold.androiddsky.ui;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.Intent;
+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.PreferenceFragment;
+import android.preference.PreferenceManager;
+import android.preference.RingtonePreference;
+import android.support.v7.app.ActionBar;
+import android.text.TextUtils;
+import android.view.MenuItem;
+
+import java.util.List;
+
+import de.rumpold.androiddsky.R;
+
+/**
+ * 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 AppCompatPreferenceActivity {
+
+ /**
+ * 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;
+ }
+ };
+
+ /**
+ * 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;
+ }
+
+ /**
+ * 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) {
+ if (preference == null) {
+ return;
+ }
+
+ // 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(), ""));
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setupActionBar();
+ }
+
+ /**
+ * Set up the {@link android.app.ActionBar}, if the API is available.
+ */
+ private void setupActionBar() {
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ // Show the Up button in the action bar.
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onIsMultiPane() {
+ return isXLargeTablet(this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+ public void onBuildHeaders(List target) {
+ loadHeadersFromResource(R.xml.pref_headers, target);
+ }
+
+ /**
+ * This method stops fragment injection in malicious applications.
+ * Make sure to deny any unknown fragments here.
+ */
+ protected boolean isValidFragment(String fragmentName) {
+ return PreferenceFragment.class.getName().equals(fragmentName)
+ || GeneralPreferenceFragment.class.getName().equals(fragmentName);
+ }
+
+ /**
+ * 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);
+ setHasOptionsMenu(true);
+
+ // 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("agc_host"));
+ bindPreferenceSummaryToValue(findPreference("agc_port"));
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ int id = item.getItemId();
+ if (id == android.R.id.home) {
+ startActivity(new Intent(getActivity(), SettingsActivity.class));
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+ }
+}
diff --git a/app/src/main/res/drawable/ic_info_black_24dp.xml b/app/src/main/res/drawable/ic_info_black_24dp.xml
new file mode 100644
index 0000000..d9c3703
--- /dev/null
+++ b/app/src/main/res/drawable/ic_info_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/layout/activity_dsky.xml b/app/src/main/res/layout/activity_dsky.xml
index 5770f68..f497ce6 100755
--- a/app/src/main/res/layout/activity_dsky.xml
+++ b/app/src/main/res/layout/activity_dsky.xml
@@ -1,4 +1,5 @@
-
+ android:layout_gravity="start|top"/>
+ android:layout_gravity="end|top"/>
+ android:layout_gravity="center_horizontal|bottom"/>
\ No newline at end of file
diff --git a/app/src/main/res/menu/dsky_menu.xml b/app/src/main/res/menu/dsky_menu.xml
new file mode 100644
index 0000000..b5bf09f
--- /dev/null
+++ b/app/src/main/res/menu/dsky_menu.xml
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 9a3436c..9be0d6a 100755
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,3 +1,10 @@
androidDSKY
+
+
+ Settings
+ Auto-connect on startup
+ Automatically connect to remote VirtualAGC when launching the app?
+ VirtualAGC host
+ VirtualAGC port
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 0000000..a5e6ee6
--- /dev/null
+++ b/app/src/main/res/xml/pref_general.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
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 0000000..49594ad
--- /dev/null
+++ b/app/src/main/res/xml/pref_headers.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+