Better connection handling and initial code for settings activity

This commit is contained in:
Adrian Rumpold
2017-12-21 23:10:15 +01:00
parent 0f3ce17384
commit 7c5f6f4917
13 changed files with 480 additions and 24 deletions

View File

@@ -21,9 +21,11 @@ android {
dependencies { dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.android.support:support-v4:27.0.2'
testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.12'
def supportLibVersion = "27.0.2" def supportLibVersion = "27.0.2"
implementation "com.android.support:appcompat-v7:$supportLibVersion" implementation "com.android.support:appcompat-v7:$supportLibVersion"
implementation "com.android.support:support-v4:$supportLibVersion" implementation "com.android.support:support-v4:$supportLibVersion"
implementation 'com.android.support:design:27.0.2'
} }

View File

@@ -2,6 +2,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="de.rumpold.androiddsky"> package="de.rumpold.androiddsky">
<uses-permission android:name="android.permission.INTERNET"/>
<application <application
android:allowBackup="true" android:allowBackup="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
@@ -19,7 +21,10 @@
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name=".ui.SettingsActivity"
android:label="@string/title_activity_settings">
</activity>
</application> </application>
<uses-permission android:name="android.permission.INTERNET" />
</manifest> </manifest>

View File

@@ -1,9 +1,13 @@
package de.rumpold.androiddsky; package de.rumpold.androiddsky;
import android.content.SharedPreferences;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.preference.PreferenceManager;
import android.util.Log; import android.util.Log;
import java.io.IOException; import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@@ -72,7 +76,18 @@ public class DSKY {
public DSKY(DSKYActivity activity) { public DSKY(DSKYActivity activity) {
this.activity = 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(register1, ' ');
Arrays.fill(register2, ' '); Arrays.fill(register2, ' ');
@@ -88,9 +103,11 @@ public class DSKY {
@Override @Override
public void run() { public void run() {
try { try {
if (client != null) {
client.connect(); client.connect();
}
} catch (IOException e) { } 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 @Override
public void run() { public void run() {
try { try {
if (client != null) {
client.disconnect(); client.disconnect();
}
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException("Cannot connect to yaAGC", e); throw new RuntimeException("Cannot connect to yaAGC", e);
} }
@@ -253,6 +272,9 @@ public class DSKY {
public void buttonPressed(final String button) { public void buttonPressed(final String button) {
Log.d(TAG, "buttonPressed: " + button); Log.d(TAG, "buttonPressed: " + button);
if (client == null) {
return;
}
AsyncTask.execute(new Runnable() { AsyncTask.execute(new Runnable() {
@Override @Override
public void run() { public void run() {
@@ -265,4 +287,8 @@ public class DSKY {
} }
}); });
} }
public boolean isConnected() {
return client != null && client.isConnected();
}
} }

View File

@@ -1,11 +1,14 @@
package de.rumpold.androiddsky.network; package de.rumpold.androiddsky.network;
import android.os.Handler;
import android.os.Looper;
import android.util.Log; import android.util.Log;
import android.widget.Toast; import android.widget.Toast;
import java.io.IOException; import java.io.IOException;
import java.net.InetSocketAddress; import java.net.SocketAddress;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.channels.ClosedByInterruptException;
import java.nio.channels.SocketChannel; import java.nio.channels.SocketChannel;
import de.rumpold.androiddsky.DSKY; import de.rumpold.androiddsky.DSKY;
@@ -15,13 +18,12 @@ import de.rumpold.androiddsky.DSKY;
*/ */
@SuppressWarnings("OctalInteger") @SuppressWarnings("OctalInteger")
public class YaAgcClient { 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 String TAG = YaAgcClient.class.getSimpleName();
private static final int KEYBOARD_CHANNEL = 015; private static final int KEYBOARD_CHANNEL = 015;
private final DSKY dsky; private final DSKY dsky;
private final SocketAddress agcAddress;
private SocketChannel agcChannel; private SocketChannel agcChannel;
private Thread handler; private Thread handler;
@@ -54,23 +56,31 @@ public class YaAgcClient {
buf.position(buf.position() + 4); buf.position(buf.position() + 4);
} }
} catch (IOException | InterruptedException e) { } 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(), Toast.makeText(dsky.getActivity().getApplicationContext(),
"Connection failure", "Connection failure",
Toast.LENGTH_SHORT).show(); Toast.LENGTH_SHORT).show();
Log.w(TAG, "Connection error", e); }
});
} }
} }
} }
} }
public YaAgcClient(DSKY dsky) { public YaAgcClient(DSKY dsky, SocketAddress agcAddress) {
this.dsky = dsky; this.dsky = dsky;
this.agcAddress = agcAddress;
} }
public void connect() throws IOException { public void connect() throws IOException {
Log.i(TAG, "Connecting to yaAGC at " + agcAddress);
agcChannel = SocketChannel.open(); agcChannel = SocketChannel.open();
boolean success = agcChannel.connect(new InetSocketAddress(YAAGC_HOST, YAAGC_SERVER_PORT)); boolean success = agcChannel.connect(agcAddress);
if (success) { if (success) {
Log.i(TAG, "connect: Successfully connected, starting DSKY I/O handler"); Log.i(TAG, "connect: Successfully connected, starting DSKY I/O handler");
@@ -80,6 +90,7 @@ public class YaAgcClient {
} }
public void disconnect() throws IOException { public void disconnect() throws IOException {
Log.i(TAG, "Disconnecting from AGC");
if (handler != null) { if (handler != null) {
handler.interrupt(); handler.interrupt();
try { try {
@@ -91,6 +102,10 @@ public class YaAgcClient {
agcChannel.close(); agcChannel.close();
} }
public boolean isConnected() {
return agcChannel.isConnected();
}
public void sendKeyCode(int keyCode) throws IOException { public void sendKeyCode(int keyCode) throws IOException {
sendChannelOutput(KEYBOARD_CHANNEL, keyCode); sendChannelOutput(KEYBOARD_CHANNEL, keyCode);
} }

View File

@@ -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;
}
}

View File

@@ -1,10 +1,14 @@
package de.rumpold.androiddsky.ui; package de.rumpold.androiddsky.ui;
import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.preference.PreferenceManager;
import android.support.annotation.IdRes; import android.support.annotation.IdRes;
import android.support.v7.app.AppCompatActivity; import android.support.v7.app.AppCompatActivity;
import android.view.HapticFeedbackConstants; import android.view.HapticFeedbackConstants;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.widget.Button; import android.widget.Button;
import android.widget.TextView; import android.widget.TextView;
@@ -25,7 +29,7 @@ public class DSKYActivity extends AppCompatActivity {
private final Handler mViewHandler = new Handler(); private final Handler mViewHandler = new Handler();
private final DSKY dsky = new DSKY(this); private DSKY dsky;
private int rightActiveColor; private int rightActiveColor;
private int activeColor; private int activeColor;
private int passiveColor; private int passiveColor;
@@ -73,6 +77,8 @@ public class DSKYActivity extends AppCompatActivity {
setContentView(R.layout.activity_dsky); setContentView(R.layout.activity_dsky);
dsky = new DSKY(this);
// Colors // Colors
rightActiveColor = getResources().getColor(R.color.indicatorRightActive); rightActiveColor = getResources().getColor(R.color.indicatorRightActive);
activeColor = getResources().getColor(R.color.indicatorActive); activeColor = getResources().getColor(R.color.indicatorActive);
@@ -82,6 +88,33 @@ public class DSKYActivity extends AppCompatActivity {
createButtonListeners(); 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() { private void createButtonListeners() {
final View.OnClickListener buttonListener = new View.OnClickListener() { final View.OnClickListener buttonListener = new View.OnClickListener() {
@Override @Override
@@ -117,12 +150,17 @@ public class DSKYActivity extends AppCompatActivity {
@Override @Override
protected void onPostCreate(Bundle savedInstanceState) { protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState); super.onPostCreate(savedInstanceState);
final boolean autoConnect = PreferenceManager.getDefaultSharedPreferences(this).getBoolean("auto_connect", false);
if (autoConnect) {
dsky.connect(); dsky.connect();
} }
}
@Override @Override
protected void onDestroy() { protected void onDestroy() {
super.onDestroy(); if (dsky.isConnected()) {
dsky.disconnect(); dsky.disconnect();
} }
super.onDestroy();
}
} }

View File

@@ -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.
* <p>
* See <a href="http://developer.android.com/design/patterns/settings.html">
* Android Design: Settings</a> for design guidelines and the <a
* href="http://developer.android.com/guide/topics/ui/settings.html">Settings
* API Guide</a> 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<Header> 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);
}
}
}

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zm1,15h-2v-6h2v6zm0,-8h-2V7h2v2z"/>
</vector>

View File

@@ -1,4 +1,5 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="match_parent"

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_connect"
android:title="Connect"
app:showAsAction="ifRoom"/>
<item
android:id="@+id/action_settings"
android:title="Settings"
app:showAsAction="never"/>
</menu>

View File

@@ -1,3 +1,10 @@
<resources> <resources>
<string name="app_name">androidDSKY</string> <string name="app_name">androidDSKY</string>
<!-- Strings related to Settings -->
<string name="title_activity_settings">Settings</string>
<string name="pref_title_auto_connect">Auto-connect on startup</string>
<string name="pref_description_auto_connect">Automatically connect to remote VirtualAGC when launching the app?</string>
<string name="pref_title_agc_host">VirtualAGC host</string>
<string name="pref_title_agc_port">VirtualAGC port</string>
</resources> </resources>

View File

@@ -0,0 +1,24 @@
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<SwitchPreference
android:defaultValue="true"
android:key="auto_connect"
android:summary="@string/pref_description_auto_connect"
android:title="@string/pref_title_auto_connect"/>
<!-- NOTE: EditTextPreference accepts EditText attributes. -->
<!-- NOTE: EditTextPreference's summary should be set to its value by the activity code. -->
<EditTextPreference
android:inputType="textUri"
android:key="agc_host"
android:maxLines="1"
android:selectAllOnFocus="true"
android:singleLine="true"
android:title="@string/pref_title_agc_host"/>
<EditTextPreference
android:defaultValue="19697"
android:key="agc_port"
android:selectAllOnFocus="true"
android:singleLine="true"
android:title="@string/pref_title_agc_port"/>
</PreferenceScreen>

View File

@@ -0,0 +1,10 @@
<preference-headers xmlns:android="http://schemas.android.com/apk/res/android">
<!-- These settings headers are only used on tablets. -->
<header
android:fragment="de.rumpold.androiddsky.ui.SettingsActivity$GeneralPreferenceFragment"
android:icon="@drawable/ic_info_black_24dp"
android:title="@string/pref_header_general"/>
</preference-headers>