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 @@ + + + + +
+ +