summaryrefslogtreecommitdiff
path: root/android/VulpIRC
diff options
context:
space:
mode:
authorTreeki <treeki@gmail.com>2014-02-18 02:54:06 +0100
committerTreeki <treeki@gmail.com>2014-02-18 02:54:06 +0100
commitd57224505be243c80e9dd64e00f0a7c3a1d841f0 (patch)
treefe2b7d5cc9b7ca993b50f526f73bcb8716c0a60e /android/VulpIRC
parent05568c427eff856d3049d4a18a707f1b0b358bd2 (diff)
downloadbounce4-d57224505be243c80e9dd64e00f0a7c3a1d841f0.tar.gz
bounce4-d57224505be243c80e9dd64e00f0a7c3a1d841f0.zip
add Android client
Diffstat (limited to 'android/VulpIRC')
-rw-r--r--android/VulpIRC/.gitignore1
-rw-r--r--android/VulpIRC/VulpIRC.iml76
-rw-r--r--android/VulpIRC/build.gradle30
-rw-r--r--android/VulpIRC/src/main/AndroidManifest.xml51
-rw-r--r--android/VulpIRC/src/main/ic_launcher-web.pngbin0 -> 47065 bytes
-rw-r--r--android/VulpIRC/src/main/java/net/brokenfox/vulpirc/BaseConn.java611
-rw-r--r--android/VulpIRC/src/main/java/net/brokenfox/vulpirc/ChannelData.java116
-rw-r--r--android/VulpIRC/src/main/java/net/brokenfox/vulpirc/ChannelFragment.java22
-rw-r--r--android/VulpIRC/src/main/java/net/brokenfox/vulpirc/Connection.java240
-rw-r--r--android/VulpIRC/src/main/java/net/brokenfox/vulpirc/IRCService.java131
-rw-r--r--android/VulpIRC/src/main/java/net/brokenfox/vulpirc/LoginActivity.java263
-rw-r--r--android/VulpIRC/src/main/java/net/brokenfox/vulpirc/MainActivity.java340
-rw-r--r--android/VulpIRC/src/main/java/net/brokenfox/vulpirc/RichText.java236
-rw-r--r--android/VulpIRC/src/main/java/net/brokenfox/vulpirc/Util.java62
-rw-r--r--android/VulpIRC/src/main/java/net/brokenfox/vulpirc/WindowData.java106
-rw-r--r--android/VulpIRC/src/main/java/net/brokenfox/vulpirc/WindowFragment.java149
-rw-r--r--android/VulpIRC/src/main/res/drawable-hdpi/drawer_shadow.9.pngbin0 -> 161 bytes
-rw-r--r--android/VulpIRC/src/main/res/drawable-hdpi/ic_drawer.pngbin0 -> 2826 bytes
-rw-r--r--android/VulpIRC/src/main/res/drawable-hdpi/ic_launcher.pngbin0 -> 7721 bytes
-rw-r--r--android/VulpIRC/src/main/res/drawable-mdpi/drawer_shadow.9.pngbin0 -> 142 bytes
-rw-r--r--android/VulpIRC/src/main/res/drawable-mdpi/ic_drawer.pngbin0 -> 2816 bytes
-rw-r--r--android/VulpIRC/src/main/res/drawable-mdpi/ic_launcher.pngbin0 -> 3769 bytes
-rw-r--r--android/VulpIRC/src/main/res/drawable-xhdpi/drawer_shadow.9.pngbin0 -> 174 bytes
-rw-r--r--android/VulpIRC/src/main/res/drawable-xhdpi/ic_drawer.pngbin0 -> 1038 bytes
-rw-r--r--android/VulpIRC/src/main/res/drawable-xhdpi/ic_launcher.pngbin0 -> 12329 bytes
-rw-r--r--android/VulpIRC/src/main/res/drawable-xxhdpi/drawer_shadow.9.pngbin0 -> 208 bytes
-rw-r--r--android/VulpIRC/src/main/res/drawable-xxhdpi/ic_drawer.pngbin0 -> 202 bytes
-rw-r--r--android/VulpIRC/src/main/res/drawable-xxhdpi/ic_launcher.pngbin0 -> 24654 bytes
-rw-r--r--android/VulpIRC/src/main/res/drawable/window_status.xml10
-rw-r--r--android/VulpIRC/src/main/res/layout/activity_login.xml111
-rw-r--r--android/VulpIRC/src/main/res/layout/activity_main.xml40
-rw-r--r--android/VulpIRC/src/main/res/layout/window_list_entry.xml23
-rw-r--r--android/VulpIRC/src/main/res/menu/login.xml2
-rw-r--r--android/VulpIRC/src/main/res/menu/main.xml12
-rw-r--r--android/VulpIRC/src/main/res/values-w820dp/dimens.xml6
-rw-r--r--android/VulpIRC/src/main/res/values/dimens.xml10
-rw-r--r--android/VulpIRC/src/main/res/values/strings.xml10
-rw-r--r--android/VulpIRC/src/main/res/values/styles.xml9
38 files changed, 2667 insertions, 0 deletions
diff --git a/android/VulpIRC/.gitignore b/android/VulpIRC/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/android/VulpIRC/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/android/VulpIRC/VulpIRC.iml b/android/VulpIRC/VulpIRC.iml
new file mode 100644
index 0000000..6d04e6c
--- /dev/null
+++ b/android/VulpIRC/VulpIRC.iml
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$/.." external.system.id="GRADLE" external.system.module.group="android" external.system.module.version="unspecified" type="JAVA_MODULE" version="4">
+ <component name="FacetManager">
+ <facet type="android" name="Android">
+ <configuration>
+ <option name="SELECTED_BUILD_VARIANT" value="debug" />
+ <option name="ASSEMBLE_TASK_NAME" value="assembleDebug" />
+ <option name="COMPILE_JAVA_TASK_NAME" value="compileDebugJava" />
+ <option name="ASSEMBLE_TEST_TASK_NAME" value="assembleDebugTest" />
+ <option name="SOURCE_GEN_TASK_NAME" value="generateDebugSources" />
+ <option name="ALLOW_USER_CONFIGURATION" value="false" />
+ <option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" />
+ <option name="RES_FOLDER_RELATIVE_PATH" value="/src/main/res" />
+ <option name="RES_FOLDERS_RELATIVE_PATH" value="file://$MODULE_DIR$/src/main/res" />
+ <option name="ASSETS_FOLDER_RELATIVE_PATH" value="/src/main/assets" />
+ </configuration>
+ </facet>
+ <facet type="android-gradle" name="Android-Gradle">
+ <configuration>
+ <option name="GRADLE_PROJECT_PATH" value=":VulpIRC" />
+ </configuration>
+ </facet>
+ </component>
+ <component name="NewModuleRootManager" inherit-compiler-output="false">
+ <output url="file://$MODULE_DIR$/build/classes/debug" />
+ <exclude-output />
+ <content url="file://$MODULE_DIR$">
+ <sourceFolder url="file://$MODULE_DIR$/build/source/r/debug" isTestSource="false" generated="true" />
+ <sourceFolder url="file://$MODULE_DIR$/build/source/aidl/debug" isTestSource="false" generated="true" />
+ <sourceFolder url="file://$MODULE_DIR$/build/source/buildConfig/debug" isTestSource="false" generated="true" />
+ <sourceFolder url="file://$MODULE_DIR$/build/source/rs/debug" isTestSource="false" generated="true" />
+ <sourceFolder url="file://$MODULE_DIR$/build/res/rs/debug" type="java-resource" />
+ <sourceFolder url="file://$MODULE_DIR$/build/source/r/test/debug" isTestSource="true" />
+ <sourceFolder url="file://$MODULE_DIR$/build/source/aidl/test/debug" isTestSource="true" />
+ <sourceFolder url="file://$MODULE_DIR$/build/source/buildConfig/test/debug" isTestSource="true" />
+ <sourceFolder url="file://$MODULE_DIR$/build/source/rs/test/debug" isTestSource="true" />
+ <sourceFolder url="file://$MODULE_DIR$/build/res/rs/test/debug" type="java-test-resource" />
+ <sourceFolder url="file://$MODULE_DIR$/src/debug/aidl" isTestSource="false" />
+ <sourceFolder url="file://$MODULE_DIR$/src/debug/assets" isTestSource="false" />
+ <sourceFolder url="file://$MODULE_DIR$/src/debug/java" isTestSource="false" />
+ <sourceFolder url="file://$MODULE_DIR$/src/debug/jni" isTestSource="false" />
+ <sourceFolder url="file://$MODULE_DIR$/src/debug/rs" isTestSource="false" />
+ <sourceFolder url="file://$MODULE_DIR$/src/debug/res" type="java-resource" />
+ <sourceFolder url="file://$MODULE_DIR$/src/debug/resources" type="java-resource" />
+ <sourceFolder url="file://$MODULE_DIR$/src/main/aidl" isTestSource="false" />
+ <sourceFolder url="file://$MODULE_DIR$/src/main/assets" isTestSource="false" />
+ <sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
+ <sourceFolder url="file://$MODULE_DIR$/src/main/jni" isTestSource="false" />
+ <sourceFolder url="file://$MODULE_DIR$/src/main/rs" isTestSource="false" />
+ <sourceFolder url="file://$MODULE_DIR$/src/main/res" type="java-resource" />
+ <sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
+ <sourceFolder url="file://$MODULE_DIR$/src/instrumentTest/aidl" isTestSource="true" />
+ <sourceFolder url="file://$MODULE_DIR$/src/instrumentTest/assets" isTestSource="true" />
+ <sourceFolder url="file://$MODULE_DIR$/src/instrumentTest/java" isTestSource="true" />
+ <sourceFolder url="file://$MODULE_DIR$/src/instrumentTest/jni" isTestSource="true" />
+ <sourceFolder url="file://$MODULE_DIR$/src/instrumentTest/rs" isTestSource="true" />
+ <sourceFolder url="file://$MODULE_DIR$/src/instrumentTest/res" type="java-test-resource" />
+ <sourceFolder url="file://$MODULE_DIR$/src/instrumentTest/resources" type="java-test-resource" />
+ <excludeFolder url="file://$MODULE_DIR$/build/apk" />
+ <excludeFolder url="file://$MODULE_DIR$/build/assets" />
+ <excludeFolder url="file://$MODULE_DIR$/build/bundles" />
+ <excludeFolder url="file://$MODULE_DIR$/build/classes" />
+ <excludeFolder url="file://$MODULE_DIR$/build/dependency-cache" />
+ <excludeFolder url="file://$MODULE_DIR$/build/incremental" />
+ <excludeFolder url="file://$MODULE_DIR$/build/libs" />
+ <excludeFolder url="file://$MODULE_DIR$/build/manifests" />
+ <excludeFolder url="file://$MODULE_DIR$/build/res" />
+ <excludeFolder url="file://$MODULE_DIR$/build/symbols" />
+ <excludeFolder url="file://$MODULE_DIR$/build/tmp" />
+ </content>
+ <orderEntry type="jdk" jdkName="Android API 19 Platform" jdkType="Android SDK" />
+ <orderEntry type="sourceFolder" forTests="false" />
+ <orderEntry type="library" exported="" name="support-v4-18.0.0" level="project" />
+ </component>
+</module>
+
diff --git a/android/VulpIRC/build.gradle b/android/VulpIRC/build.gradle
new file mode 100644
index 0000000..0e1ac13
--- /dev/null
+++ b/android/VulpIRC/build.gradle
@@ -0,0 +1,30 @@
+buildscript {
+ repositories {
+ mavenCentral()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:0.7.+'
+ }
+}
+apply plugin: 'android'
+
+repositories {
+ mavenCentral()
+}
+
+android {
+ compileSdkVersion 19
+ buildToolsVersion "19.0.1"
+
+ defaultConfig {
+ minSdkVersion 14
+ targetSdkVersion 19
+ }
+}
+
+dependencies {
+
+ // You must install or update the Support Repository through the SDK manager to use this dependency.
+ // The Support Repository (separate from the corresponding library) can be found in the Extras category.
+ compile 'com.android.support:support-v4:18.0.0'
+}
diff --git a/android/VulpIRC/src/main/AndroidManifest.xml b/android/VulpIRC/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..8bfcc36
--- /dev/null
+++ b/android/VulpIRC/src/main/AndroidManifest.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="net.brokenfox.vulpirc"
+ android:versionCode="1"
+ android:versionName="1.0" >
+
+ <uses-sdk
+ android:minSdkVersion="14"
+ android:targetSdkVersion="19" />
+
+ <uses-permission android:name="android.permission.INTERNET" />
+
+ <application
+ android:allowBackup="true"
+ android:icon="@drawable/ic_launcher"
+ android:label="@string/app_name"
+ android:theme="@style/AppTheme" >
+ <activity
+ android:name=".MainActivity"
+ android:launchMode="singleTask"
+ android:label="@string/app_name" >
+ </activity>
+
+ <service android:name="net.brokenfox.vulpirc.IRCService" />
+
+ <activity
+ android:name="net.brokenfox.vulpirc.LoginActivity"
+ android:launchMode="singleTask"
+ android:label="VulpIRC"
+ android:windowSoftInputMode="adjustResize|stateVisible" >
+
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+
+ <category android:name="android.intent.category.LAUNCHER" />
+ <category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
+ </intent-filter>
+ </activity>
+
+ <uses-library
+ android:required="false"
+ android:name="com.sec.android.app.multiwindow">
+ </uses-library>
+ <meta-data android:name="com.sec.android.support.multiwindow" android:value="true" />
+ <meta-data android:name="com.sec.android.multiwindow.DEFAULT_SIZE_W" android:resource="@dimen/app_defaultsize_w" />
+ <meta-data android:name="com.sec.android.multiwindow.DEFAULT_SIZE_H" android:resource="@dimen/app_defaultsize_h" />
+ <meta-data android:name="com.sec.android.multiwindow.MINIMUM_SIZE_W" android:resource="@dimen/app_minimumsize_w" />
+ <meta-data android:name="com.sec.android.multiwindow.MINIMUM_SIZE_H" android:resource="@dimen/app_minimumsize_h" />
+ </application>
+
+</manifest>
diff --git a/android/VulpIRC/src/main/ic_launcher-web.png b/android/VulpIRC/src/main/ic_launcher-web.png
new file mode 100644
index 0000000..4b14629
--- /dev/null
+++ b/android/VulpIRC/src/main/ic_launcher-web.png
Binary files differ
diff --git a/android/VulpIRC/src/main/java/net/brokenfox/vulpirc/BaseConn.java b/android/VulpIRC/src/main/java/net/brokenfox/vulpirc/BaseConn.java
new file mode 100644
index 0000000..5b6ac83
--- /dev/null
+++ b/android/VulpIRC/src/main/java/net/brokenfox/vulpirc/BaseConn.java
@@ -0,0 +1,611 @@
+package net.brokenfox.vulpirc;
+
+import android.os.Handler;
+import android.os.Message;
+import android.util.Log;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.concurrent.LinkedBlockingQueue;
+
+/**
+ * Created by ninji on 1/29/14.
+ */
+public class BaseConn {
+ // Listeners
+ public interface BaseConnListener {
+ void handleSessionStarted();
+ void handleSessionEnded();
+ void handleSocketStateChanged();
+ void handlePacketReceived(int type, byte[] data);
+ void handleLoginError(String error);
+ void handleStatusMessage(String message);
+ }
+
+ private BaseConnListener mListener = null;
+ public void setListener(BaseConnListener l) {
+ mListener = l;
+ }
+
+
+ // Internal communications
+ private final static int MSG_SESSION_STARTED = 100;
+ private final static int MSG_SESSION_ENDED = 101;
+ private final static int MSG_SOCKET_STATE_CHANGED = 102;
+ private final static int MSG_PACKET_RECEIVED = 103;
+ private final static int MSG_LOGIN_ERROR = 104;
+ private final static int MSG_STATUS_MESSAGE = 105;
+ private final static int MSG_TIMED_RECONNECT = 200;
+
+ private final Handler.Callback mHandlerCallback = new Handler.Callback() {
+ @Override
+ public boolean handleMessage(Message message) {
+ switch (message.what) {
+ case MSG_SESSION_STARTED:
+ if (mListener != null)
+ mListener.handleSessionStarted();
+ return true;
+ case MSG_SESSION_ENDED:
+ if (mListener != null)
+ mListener.handleSessionEnded();
+ return true;
+ case MSG_SOCKET_STATE_CHANGED:
+ if (mListener != null)
+ mListener.handleSocketStateChanged();
+ return true;
+ case MSG_PACKET_RECEIVED:
+ if (mListener != null)
+ mListener.handlePacketReceived(message.arg1, (byte[]) message.obj);
+ return true;
+ case MSG_LOGIN_ERROR:
+ if (mListener != null)
+ mListener.handleLoginError((String)message.obj);
+ return true;
+ case MSG_STATUS_MESSAGE:
+ if (mListener != null)
+ mListener.handleStatusMessage((String)message.obj);
+ return true;
+ case MSG_TIMED_RECONNECT:
+ initiateConnection();
+ return true;
+ }
+ return false;
+ }
+ };
+ private final Handler mHandler = new Handler(mHandlerCallback);
+
+
+
+
+ // Packet system
+ public final static int PROTOCOL_VERSION = 1;
+ private final static int SESSION_KEY_SIZE = 16;
+
+ private static class Packet {
+ public int id;
+ public int type;
+ public byte[] data;
+ }
+
+ private final Object mPacketLock = new Object();
+
+ // Anything that touches the following fields must lock on
+ // mPacketLock first..!
+ private final ArrayList<Packet> mPacketCache =
+ new ArrayList<Packet>();
+
+ private byte[] mSessionKey = null;
+ private int mNextPacketID = 1;
+ private int mLastReceivedFromServer = 0;
+ private boolean mSessionActive = false;
+ private boolean mSessionWillTerminate = false;
+
+ public boolean getSessionActive() { return mSessionActive; }
+
+ // NOTE: This executes on the Socket thread, so locking
+ // is absolutely necessary!
+ private void processRawPacket(int packetType, int packetSize, int msgID, int lastReceivedByServer, byte[] buffer, int bufferPos) {
+ Log.i("VulpIRC", "Packet received: " + packetType + ", " + packetSize);
+
+ if ((packetType & 0x8000) == 0) {
+ // For in-band packets, handle the caching junk
+ synchronized (mPacketLock) {
+ clearCachedPackets(lastReceivedByServer);
+
+ if (msgID > mLastReceivedFromServer) {
+ // This is a new packet
+ mLastReceivedFromServer = msgID;
+ } else {
+ // We've already seen this packet, so ignore it
+ return;
+ }
+ }
+
+ // Fire off the packet to the Handler for further
+ // processing by a subclass
+ mHandler.sendMessage(mHandler.obtainMessage(
+ MSG_PACKET_RECEIVED,
+ packetType, 0,
+ Arrays.copyOfRange(buffer, bufferPos, bufferPos + packetSize)));
+ } else {
+ // Out-of-band packets are handled here.
+
+ synchronized (mPacketLock) {
+ switch (packetType) {
+ case 0x8001:
+ // Successful login
+ Log.i("VulpIRC", "*** Successful login. ***");
+ mHandler.sendMessage(
+ mHandler.obtainMessage(MSG_STATUS_MESSAGE,
+ "Logged in successfully."));
+
+ mSessionKey = Arrays.copyOfRange(buffer, bufferPos, bufferPos + packetSize);
+ mLastReceivedFromServer = 0;
+ mNextPacketID = 1;
+ mPacketCache.clear();
+
+ if (mSessionActive)
+ mHandler.sendEmptyMessage(MSG_SESSION_ENDED);
+ mSessionActive = true;
+ mSessionWillTerminate = false;
+ mHandler.sendEmptyMessage(MSG_SESSION_STARTED);
+
+ break;
+
+ case 0x8002:
+ // Login failure. Output this to the UI somewhere?
+ Log.e("VulpIRC", "*** Login failed! ***");
+
+ ByteBuffer b = ByteBuffer.wrap(buffer);
+ b.order(ByteOrder.LITTLE_ENDIAN);
+ b.position(bufferPos);
+ int code = b.getInt();
+
+ mHandler.sendMessage(
+ mHandler.obtainMessage(MSG_LOGIN_ERROR,
+ "Login failure: " + code));
+
+ if (mSessionActive)
+ mHandler.sendEmptyMessage(MSG_SESSION_ENDED);
+ mSessionActive = false;
+
+ requestEndSession();
+
+ break;
+
+ case 0x8003:
+ // Session resumed.
+ Log.i("VulpIRC", "*** Session resumed. ***");
+ mHandler.sendMessage(
+ mHandler.obtainMessage(MSG_STATUS_MESSAGE,
+ "Session resumed."));
+
+ lastReceivedByServer = buffer[bufferPos] |
+ (buffer[bufferPos + 1] << 8) |
+ (buffer[bufferPos + 2] << 16) |
+ (buffer[bufferPos + 3] << 24);
+
+ clearCachedPackets(lastReceivedByServer);
+ for (Packet p : mPacketCache)
+ sendPacketOverWire(p);
+ break;
+ }
+ }
+ }
+ }
+
+
+ // This function must only be called while mPacketLock is held!
+ private void clearCachedPackets(int maxID) {
+ while (!mPacketCache.isEmpty()) {
+ if (mPacketCache.get(0).id > maxID)
+ break;
+ else
+ mPacketCache.remove(0);
+ }
+ }
+
+
+ private void sendPacketOverWire(Packet p) {
+ int headerSize = ((p.type & 0x8000) == 0) ? 16 : 8;
+ ByteBuffer b = ByteBuffer.allocate(headerSize + p.data.length);
+
+ b.order(ByteOrder.LITTLE_ENDIAN);
+ b.putShort((short)p.type);
+ b.putShort((short)0);
+ b.putInt(p.data.length);
+
+ if ((p.type & 0x8000) == 0) {
+ b.putInt(p.id);
+ b.putInt(mLastReceivedFromServer);
+ }
+
+ b.put(p.data);
+
+ mWriteQueue.offer(b.array());
+ }
+
+
+ public void sendPacket(int type, byte[] data) {
+ Packet p = new Packet();
+ p.type = type;
+
+ if ((type & 0x8000) == 0) {
+ p.id = mNextPacketID;
+ ++mNextPacketID;
+ }
+
+ p.data = data;
+
+ synchronized (mPacketLock) {
+ if ((type & 0x8000) == 0)
+ mPacketCache.add(p);
+
+ synchronized (mSocketStateLock) {
+ if (mSocketState == SocketState.CONNECTED && mSessionActive)
+ sendPacketOverWire(p);
+ }
+ }
+ }
+
+
+ // Socket junk
+ private Socket mSocket = null;
+ private LinkedBlockingQueue<byte[]> mWriteQueue =
+ new LinkedBlockingQueue<byte[]>();
+
+
+ public enum SocketState {
+ DISCONNECTED, CONNECTING, CONNECTED, DISCONNECTING
+ }
+ // mSocketState can be accessed from multiple threads
+ // Always synchronise on the lock before doing anything with it!
+ private final Object mSocketStateLock = new Object();
+ private SocketState mSocketState = SocketState.DISCONNECTED;
+ public SocketState getSocketState() { return mSocketState; }
+
+ private String mHostname, mUsername, mPassword;
+ private int mPort;
+ private boolean mUseTls;
+
+
+ private class WriterThread implements Runnable {
+ private OutputStream mOutputStream = null;
+ public WriterThread(OutputStream os) {
+ mOutputStream = os;
+ }
+
+ @Override
+ public void run() {
+ // Should I be using a BufferedOutputStream?
+ // Not sure.
+
+ while (true) {
+ try {
+ byte[] data = mWriteQueue.take();
+ mOutputStream.write(data);
+ mOutputStream.flush();
+ } catch (IOException e) {
+ // oops
+ Log.e("VulpIRC", "WriterThread write failure:");
+ Log.e("VulpIRC", e.toString());
+ // Should disconnect here? Maybe?
+ break;
+ } catch (InterruptedException e) {
+ // nothing wrong here
+ break;
+ }
+ }
+ }
+ }
+ private Thread mSocketThread = null;
+
+ private class SocketThread implements Runnable {
+ @Override
+ public void run() {
+ synchronized (mSocketStateLock) {
+ mSocketState = SocketState.CONNECTING;
+ mHandler.sendEmptyMessage(MSG_SOCKET_STATE_CHANGED);
+ }
+
+ Log.i("VulpIRC", "SocketThread running");
+
+ // No SSL just yet, let's simplify things for now
+
+ mSocket = new Socket();
+ try {
+ mHandler.sendMessage(
+ mHandler.obtainMessage(MSG_STATUS_MESSAGE,
+ "Connecting..."));
+
+ mSocket.connect(new InetSocketAddress(mHostname, mPort));
+ } catch (IOException e) {
+ mHandler.sendMessage(
+ mHandler.obtainMessage(MSG_STATUS_MESSAGE,
+ "Connection failure (1): " + e.toString()));
+
+ mHandler.sendMessage(mHandler.obtainMessage(MSG_LOGIN_ERROR, e.toString()));
+ cleanUpConnection();
+ return;
+ }
+
+ // We're connected, yay
+ Log.i("VulpIRC", "Connected!");
+
+ InputStream inputStream;
+ OutputStream outputStream;
+
+ try {
+ inputStream = mSocket.getInputStream();
+ } catch (IOException e) {
+ mHandler.sendMessage(
+ mHandler.obtainMessage(MSG_STATUS_MESSAGE,
+ "Connection failure (2): " + e.toString()));
+
+ mHandler.sendMessage(mHandler.obtainMessage(MSG_LOGIN_ERROR, e.toString()));
+ cleanUpConnection();
+ return;
+ }
+
+ try {
+ outputStream = mSocket.getOutputStream();
+ } catch (IOException e) {
+ mHandler.sendMessage(
+ mHandler.obtainMessage(MSG_STATUS_MESSAGE,
+ "Connection failure (3): " + e.toString()));
+
+ mHandler.sendMessage(mHandler.obtainMessage(MSG_LOGIN_ERROR, e.toString()));
+ cleanUpConnection();
+ return;
+ }
+
+ // Start up our writer
+ mWriteQueue.clear();
+
+ Thread writerThread = new Thread(new WriterThread(outputStream));
+ writerThread.start();
+
+ // Send the initial login packet
+ byte[] encPW = Util.encodeString(mPassword);
+
+ ByteBuffer loginData = ByteBuffer.allocate(12 + encPW.length + SESSION_KEY_SIZE);
+ loginData.order(ByteOrder.LITTLE_ENDIAN);
+ loginData.putInt(PROTOCOL_VERSION);
+ loginData.putInt(mLastReceivedFromServer);
+ loginData.putInt(encPW.length);
+ loginData.put(encPW);
+
+ if (mSessionKey == null) {
+ for (int i = 0; i < SESSION_KEY_SIZE; i++)
+ loginData.put((byte)0);
+ } else {
+ loginData.put(mSessionKey);
+ }
+
+ Packet loginPack = new Packet();
+ loginPack.type = 0x8001;
+ loginPack.data = loginData.array();
+ sendPacketOverWire(loginPack);
+
+ // Let's go !
+ synchronized (mSocketStateLock) {
+ mSocketState = SocketState.CONNECTED;
+ mHandler.sendEmptyMessage(MSG_SOCKET_STATE_CHANGED);
+ }
+
+ Log.i("VulpIRC", "Beginning socket read");
+
+ byte[] readBuffer = new byte[16384];
+ int readBufSize = 0;
+
+ while (true) {
+ // Do we have enough space to (try and) read 8k?
+ // If not, make the buffer bigger
+ if ((readBufSize + 8192) > readBuffer.length)
+ readBuffer = Arrays.copyOf(readBuffer, readBuffer.length + 16384);
+
+ // OK, now we should have enough!
+ try {
+ int amount = inputStream.read(readBuffer, readBufSize, 8192);
+ readBufSize += amount;
+
+ if (amount == -1)
+ break;
+ } catch (IOException e) {
+ mHandler.sendMessage(
+ mHandler.obtainMessage(MSG_STATUS_MESSAGE,
+ "Connection lost (4): " + e.toString()));
+
+ break;
+ }
+
+ // Try and parse as many packets as we can!
+ int pos = 0;
+
+ while (true) {
+ // Do we have enough to parse a packet header?
+ if ((readBufSize - pos) < 8)
+ break;
+
+ int headerSize = 8;
+ int packetType = (0xFF & (int)readBuffer[pos]) |
+ ((0xFF & readBuffer[pos + 1]) << 8);
+ int packetSize = (0xFF & (int)readBuffer[pos + 4]) |
+ ((0xFF & (int)readBuffer[pos + 5]) << 8) |
+ ((0xFF & (int)readBuffer[pos + 6]) << 16) |
+ ((0xFF & (int)readBuffer[pos + 7]) << 24);
+
+ int msgID = 0, lastReceivedByServer = 0;
+
+ // In-band packets have extra stuff
+ if ((packetType & 0x8000) == 0) {
+ if ((readBufSize - pos) < 16)
+ break;
+
+ headerSize = 16;
+
+ msgID = (0xFF & (int)readBuffer[pos + 8]) |
+ ((0xFF & (int)readBuffer[pos + 9]) << 8) |
+ ((0xFF & (int)readBuffer[pos + 10]) << 16) |
+ ((0xFF & (int)readBuffer[pos + 11]) << 24);
+ lastReceivedByServer = (0xFF & (int)readBuffer[pos + 12]) |
+ ((0xFF & (int)readBuffer[pos + 13]) << 8) |
+ ((0xFF & (int)readBuffer[pos + 14]) << 16) |
+ ((0xFF & (int)readBuffer[pos + 15]) << 24);
+ }
+
+ // Negative packet sizes aren't right
+ if (packetSize < 0)
+ break;
+
+ // Enough data?
+ if ((readBufSize - pos) < (packetSize + headerSize))
+ break;
+
+ // OK, this should mean we can parse things now!
+ processRawPacket(
+ packetType, packetSize,
+ msgID, lastReceivedByServer,
+ readBuffer, pos + headerSize);
+
+ pos += headerSize + packetSize;
+ }
+
+ if (pos > 0) {
+ if (pos >= readBufSize) {
+ // We've read everything, no copying needed, just wipe it all
+ readBufSize = 0;
+ } else {
+ // Move the remainder to the beginning of the buffer
+ System.arraycopy(readBuffer, pos, readBuffer, 0, readBufSize - pos);
+ readBufSize -= pos;
+ }
+ }
+ }
+
+ synchronized (mSocketStateLock) {
+ mSocketState = SocketState.DISCONNECTING;
+ mHandler.sendEmptyMessage(MSG_SOCKET_STATE_CHANGED);
+ }
+
+ // Clean up everything
+ writerThread.interrupt();
+ cleanUpConnection();
+ }
+
+ private void cleanUpConnection() {
+ if (mSocket != null) {
+ try {
+ mSocket.close();
+ } catch (IOException e) {
+ // don't care
+ }
+ mSocket = null;
+ }
+
+ synchronized (mSocketStateLock) {
+ synchronized (mPacketLock) {
+ if (mSessionWillTerminate) {
+ mHandler.sendMessage(
+ mHandler.obtainMessage(MSG_STATUS_MESSAGE,
+ "Session completed."));
+
+ mSessionWillTerminate = false;
+ mSessionKey = null;
+ if (mSessionActive)
+ mHandler.sendEmptyMessage(MSG_SESSION_ENDED);
+ mSessionActive = false;
+ mHandler.removeMessages(MSG_TIMED_RECONNECT);
+ } else if (mSessionActive) {
+ mHandler.sendMessage(
+ mHandler.obtainMessage(MSG_STATUS_MESSAGE,
+ "Connection closed. Reconnecting in 15 seconds."));
+ mHandler.sendEmptyMessageDelayed(MSG_TIMED_RECONNECT, 15 * 1000);
+ }
+ }
+
+ mSocketState = SocketState.DISCONNECTED;
+ mSocketThread = null;
+ mHandler.sendEmptyMessage(MSG_SOCKET_STATE_CHANGED);
+ }
+
+ Log.i("VulpIRC", "Connection closed.");
+ }
+ }
+
+
+ public boolean initiateConnection(String hostname, int port, boolean useTls, String username, String password) {
+ synchronized (mSocketStateLock) {
+ if (mSocketState != SocketState.DISCONNECTED)
+ return false;
+
+ mHostname = hostname;
+ mPort = port;
+ mUseTls = useTls;
+ mUsername = username;
+ mPassword = password;
+
+ return initiateConnection();
+ }
+ }
+
+ public boolean initiateConnection() {
+ synchronized (mSocketStateLock) {
+ if (mSocketState != SocketState.DISCONNECTED)
+ return false;
+
+ mSocketState = SocketState.CONNECTING;
+ mHandler.sendEmptyMessage(MSG_SOCKET_STATE_CHANGED);
+
+ mSocketThread = new Thread(new SocketThread());
+ mSocketThread.start();
+
+ return true;
+ }
+ }
+
+ public boolean requestDisconnection() {
+ synchronized (mSocketStateLock) {
+ if (mSocketState != SocketState.DISCONNECTED) {
+ try {
+ if (mSocket != null)
+ mSocket.close();
+ } catch (IOException e) {
+ Log.w("VulpIRC", "requestDisconnection could not close socket:");
+ Log.w("VulpIRC", e.toString());
+ }
+
+ mSocketThread.interrupt();
+ return true;
+ }
+ return false;
+ }
+ }
+ public boolean requestEndSession() {
+ // Should send an OOB logout packet to the server too, if possible.
+
+ synchronized (mSocketStateLock) {
+ mHandler.removeMessages(MSG_TIMED_RECONNECT);
+
+ synchronized (mPacketLock) {
+ if (mSocketState == SocketState.DISCONNECTED) {
+ if (mSessionActive) {
+ mSessionActive = false;
+ mHandler.sendEmptyMessage(MSG_SESSION_ENDED);
+ }
+ return true;
+ } else {
+ mSessionWillTerminate = true;
+ return requestDisconnection();
+ }
+ }
+ }
+ }
+}
diff --git a/android/VulpIRC/src/main/java/net/brokenfox/vulpirc/ChannelData.java b/android/VulpIRC/src/main/java/net/brokenfox/vulpirc/ChannelData.java
new file mode 100644
index 0000000..d98b261
--- /dev/null
+++ b/android/VulpIRC/src/main/java/net/brokenfox/vulpirc/ChannelData.java
@@ -0,0 +1,116 @@
+package net.brokenfox.vulpirc;
+
+import android.support.v4.app.Fragment;
+
+import java.nio.ByteBuffer;
+import java.util.HashMap;
+
+/**
+ * Created by ninji on 2/6/14.
+ */
+public class ChannelData extends WindowData {
+ public static class UserData {
+ public int id;
+ public int modes;
+ public char prefix;
+ }
+ public HashMap<String, UserData> users = new HashMap<String, UserData>();
+ public String topic;
+
+ public interface ChannelListener extends WindowListener {
+ void handleUsersChanged();
+ }
+
+ private int mNextUserID = 1;
+
+
+ @Override
+ protected Fragment instantiateFragmentClass() {
+ return new ChannelFragment();
+ }
+
+ @Override
+ public void processInitialSync(ByteBuffer p) {
+ super.processInitialSync(p);
+
+ processAddUsers(p);
+ processChangeTopic(p);
+ }
+
+ public void processChannelPacket(int type, ByteBuffer p) {
+ switch (type) {
+ case 0x120: processAddUsers(p); break;
+ case 0x121: processRemoveUsers(p); break;
+ case 0x122: processRenameUser(p); break;
+ case 0x123: processChangeUserMode(p); break;
+ case 0x124: processChangeTopic(p); break;
+ }
+ }
+
+ private void processAddUsers(ByteBuffer p) {
+ int count = p.getInt();
+
+ for (int i = 0; i < count; i++) {
+ String nick = Util.readStringFromBuffer(p);
+
+ UserData d = new UserData();
+ d.id = ++mNextUserID;
+ d.modes = p.getInt();
+ d.prefix = (char)p.get();
+
+ users.put(nick, d);
+ }
+
+ if (count > 0)
+ for (WindowListener l : mListeners)
+ ((ChannelListener)l).handleUsersChanged();
+ }
+
+ private void processRemoveUsers(ByteBuffer p) {
+ int count = p.getInt();
+
+ if (count == 0) {
+ users.clear();
+ } else {
+ for (int i = 0; i < count; i++) {
+ String nick = Util.readStringFromBuffer(p);
+ users.remove(nick);
+ }
+ }
+
+ for (WindowListener l : mListeners)
+ ((ChannelListener)l).handleUsersChanged();
+ }
+
+ private void processRenameUser(ByteBuffer p) {
+ String fromNick = Util.readStringFromBuffer(p);
+ String toNick = Util.readStringFromBuffer(p);
+
+ if (users.containsKey(fromNick)) {
+ users.put(toNick, users.get(fromNick));
+ users.remove(fromNick);
+
+ for (WindowListener l : mListeners)
+ ((ChannelListener)l).handleUsersChanged();
+ }
+ }
+
+ private void processChangeUserMode(ByteBuffer p) {
+ String nick = Util.readStringFromBuffer(p);
+ int modes = p.getInt();
+ char prefix = (char)p.get();
+
+ if (users.containsKey(nick)) {
+ UserData d = users.get(nick);
+ d.modes = modes;
+ d.prefix = prefix;
+
+ for (WindowListener l : mListeners)
+ ((ChannelListener)l).handleUsersChanged();
+ }
+ }
+
+ private void processChangeTopic(ByteBuffer p) {
+ topic = Util.readStringFromBuffer(p);
+ }
+}
diff --git a/android/VulpIRC/src/main/java/net/brokenfox/vulpirc/ChannelFragment.java b/android/VulpIRC/src/main/java/net/brokenfox/vulpirc/ChannelFragment.java
new file mode 100644
index 0000000..d60fc5f
--- /dev/null
+++ b/android/VulpIRC/src/main/java/net/brokenfox/vulpirc/ChannelFragment.java
@@ -0,0 +1,22 @@
+package net.brokenfox.vulpirc;
+
+import android.os.Bundle;
+
+/**
+ * Created by ninji on 2/6/14.
+ */
+public class ChannelFragment extends WindowFragment implements ChannelData.ChannelListener {
+ private ChannelData mChannelData;
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ mChannelData = (ChannelData)mData;
+ }
+
+ @Override
+ public void handleUsersChanged() {
+ // boop
+ }
+}
diff --git a/android/VulpIRC/src/main/java/net/brokenfox/vulpirc/Connection.java b/android/VulpIRC/src/main/java/net/brokenfox/vulpirc/Connection.java
new file mode 100644
index 0000000..845717c
--- /dev/null
+++ b/android/VulpIRC/src/main/java/net/brokenfox/vulpirc/Connection.java
@@ -0,0 +1,240 @@
+package net.brokenfox.vulpirc;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+
+/**
+ * Created by ninji on 1/30/14.
+ */
+public class Connection implements BaseConn.BaseConnListener {
+ private static Connection mInstance = new Connection();
+ public static Connection get() { return mInstance; }
+
+
+
+ private BaseConn mBaseConn = new BaseConn();
+ private Connection() {
+ mBaseConn.setListener(this);
+
+ statusWindow.id = -1;
+ statusWindow.title = "Status";
+ }
+
+ // Listener junk
+ public interface ConnectionListener {
+ void handleWindowsUpdated();
+ }
+
+ private ArrayList<ConnectionListener> mListeners = new ArrayList<ConnectionListener>();
+ public void registerListener(ConnectionListener l) {
+ mListeners.add(l);
+ }
+
+ public void deregisterListener(ConnectionListener l) {
+ mListeners.remove(l);
+ }
+
+
+
+ private ArrayList<LoginStateListener> mLoginStateListeners = new ArrayList<LoginStateListener>();
+ public interface LoginStateListener {
+ void handleLoginStateChanged();
+ }
+ public void registerLoginStateListener(LoginStateListener l) {
+ mLoginStateListeners.add(l);
+ }
+ public void deregisterLoginStateListener(LoginStateListener l) {
+ mLoginStateListeners.remove(l);
+ }
+
+
+ // Connection control
+ public void connect(String hostname, int port, boolean useTls, String username, String password) {
+ clearLoginError();
+ mBaseConn.initiateConnection(hostname, port, useTls, username, password);
+ }
+ public void breakConn() {
+ mBaseConn.requestDisconnection();
+ }
+ public void disconnect() {
+ mBaseConn.requestEndSession();
+ }
+
+ public BaseConn.SocketState getSocketState() {
+ return mBaseConn.getSocketState();
+ }
+ public boolean getSessionActive() {
+ return mBaseConn.getSessionActive();
+ }
+
+ private String mLoginError = null;
+ public String getLoginError() {
+ return mLoginError;
+ }
+ public void clearLoginError() {
+ mLoginError = null;
+ }
+
+
+ // BaseConn handlers
+ @Override
+ public void handleSessionStarted() {
+ statusWindow.pushMessage("Session started!");
+
+ for (LoginStateListener l : mLoginStateListeners)
+ l.handleLoginStateChanged();
+
+ windows.clear();
+ for (ConnectionListener l : mListeners)
+ l.handleWindowsUpdated();
+ }
+
+ @Override
+ public void handleSessionEnded() {
+ statusWindow.pushMessage("Session ended!");
+
+ for (LoginStateListener l : mLoginStateListeners)
+ l.handleLoginStateChanged();
+ }
+
+ @Override
+ public void handleSocketStateChanged() {
+ for (LoginStateListener l : mLoginStateListeners)
+ l.handleLoginStateChanged();
+ }
+
+ @Override
+ public void handleLoginError(String error) {
+ mLoginError = error;
+ }
+
+ @Override
+ public void handlePacketReceived(int type, byte[] data) {
+ //statusWindow.pushMessage("Packet received! " + type + " " + data.length);
+
+ ByteBuffer p = ByteBuffer.wrap(data);
+ p.order(ByteOrder.LITTLE_ENDIAN);
+
+ if (type == 1) {
+
+ statusWindow.pushMessage(Util.readStringFromBuffer(p));
+
+ } else if (type == 0x100) {
+ // Add windows!
+ int windowCount = p.getInt();
+ if (windowCount <= 0)
+ return;
+
+ for (int i = 0; i < windowCount; i++) {
+ int windowType = p.getInt();
+
+ WindowData w;
+ if (windowType == 2)
+ w = new ChannelData();
+ else
+ w = new WindowData();
+
+ w.processInitialSync(p);
+
+ windows.add(w);
+ }
+
+ for (ConnectionListener l : mListeners)
+ l.handleWindowsUpdated();
+
+ } else if (type == 0x101) {
+ // Remove windows
+ int windowCount = p.getInt();
+ if (windowCount <= 0)
+ return;
+
+ for (int i = 0; i < windowCount; i++) {
+ int windowID = p.getInt();
+ for (int j = 0; j < windows.size(); j++) {
+ if (windows.get(j).id == windowID) {
+ windows.remove(j);
+ break;
+ }
+ }
+
+ if ((mActiveWindow != null) && (mActiveWindow.id == windowID))
+ mActiveWindow = null;
+ }
+
+ for (ConnectionListener l : mListeners)
+ l.handleWindowsUpdated();
+
+ } else if (type == 0x102) {
+ // Add message to window
+ int windowID = p.getInt();
+ byte priority = p.get();
+ String message = Util.readStringFromBuffer(p);
+
+ WindowData w = findWindowByID(windowID);
+ if (w != null) {
+ w.pushMessage(message);
+
+ if (priority > w.unreadLevel && w != mActiveWindow)
+ w.setUnreadLevel(priority);
+ }
+
+ } else if (type == 0x103) {
+ // Rename window
+ int windowID = p.getInt();
+ String newTitle = Util.readStringFromBuffer(p);
+
+ WindowData w = findWindowByID(windowID);
+ if (w != null)
+ w.setTitle(newTitle);
+
+ } else if ((type >= 0x120) && (type < 0x124)) {
+ // Channel packets
+ int windowID = p.getInt();
+ WindowData w = findWindowByID(windowID);
+
+ if (w != null && w instanceof ChannelData) {
+ ((ChannelData)w).processChannelPacket(type, p);
+ }
+ }
+ }
+
+ public WindowData findWindowByID(int id) {
+ if (id == -1)
+ return statusWindow;
+
+ for (WindowData w : windows)
+ if (w.id == id)
+ return w;
+
+ return null;
+ }
+
+ @Override
+ public void handleStatusMessage(String message) {
+ statusWindow.pushMessage(message);
+ }
+
+
+
+ public void sendPacket(int type, byte[] data) {
+ mBaseConn.sendPacket(type, data);
+ }
+
+
+ // Windows.
+ public WindowData statusWindow = new WindowData();
+ public ArrayList<WindowData> windows = new ArrayList<WindowData>();
+ private WindowData mActiveWindow = null;
+
+ public void notifyWindowsUpdated() {
+ for (ConnectionListener l : mListeners)
+ l.handleWindowsUpdated();
+ }
+
+ public WindowData getActiveWindow() { return mActiveWindow; }
+ public void setActiveWindow(WindowData w) {
+ mActiveWindow = w;
+ w.setUnreadLevel(0);
+ }
+}
diff --git a/android/VulpIRC/src/main/java/net/brokenfox/vulpirc/IRCService.java b/android/VulpIRC/src/main/java/net/brokenfox/vulpirc/IRCService.java
new file mode 100644
index 0000000..6f76c1b
--- /dev/null
+++ b/android/VulpIRC/src/main/java/net/brokenfox/vulpirc/IRCService.java
@@ -0,0 +1,131 @@
+package net.brokenfox.vulpirc;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Intent;
+import android.os.Binder;
+import android.os.IBinder;
+
+/**
+ * Created by ninji on 1/30/14.
+ */
+public class IRCService extends Service implements Connection.LoginStateListener {
+ private final IBinder mBinder = new LocalBinder();
+ public class LocalBinder extends Binder {
+ IRCService getService() {
+ return IRCService.this;
+ }
+ }
+
+
+ // Lifecycle junk.
+
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ Connection.get().registerLoginStateListener(this);
+ mNM = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
+ }
+
+ @Override
+ public void onDestroy() {
+ Connection.get().deregisterLoginStateListener(this);
+ disableForeground();
+ super.onDestroy();
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ // If we're killed, don't automatically restart -- we only want to be
+ // started if the main VulpIRC app requests it.
+
+ return START_NOT_STICKY;
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mBinder;
+ }
+
+
+
+
+ private NotificationManager mNM = null;
+
+ private boolean mInForeground = false;
+ private void enableForeground() {
+ Notification n = generateNotification();
+
+ if (mInForeground) {
+ // Just refresh the notification
+ mNM.notify(1, n);
+ } else {
+ mInForeground = true;
+
+ startForeground(1, n);
+ }
+ }
+ private void disableForeground() {
+ if (!mInForeground)
+ return;
+
+ stopForeground(true);
+ mInForeground = false;
+ }
+
+
+ private Notification generateNotification() {
+ boolean active = Connection.get().getSessionActive();
+ String desc1, desc2 = "";
+ desc1 = active ? "Logged in, " : "Not logged in, ";
+ switch (Connection.get().getSocketState()) {
+ case DISCONNECTED:
+ desc2 = "disconnected";
+ break;
+ case DISCONNECTING:
+ desc2 = "disconnecting";
+ break;
+ case CONNECTING:
+ desc2 = "connecting...";
+ break;
+ case CONNECTED:
+ desc2 = "connected";
+ break;
+ }
+
+ Intent i = new Intent(this, active ? MainActivity.class : LoginActivity.class);
+ i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ //i.setFlags(i.getFlags() | 0x2000); // enables halo intent, maybe?
+ PendingIntent pi = PendingIntent.getActivity(this, 0, i, 0);
+
+ Notification n = new Notification.Builder(this)
+ .setContentTitle("VulpIRC")
+ .setContentText(desc1 + desc2)
+ .setContentIntent(pi)
+ .setSmallIcon(R.drawable.ic_launcher)
+ .setOngoing(true)
+ .setDefaults(0)
+ .getNotification();
+
+ return n;
+ }
+
+
+ // IRC FUNTIMES
+
+ @Override
+ public void handleLoginStateChanged() {
+ if (Connection.get().getSessionActive()) {
+ enableForeground();
+ } else {
+ if (Connection.get().getSocketState() == BaseConn.SocketState.DISCONNECTED) {
+ disableForeground();
+ stopSelf();
+ } else
+ enableForeground();
+ }
+ }
+}
diff --git a/android/VulpIRC/src/main/java/net/brokenfox/vulpirc/LoginActivity.java b/android/VulpIRC/src/main/java/net/brokenfox/vulpirc/LoginActivity.java
new file mode 100644
index 0000000..85092e9
--- /dev/null
+++ b/android/VulpIRC/src/main/java/net/brokenfox/vulpirc/LoginActivity.java
@@ -0,0 +1,263 @@
+package net.brokenfox.vulpirc;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.app.Activity;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.Menu;
+import android.view.View;
+import android.widget.CheckBox;
+import android.widget.EditText;
+import android.widget.TextView;
+import android.widget.Toast;
+
+public class LoginActivity extends Activity implements Connection.LoginStateListener {
+ private View mContainerLoginForm;
+ private View mContainerLoginStatus;
+ private TextView mLoginStatus;
+ private EditText mInputUsername, mInputPassword;
+ private EditText mInputHostname, mInputPort;
+ private CheckBox mCheckUseTls;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.activity_login);
+
+ mContainerLoginForm = findViewById(R.id.containerLoginForm);
+ mContainerLoginStatus = findViewById(R.id.containerLoginStatus);
+
+ mLoginStatus = (TextView)findViewById(R.id.loginStatus);
+
+ mInputUsername = (EditText)findViewById(R.id.inputUsername);
+ mInputPassword = (EditText)findViewById(R.id.inputPassword);
+ mInputHostname = (EditText)findViewById(R.id.inputHostname);
+ mInputPort = (EditText)findViewById(R.id.inputPort);
+
+ mCheckUseTls = (CheckBox)findViewById(R.id.checkUseTls);
+
+ findViewById(R.id.buttonConnect).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ tryAndConnect();
+ }
+ });
+
+ findViewById(R.id.buttonCancel).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ tryAndCancel();
+ }
+ });
+
+ doBindService();
+ }
+
+ @Override
+ protected void onDestroy() {
+ doUnbindService();
+ super.onDestroy();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+ getMenuInflater().inflate(R.menu.login, menu);
+ return true;
+ }
+
+
+
+ // Junk.
+
+ private boolean mIsConnecting = false;
+
+ private void tryAndConnect() {
+ if (mIsConnecting)
+ return;
+
+ mInputUsername.setError(null);
+ mInputPassword.setError(null);
+ mInputHostname.setError(null);
+ mInputPort.setError(null);
+
+ String username = mInputUsername.getText().toString();
+ String password = mInputPassword.getText().toString();
+ String hostname = mInputHostname.getText().toString();
+ String portStr = mInputPort.getText().toString();
+ int port = -1;
+ if (!TextUtils.isEmpty(portStr)) {
+ port = Integer.parseInt(portStr);
+ }
+
+ boolean usesTls = mCheckUseTls.isChecked();
+
+ View errorView = null;
+
+ //if (TextUtils.isEmpty(password)) {
+ // mInputPassword.setError("Required");
+ // errorView = mInputPassword;
+ //}
+ if (TextUtils.isEmpty(username)) {
+ mInputUsername.setError("Required");
+ errorView = mInputUsername;
+ }
+
+ if (port < 0 || port > 65535) {
+ mInputPort.setError("Invalid");
+ errorView = mInputPort;
+ }
+ if (TextUtils.isEmpty(hostname)) {
+ mInputHostname.setError("Required");
+ errorView = mInputHostname;
+ }
+
+
+ if (errorView != null) {
+ errorView.requestFocus();
+ } else {
+ mIsConnecting = true;
+
+ mLoginStatus.setText("Starting up...");
+
+ showProgress(true);
+
+ // start service so it'll remain around...
+ Intent i = new Intent(this, IRCService.class);
+ startService(i);
+
+ Connection.get().connect(hostname, port, usesTls, username, password);
+ }
+ }
+
+ private void tryAndCancel() {
+ Connection.get().disconnect();
+ }
+
+
+
+ public void handleLoginStateChanged() {
+ switch (Connection.get().getSocketState()) {
+ case DISCONNECTED:
+ mLoginStatus.setText("Disconnected.");
+ String details = Connection.get().getLoginError();
+ if (details != null) {
+ Toast.makeText(this, details, Toast.LENGTH_LONG).show();
+ Connection.get().clearLoginError();
+ }
+ showProgress(false);
+ mIsConnecting = false;
+ break;
+ case DISCONNECTING:
+ mLoginStatus.setText("Disconnecting...");
+ break;
+ case CONNECTING:
+ mLoginStatus.setText("Connecting...");
+ break;
+ case CONNECTED:
+ if (Connection.get().getSessionActive()) {
+ mLoginStatus.setText("Connected!");
+
+ completeLoginScreen();
+ } else {
+ mLoginStatus.setText("Logging in...");
+ }
+ break;
+ }
+ }
+
+
+ private void showProgress(final boolean show) {
+ // Straight from the LoginActivity template!
+ // .... Almost.
+
+ int shortAnimTime = getResources().getInteger(android.R.integer.config_shortAnimTime);
+
+ mContainerLoginStatus.setVisibility(View.VISIBLE);
+ mContainerLoginStatus.animate()
+ .setDuration(shortAnimTime)
+ .alpha(show ? 1 : 0)
+ .setListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mContainerLoginStatus.setVisibility(show ? View.VISIBLE : View.INVISIBLE);
+ }
+ });
+
+ mContainerLoginForm.setVisibility(View.VISIBLE);
+ mContainerLoginForm.animate()
+ .setDuration(shortAnimTime)
+ .alpha(show ? 0 : 1)
+ .setListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mContainerLoginForm.setVisibility(show ? View.INVISIBLE : View.VISIBLE);
+ }
+ });
+ }
+
+
+ private void completeLoginScreen() {
+ if (!isFinishing()) {
+ Intent i = new Intent(LoginActivity.this, MainActivity.class);
+ //i.setFlags(i.getFlags() | 0x2000); // enables halo intent, maybe?
+ startActivity(i);
+ finish();
+ }
+ }
+
+
+ // SERVICE JUNK
+ // Not happy about having this both here and in MainActivity.
+ // Moof.
+ private IRCService mService = null;
+
+ private final ServiceConnection mServiceConnection = new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
+ Log.i("VulpIRC", "[LoginActivity connected to IRCService]");
+
+ mService = ((IRCService.LocalBinder)iBinder).getService();
+ Connection.get().registerLoginStateListener(LoginActivity.this);
+
+ if (Connection.get().getSessionActive()) {
+ // We have a session, just jump right into the client
+ completeLoginScreen();
+ } else {
+ handleLoginStateChanged();
+ if (Connection.get().getSocketState() == BaseConn.SocketState.DISCONNECTED) {
+ mContainerLoginForm.setVisibility(View.VISIBLE);
+ } else {
+ mContainerLoginStatus.setVisibility(View.VISIBLE);
+ }
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName componentName) {
+ Log.i("VulpIRC", "[LoginActivity disconnected from IRCService]");
+
+ Connection.get().deregisterLoginStateListener(LoginActivity.this);
+ mService = null;
+ }
+ };
+
+ private void doBindService() {
+ Log.i("VulpIRC", "[LoginActivity binding to IRCService...]");
+
+ Intent i = new Intent(this, IRCService.class);
+ bindService(i, mServiceConnection, BIND_AUTO_CREATE);
+ }
+
+ private void doUnbindService() {
+ Log.i("VulpIRC", "[LoginActivity unbinding from IRCService...]");
+
+ unbindService(mServiceConnection);
+ }}
diff --git a/android/VulpIRC/src/main/java/net/brokenfox/vulpirc/MainActivity.java b/android/VulpIRC/src/main/java/net/brokenfox/vulpirc/MainActivity.java
new file mode 100644
index 0000000..0661d85
--- /dev/null
+++ b/android/VulpIRC/src/main/java/net/brokenfox/vulpirc/MainActivity.java
@@ -0,0 +1,340 @@
+package net.brokenfox.vulpirc;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.TypedArray;
+import android.support.v4.app.*;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.support.v4.view.ViewPager;
+import android.support.v4.widget.DrawerLayout;
+import android.util.Log;
+import android.view.*;
+import android.widget.*;
+
+public class MainActivity extends FragmentActivity implements Connection.ConnectionListener, Connection.LoginStateListener {
+
+ private ListView mWindowList;
+ private ViewPager mWindowPager;
+ private WindowListAdapter mWindowListAdapter;
+ private WindowPagerAdapter mWindowPagerAdapter;
+ private DrawerLayout mDrawerLayout;
+ private ActionBarDrawerToggle mDrawerToggle;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+
+ mDrawerLayout = ((DrawerLayout)findViewById(R.id.drawerLayout));
+ mDrawerToggle = new ActionBarDrawerToggle(this, mDrawerLayout,
+ R.drawable.ic_drawer,
+ R.string.drawer_open, R.string.drawer_close) {
+ @Override
+ public void onDrawerOpened(View drawerView) {
+ super.onDrawerOpened(drawerView);
+ getActionBar().setTitle("VulpIRC");
+ invalidateOptionsMenu();
+ }
+
+ @Override
+ public void onDrawerClosed(View drawerView) {
+ super.onDrawerClosed(drawerView);
+
+ fixActionBarTitle();
+ invalidateOptionsMenu();
+ }
+ };
+ mDrawerLayout.setDrawerListener(mDrawerToggle);
+ mDrawerLayout.setDrawerShadow(R.drawable.drawer_shadow, Gravity.LEFT);
+
+ getActionBar().setDisplayHomeAsUpEnabled(true);
+ getActionBar().setHomeButtonEnabled(true);
+
+ mWindowList = ((ListView)findViewById(R.id.windowList));
+ mWindowList.setChoiceMode(AbsListView.CHOICE_MODE_SINGLE);
+ mWindowList.setOnItemClickListener(new WindowListItemClickListener());
+ mWindowList.setBackgroundColor(0x60000000);
+ mWindowListAdapter = new WindowListAdapter();
+
+ mWindowPager = ((ViewPager)findViewById(R.id.windowPager));
+ mWindowPager.setOnPageChangeListener(new WindowPagerPageChangeListener());
+ mWindowPagerAdapter = new WindowPagerAdapter(getSupportFragmentManager());
+
+ doBindService();
+ }
+
+ @Override
+ protected void onPostCreate(Bundle savedInstanceState) {
+ super.onPostCreate(savedInstanceState);
+ mDrawerToggle.syncState();
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ mDrawerToggle.onConfigurationChanged(newConfig);
+ }
+
+ @Override
+ protected void onDestroy() {
+ if (mService != null) {
+ Connection.get().deregisterListener(this);
+ Connection.get().deregisterLoginStateListener(this);
+ }
+ doUnbindService();
+ super.onDestroy();
+ }
+
+ @Override
+ public void handleWindowsUpdated() {
+ mWindowPagerAdapter.notifyDataSetChanged();
+ mWindowListAdapter.notifyDataSetChanged();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.main, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (mDrawerToggle.onOptionsItemSelected(item))
+ return true;
+
+ switch (item.getItemId()) {
+ case R.id.action_settings:
+ return true;
+
+ case R.id.actionLogOut:
+ Connection.get().disconnect();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+
+ private void fixActionBarTitle() {
+ if (mDrawerLayout.isDrawerOpen(mWindowList))
+ return;
+
+ WindowData w = Connection.get().getActiveWindow();
+ if (w == null)
+ getActionBar().setTitle("VulpIRC");
+ else
+ getActionBar().setTitle(w.title);
+ }
+
+
+ @Override
+ public void handleLoginStateChanged() {
+ BaseConn.SocketState s = Connection.get().getSocketState();
+ switch (s) {
+ case CONNECTED:
+ getActionBar().setSubtitle("Connected!");
+ break;
+ case CONNECTING:
+ getActionBar().setSubtitle("Connecting...");
+ break;
+ case DISCONNECTED:
+ getActionBar().setSubtitle("Disconnected.");
+ break;
+ case DISCONNECTING:
+ getActionBar().setSubtitle("Disconnecting...");
+ break;
+ }
+ }
+
+ private class WindowListItemClickListener implements AdapterView.OnItemClickListener {
+ @Override
+ public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) {
+ mWindowPager.setCurrentItem(position);
+ mWindowList.setItemChecked(position, true);
+ }
+ }
+
+ private class WindowPagerPageChangeListener extends ViewPager.SimpleOnPageChangeListener {
+ @Override
+ public void onPageSelected(int position) {
+ mWindowList.setItemChecked(position, true);
+ if (Connection.get().getActiveWindow() != null)
+ Log.i("VulpIRC", "Boop1 : " + Connection.get().getActiveWindow().title);
+
+ if (position == 0)
+ Connection.get().setActiveWindow(Connection.get().statusWindow);
+ else
+ Connection.get().setActiveWindow(Connection.get().windows.get(position - 1));
+
+ Log.i("VulpIRC", "Boop2 : " + Connection.get().getActiveWindow().title);
+ fixActionBarTitle();
+ }
+ }
+
+
+ private class WindowListItemView extends LinearLayout {
+ private ImageView mStatusView;
+ private TextView mTitleView;
+
+ public WindowListItemView(Context context) {
+ super(context);
+
+ setOrientation(HORIZONTAL);
+
+ mStatusView = new ImageView(context);
+ mStatusView.setImageDrawable(getResources().getDrawable(R.drawable.window_status));
+ mTitleView = new TextView(context);
+ mTitleView.setGravity(0x800003|0x10); // start|center_vertical
+
+ float density = getResources().getDisplayMetrics().density;
+ setLayoutParams(new AbsListView.LayoutParams(LayoutParams.MATCH_PARENT, (int) (36 * density + 0.5f)));
+
+ int statusSize = (int)(8 * density + 0.5f);
+ LayoutParams statuslp = new LayoutParams(statusSize, LayoutParams.MATCH_PARENT);
+
+ int titlePadding = (int)(16 * density + 0.5f);
+ mTitleView.setPadding(titlePadding, 0, 0, 0);
+
+ addView(mStatusView, statuslp);
+ addView(mTitleView, new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT));
+
+ // I really don't like this.
+ TypedArray a = getTheme().obtainStyledAttributes(new int[] { android.R.attr.activatedBackgroundIndicator });
+ //setBackground(getResources().getDrawable(android.R.attr.activatedBackgroundIndicator));
+ setBackgroundDrawable(a.getDrawable(0));
+ a.recycle();
+ }
+
+ public void setPlaceholder() {
+ mTitleView.setText("Placeholder");
+ }
+ public void setWindow(WindowData wd) {
+ mTitleView.setText(wd.title);
+ mStatusView.setImageLevel(wd.unreadLevel);
+ }
+ }
+
+ private class WindowListAdapter extends BaseAdapter implements ListAdapter {
+ @Override
+ public int getCount() {
+ return 1 + Connection.get().windows.size();
+ }
+
+ @Override
+ public Object getItem(int i) {
+ if (i == 0)
+ return "boop";
+ else
+ return Connection.get().windows.get(i - 1);
+ }
+
+ @Override
+ public long getItemId(int i) {
+ if (i == 0)
+ return -1;
+ else
+ return Connection.get().windows.get(i - 1).id;
+ }
+
+ @Override
+ public View getView(int i, View view, ViewGroup viewGroup) {
+ WindowListItemView iv;
+
+ if (view != null && view instanceof WindowListItemView) {
+ iv = (WindowListItemView)view;
+ } else {
+ iv = new WindowListItemView(viewGroup.getContext());
+ }
+
+ if (i == 0)
+ iv.setWindow(Connection.get().statusWindow);
+ else
+ iv.setWindow(Connection.get().windows.get(i - 1));
+ return iv;
+ }
+ }
+
+ private class WindowPagerAdapter extends FragmentPagerAdapter {
+ WindowPagerAdapter(FragmentManager fm) {
+ super(fm);
+ }
+
+ @Override
+ public Fragment getItem(int i) {
+ WindowData window = null;
+ if (i == 0)
+ window = Connection.get().statusWindow;
+ else
+ window = Connection.get().windows.get(i - 1);
+
+ return window.createFragment();
+ }
+
+ @Override
+ public long getItemId(int position) {
+ if (position == 0)
+ return -1;
+ else
+ return Connection.get().windows.get(position - 1).id;
+ }
+
+ @Override
+ public CharSequence getPageTitle(int position) {
+ if (position == 0)
+ return "Status";
+ else
+ return Connection.get().windows.get(position - 1).title;
+ }
+
+ @Override
+ public int getCount() {
+ return 1 + Connection.get().windows.size();
+ }
+ }
+
+
+
+
+ // SERVICE JUNK
+ private IRCService mService = null;
+
+ private final ServiceConnection mServiceConnection = new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
+ Log.i("VulpIRC", "[MainActivity connected to IRCService]");
+
+ mService = ((IRCService.LocalBinder)iBinder).getService();
+ Connection.get().registerListener(MainActivity.this);
+ Connection.get().registerLoginStateListener(MainActivity.this);
+
+ mWindowList.setAdapter(mWindowListAdapter);
+ mWindowPager.setAdapter(mWindowPagerAdapter);
+
+ handleLoginStateChanged();
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName componentName) {
+ Log.i("VulpIRC", "[MainActivity disconnected from IRCService]");
+
+ mService = null;
+ }
+ };
+
+ private void doBindService() {
+ Log.i("VulpIRC", "[MainActivity binding to IRCService...]");
+
+ Intent i = new Intent(this, IRCService.class);
+ bindService(i, mServiceConnection, BIND_AUTO_CREATE);
+ }
+
+ private void doUnbindService() {
+ Log.i("VulpIRC", "[MainActivity unbinding from IRCService...]");
+
+ unbindService(mServiceConnection);
+ }
+
+}
diff --git a/android/VulpIRC/src/main/java/net/brokenfox/vulpirc/RichText.java b/android/VulpIRC/src/main/java/net/brokenfox/vulpirc/RichText.java
new file mode 100644
index 0000000..dbe6f6b
--- /dev/null
+++ b/android/VulpIRC/src/main/java/net/brokenfox/vulpirc/RichText.java
@@ -0,0 +1,236 @@
+package net.brokenfox.vulpirc;
+
+import android.graphics.Typeface;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.style.BackgroundColorSpan;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.StyleSpan;
+import android.text.style.UnderlineSpan;
+
+import java.util.HashMap;
+
+/**
+ * Created by ninji on 2/14/14.
+ */
+public class RichText {
+ private final static int[] presetColors = new int[] {
+ // IRC colours
+ 0xFFFFFF, 0x000000, 0x000080, 0x008000,
+ 0xFF0000, 0x800000, 0x800080, 0x808000,
+ 0xFFFF00, 0x00FF00, 0x008080, 0x00FFFF,
+ 0x0000FF, 0xFF00FF, 0x808080, 0xC0C0C0,
+ // Default FG, BG
+ 0xFFFFFF, 0x000000,
+ // Preset message colours
+ 0x6600CC, 0x009900, 0x660000, 0x660000,
+ 0x660000, 0x336699,
+ };
+
+ private final static StyleSpan boldSpan = new StyleSpan(Typeface.BOLD);
+ private final static StyleSpan italicSpan = new StyleSpan(Typeface.ITALIC);
+ private final static UnderlineSpan underlineSpan = new UnderlineSpan();
+ private static ForegroundColorSpan[] presetForegroundSpans = null;
+ private static BackgroundColorSpan[] presetBackgroundSpans = null;
+ private static HashMap<Integer, ForegroundColorSpan> cachedForegroundSpans = new HashMap<Integer, ForegroundColorSpan>();
+ private static HashMap<Integer, BackgroundColorSpan> cachedBackgroundSpans = new HashMap<Integer, BackgroundColorSpan>();
+
+ private static void setupPresets() {
+ presetForegroundSpans = new ForegroundColorSpan[presetColors.length];
+ presetBackgroundSpans = new BackgroundColorSpan[presetColors.length];
+
+ for (int i = 0; i < presetColors.length; i++) {
+ presetForegroundSpans[i] = new ForegroundColorSpan(0xFF000000 | presetColors[i]);
+ presetBackgroundSpans[i] = new BackgroundColorSpan(0xFF000000 | presetColors[i]);
+ }
+ }
+
+ // This is kinda kludgey but... I don't want to make two extra allocations
+ // every time I process a string.
+ // So here.
+ private static ForegroundColorSpan[] currentFgSpans = new ForegroundColorSpan[4];
+ private static BackgroundColorSpan[] currentBgSpans = new BackgroundColorSpan[4];
+
+ public static Spanned process(String source) {
+ if (presetForegroundSpans == null)
+ setupPresets();
+
+ SpannableStringBuilder s = new SpannableStringBuilder();
+
+ int boldStart = -1;
+ int italicStart = -1;
+ int underlineStart = -1;
+ int fgStart = -1, bgStart = -1;
+ ForegroundColorSpan fgSpan = null;
+ BackgroundColorSpan bgSpan = null;
+ int rawTextStart = -1;
+
+ for (int i = 0; i < 4; i++) {
+ currentFgSpans[i] = null;
+ currentBgSpans[i] = null;
+ }
+
+ int in = 0, out = 0;
+
+ for (in = 0; in < source.length(); in++) {
+ char c = source.charAt(in);
+ if (c >= 0 && c <= 0x1F) {
+ // Control code!
+ if (rawTextStart != -1) {
+ s.append(source, rawTextStart, in);
+ rawTextStart = -1;
+ }
+
+ // Process the code...
+ if (c == 1 && boldStart == -1) {
+ boldStart = out;
+ } else if (c == 2 && boldStart > -1) {
+ s.setSpan(boldSpan, boldStart, out, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ boldStart = -1;
+
+ } else if (c == 3 && italicStart == -1) {
+ italicStart = out;
+ } else if (c == 4 && italicStart > -1) {
+ s.setSpan(italicSpan, italicStart, out, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ italicStart = -1;
+
+ } else if (c == 5 && underlineStart == -1) {
+ underlineStart = out;
+ } else if (c == 6 && underlineStart > -1) {
+ s.setSpan(underlineSpan, underlineStart, out, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ underlineStart = -1;
+
+ } else if (c >= 0x10 && c <= 0x1F) {
+ boolean isBG = ((c & 4) == 4);
+ boolean isEnd = ((c & 8) == 8);
+ int layer = (c & 3);
+
+ // Read what comes afterwards, to decide what we're doing
+ Object chosenSpan = null;
+
+ if (!isEnd && ((in + 1) < source.length())) {
+ char first = source.charAt(in + 1);
+
+ if ((first & 1) == 1) {
+ // Preset colour
+ if (isBG)
+ chosenSpan = presetBackgroundSpans[first >> 1];
+ else
+ chosenSpan = presetForegroundSpans[first >> 1];
+ in++; // Skip the extra
+
+ } else if ((in + 3) < source.length()) {
+ // RGB colour
+ int r = first;
+ int g = source.charAt(in + 2);
+ int b = source.charAt(in + 3);
+ int col = 0xFF000000 | (r << 17) | (g << 9) | (b << 1);
+
+ if (isBG) {
+ if (cachedBackgroundSpans.containsKey(col)) {
+ chosenSpan = cachedBackgroundSpans.get(col);
+ } else {
+ chosenSpan = new BackgroundColorSpan(col);
+ cachedBackgroundSpans.put(col, (BackgroundColorSpan)chosenSpan);
+ }
+ } else {
+ if (cachedForegroundSpans.containsKey(col)) {
+ chosenSpan = cachedForegroundSpans.get(col);
+ } else {
+ chosenSpan = new ForegroundColorSpan(col);
+ cachedForegroundSpans.put(col, (ForegroundColorSpan)chosenSpan);
+ }
+ }
+
+ in += 3; // Skip the extra
+ }
+ }
+
+ // OK, are we actually changing?
+ Object oldSpan, newSpan = null;
+
+ if (isBG)
+ oldSpan = bgSpan;
+ else
+ oldSpan = fgSpan;
+
+ // Modify the array, and figure out our new active span
+ if (isBG) {
+ if (isEnd)
+ currentBgSpans[layer] = null;
+ else
+ currentBgSpans[layer] = (BackgroundColorSpan)chosenSpan;
+
+ for (int i = 0; i < 4; i++)
+ if (currentBgSpans[i] != null)
+ newSpan = currentBgSpans[i];
+
+ } else {
+ if (isEnd)
+ currentFgSpans[layer] = null;
+ else
+ currentFgSpans[layer] = (ForegroundColorSpan)chosenSpan;
+
+ for (int i = 0; i < 4; i++)
+ if (currentFgSpans[i] != null)
+ newSpan = currentFgSpans[i];
+ }
+
+ // If we changed spans....
+ if (oldSpan != newSpan) {
+ // End the existing span, no matter what...
+ if (isBG) {
+ if (bgStart > -1) {
+ s.setSpan(bgSpan, bgStart, out, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ bgStart = -1;
+ }
+ } else {
+ if (fgStart > -1) {
+ s.setSpan(fgSpan, fgStart, out, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ fgStart = -1;
+ }
+ }
+
+ // .. and if we have another span, add it!
+ if (newSpan != null) {
+ // Preset colour
+ if (isBG) {
+ bgSpan = (BackgroundColorSpan)newSpan;
+ bgStart = out;
+ } else {
+ fgSpan = (ForegroundColorSpan)newSpan;
+ fgStart = out;
+ }
+ }
+ }
+ }
+
+ } else {
+ // Regular character.
+ if (rawTextStart == -1) {
+ rawTextStart = in;
+ }
+ out++;
+ }
+ }
+
+ // Anything left?
+ if (rawTextStart != -1) {
+ s.append(source, rawTextStart, source.length());
+ }
+
+ // Any un-applied spans?
+ if (boldStart > -1)
+ s.setSpan(boldSpan, boldStart, out, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ if (italicStart > -1)
+ s.setSpan(italicSpan, italicStart, out, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ if (underlineStart > -1)
+ s.setSpan(underlineSpan, underlineStart, out, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ if (bgStart > -1)
+ s.setSpan(bgSpan, bgStart, out, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ if (fgStart > -1)
+ s.setSpan(fgSpan, fgStart, out, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+
+ return s;
+ }
+}
diff --git a/android/VulpIRC/src/main/java/net/brokenfox/vulpirc/Util.java b/android/VulpIRC/src/main/java/net/brokenfox/vulpirc/Util.java
new file mode 100644
index 0000000..c5dbff1
--- /dev/null
+++ b/android/VulpIRC/src/main/java/net/brokenfox/vulpirc/Util.java
@@ -0,0 +1,62 @@
+package net.brokenfox.vulpirc;
+
+import android.util.Log;
+
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.CharacterCodingException;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetDecoder;
+import java.nio.charset.CharsetEncoder;
+
+/**
+ * Created by ninji on 2/3/14.
+ */
+public class Util {
+
+ private static CharsetDecoder mUTF8Decoder = null;
+ private static CharsetEncoder mUTF8Encoder = null;
+
+ public static synchronized String readStringFromBuffer(ByteBuffer p) {
+ if (mUTF8Decoder == null)
+ mUTF8Decoder = Charset.forName("UTF-8").newDecoder();
+
+ int size = p.getInt();
+ if (size <= 0)
+ return "";
+
+ int beginPos = p.position();
+ int endPos = beginPos + size;
+ int saveLimit = p.limit();
+ p.limit(endPos);
+
+ String result = "";
+
+ try {
+ result = mUTF8Decoder.decode(p).toString();
+ } catch (CharacterCodingException e) {
+ Log.e("VulpIRC", "Utils.readStringFromBuffer caught decode exception:");
+ Log.e("VulpIRC", e.toString());
+ }
+
+ p.limit(saveLimit);
+ p.position(endPos);
+
+ return result;
+ }
+
+ public static synchronized byte[] encodeString(CharSequence s) {
+ if (mUTF8Encoder == null)
+ mUTF8Encoder = Charset.forName("UTF-8").newEncoder();
+
+ try {
+ return mUTF8Encoder.encode(CharBuffer.wrap(s)).array();
+ } catch (CharacterCodingException e) {
+ Log.e("VulpIRC", "Utils.encodeString caught encode exception:");
+ Log.e("VulpIRC", e.toString());
+
+ return new byte[0];
+ }
+ }
+
+}
diff --git a/android/VulpIRC/src/main/java/net/brokenfox/vulpirc/WindowData.java b/android/VulpIRC/src/main/java/net/brokenfox/vulpirc/WindowData.java
new file mode 100644
index 0000000..09c6606
--- /dev/null
+++ b/android/VulpIRC/src/main/java/net/brokenfox/vulpirc/WindowData.java
@@ -0,0 +1,106 @@
+package net.brokenfox.vulpirc;
+
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.text.format.Time;
+import android.util.Log;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.text.*;
+import java.util.ArrayList;
+import java.util.Date;
+
+/**
+ * Created by ninji on 2/3/14.
+ */
+public class WindowData {
+ public int id;
+ public String title;
+ public int unreadLevel = 0;
+
+ public ArrayList<CharSequence> messages = new ArrayList<CharSequence>();
+
+ protected Fragment instantiateFragmentClass() {
+ return new WindowFragment();
+ }
+ public Fragment createFragment() {
+ Bundle args = new Bundle();
+ args.putInt("winID", id);
+
+ Fragment wf = instantiateFragmentClass();
+ wf.setArguments(args);
+ return wf;
+ }
+
+ public void setUnreadLevel(int newLevel) {
+ unreadLevel = newLevel;
+ Connection.get().notifyWindowsUpdated();
+ }
+
+ public void setTitle(String newTitle) {
+ title = newTitle;
+ Connection.get().notifyWindowsUpdated();
+ }
+
+
+ public interface WindowListener {
+ void handleMessagesChanged();
+ }
+
+ protected ArrayList<WindowListener> mListeners = new ArrayList<WindowListener>();
+
+ public void registerListener(WindowListener l) {
+ mListeners.add(l);
+ }
+ public void deregisterListener(WindowListener l) {
+ mListeners.remove(l);
+ }
+
+
+
+ // This is a kludge.
+ private final DateFormat timestampFormat = new SimpleDateFormat("\u0001[\u0002\u0010\u001DHH:mm:ss\u0018\u0001]\u0002 ", new DateFormatSymbols());
+ private String generateTimestamp() {
+ return timestampFormat.format(new Date());
+ }
+
+ public void pushMessage(String message) {
+ messages.add(RichText.process(generateTimestamp() + message));
+ for (WindowListener l : mListeners)
+ l.handleMessagesChanged();
+ }
+
+
+ public void processInitialSync(ByteBuffer p) {
+ id = p.getInt();
+ title = Util.readStringFromBuffer(p);
+
+ int messageCount = p.getInt();
+ Log.i("VulpIRC", "id=" + id + ", title=[" + title + "]");
+
+ if (messageCount > 0) {
+ messages.ensureCapacity(messageCount);
+ for (int j = 0; j < messageCount; j++) {
+ String msg = Util.readStringFromBuffer(p);
+ //Log.i("VulpIRC", "msg " + j + ": " + msg);
+ messages.add(RichText.process(msg));
+ }
+ }
+ }
+
+
+
+ public void sendUserInput(CharSequence message) {
+ byte[] enc = Util.encodeString(message);
+
+ ByteBuffer buf = ByteBuffer.allocate(8 + enc.length);
+ buf.order(ByteOrder.LITTLE_ENDIAN);
+
+ buf.putInt(id);
+ buf.putInt(enc.length);
+ buf.put(enc);
+
+ Connection.get().sendPacket(0x102, buf.array());
+ }
+}
diff --git a/android/VulpIRC/src/main/java/net/brokenfox/vulpirc/WindowFragment.java b/android/VulpIRC/src/main/java/net/brokenfox/vulpirc/WindowFragment.java
new file mode 100644
index 0000000..da89e03
--- /dev/null
+++ b/android/VulpIRC/src/main/java/net/brokenfox/vulpirc/WindowFragment.java
@@ -0,0 +1,149 @@
+package net.brokenfox.vulpirc;
+
+import android.app.Activity;
+import android.content.Context;
+import android.support.v4.app.Fragment;
+import android.os.Bundle;
+import android.text.InputType;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.inputmethod.EditorInfo;
+import android.widget.*;
+
+/**
+ * Created by ninji on 2/3/14.
+ */
+public class WindowFragment extends Fragment implements WindowData.WindowListener, TextView.OnEditorActionListener {
+ private MessagesAdapter mMessagesAdapter;
+ private ListView mMessagesList;
+ private EditText mInput;
+ protected WindowData mData = null;
+
+ public WindowFragment() {
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ Context ctx = container.getContext();
+
+ LinearLayout l = new LinearLayout(ctx);
+ l.setOrientation(LinearLayout.VERTICAL);
+
+ float density = getResources().getDisplayMetrics().density;
+ int padding = (int)(4 * density + 0.5f);
+
+ mMessagesList = new ListView(ctx);
+ mMessagesList.setStackFromBottom(true);
+ mMessagesList.setTranscriptMode(AbsListView.TRANSCRIPT_MODE_NORMAL);
+ mMessagesList.setDivider(null);
+ mMessagesList.setDividerHeight(0);
+ mMessagesList.setPadding(padding, padding, padding, padding);
+
+ mInput = new EditText(ctx);
+ mInput.setImeOptions(EditorInfo.IME_ACTION_SEND | EditorInfo.IME_FLAG_NO_FULLSCREEN);
+ mInput.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_NORMAL);
+ mInput.setOnEditorActionListener(this);
+
+ LinearLayout.LayoutParams mlParams = new LinearLayout.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT);
+ mlParams.weight = 1;
+
+ LinearLayout.LayoutParams inputParams = new LinearLayout.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT);
+
+ l.addView(mMessagesList, mlParams);
+ l.addView(mInput, inputParams);
+
+ return l;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setRetainInstance(true);
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ Bundle b = getArguments();
+ int winID = b.getInt("winID");
+ mData = Connection.get().findWindowByID(winID);
+
+ mMessagesAdapter = new MessagesAdapter();
+ mMessagesList.setAdapter(mMessagesAdapter);
+
+ mData.registerListener(this);
+ }
+
+ @Override
+ public void onDestroy() {
+ if (mData != null)
+ mData.deregisterListener(this);
+ super.onDestroy();
+ }
+
+
+ @Override
+ public void handleMessagesChanged() {
+ mMessagesAdapter.notifyDataSetChanged();
+ }
+
+ @Override
+ public boolean onEditorAction(TextView textView, int i, KeyEvent keyEvent) {
+ if (i == EditorInfo.IME_ACTION_SEND) {
+ sendEnteredThing();
+ return true;
+ }
+
+ return false;
+ }
+
+
+
+ private void sendEnteredThing() {
+ CharSequence text = mInput.getText().toString();
+ mInput.getText().clear();
+
+ mData.sendUserInput(text);
+ }
+
+
+
+ private class MessagesAdapter extends BaseAdapter implements ListAdapter {
+ @Override
+ public int getCount() {
+ return mData.messages.size();
+ }
+
+ @Override
+ public Object getItem(int i) {
+ return mData.messages.get(i);
+ }
+
+ @Override
+ public long getItemId(int i) {
+ return i;
+ }
+
+ @Override
+ public View getView(int i, View view, ViewGroup viewGroup) {
+ TextView tv;
+
+ if (view != null && view instanceof TextView) {
+ tv = (TextView)view;
+ } else {
+ tv = new TextView(viewGroup.getContext());
+ }
+
+ tv.setText(mData.messages.get(i));
+ return tv;
+ }
+ }
+} \ No newline at end of file
diff --git a/android/VulpIRC/src/main/res/drawable-hdpi/drawer_shadow.9.png b/android/VulpIRC/src/main/res/drawable-hdpi/drawer_shadow.9.png
new file mode 100644
index 0000000..236bff5
--- /dev/null
+++ b/android/VulpIRC/src/main/res/drawable-hdpi/drawer_shadow.9.png
Binary files differ
diff --git a/android/VulpIRC/src/main/res/drawable-hdpi/ic_drawer.png b/android/VulpIRC/src/main/res/drawable-hdpi/ic_drawer.png
new file mode 100644
index 0000000..6614ea4
--- /dev/null
+++ b/android/VulpIRC/src/main/res/drawable-hdpi/ic_drawer.png
Binary files differ
diff --git a/android/VulpIRC/src/main/res/drawable-hdpi/ic_launcher.png b/android/VulpIRC/src/main/res/drawable-hdpi/ic_launcher.png
new file mode 100644
index 0000000..c1b44cc
--- /dev/null
+++ b/android/VulpIRC/src/main/res/drawable-hdpi/ic_launcher.png
Binary files differ
diff --git a/android/VulpIRC/src/main/res/drawable-mdpi/drawer_shadow.9.png b/android/VulpIRC/src/main/res/drawable-mdpi/drawer_shadow.9.png
new file mode 100644
index 0000000..ffe3a28
--- /dev/null
+++ b/android/VulpIRC/src/main/res/drawable-mdpi/drawer_shadow.9.png
Binary files differ
diff --git a/android/VulpIRC/src/main/res/drawable-mdpi/ic_drawer.png b/android/VulpIRC/src/main/res/drawable-mdpi/ic_drawer.png
new file mode 100644
index 0000000..b05c026
--- /dev/null
+++ b/android/VulpIRC/src/main/res/drawable-mdpi/ic_drawer.png
Binary files differ
diff --git a/android/VulpIRC/src/main/res/drawable-mdpi/ic_launcher.png b/android/VulpIRC/src/main/res/drawable-mdpi/ic_launcher.png
new file mode 100644
index 0000000..a1bf709
--- /dev/null
+++ b/android/VulpIRC/src/main/res/drawable-mdpi/ic_launcher.png
Binary files differ
diff --git a/android/VulpIRC/src/main/res/drawable-xhdpi/drawer_shadow.9.png b/android/VulpIRC/src/main/res/drawable-xhdpi/drawer_shadow.9.png
new file mode 100644
index 0000000..fabe9d9
--- /dev/null
+++ b/android/VulpIRC/src/main/res/drawable-xhdpi/drawer_shadow.9.png
Binary files differ
diff --git a/android/VulpIRC/src/main/res/drawable-xhdpi/ic_drawer.png b/android/VulpIRC/src/main/res/drawable-xhdpi/ic_drawer.png
new file mode 100644
index 0000000..bcf49dd
--- /dev/null
+++ b/android/VulpIRC/src/main/res/drawable-xhdpi/ic_drawer.png
Binary files differ
diff --git a/android/VulpIRC/src/main/res/drawable-xhdpi/ic_launcher.png b/android/VulpIRC/src/main/res/drawable-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..3b5ae5b
--- /dev/null
+++ b/android/VulpIRC/src/main/res/drawable-xhdpi/ic_launcher.png
Binary files differ
diff --git a/android/VulpIRC/src/main/res/drawable-xxhdpi/drawer_shadow.9.png b/android/VulpIRC/src/main/res/drawable-xxhdpi/drawer_shadow.9.png
new file mode 100644
index 0000000..b91e9d7
--- /dev/null
+++ b/android/VulpIRC/src/main/res/drawable-xxhdpi/drawer_shadow.9.png
Binary files differ
diff --git a/android/VulpIRC/src/main/res/drawable-xxhdpi/ic_drawer.png b/android/VulpIRC/src/main/res/drawable-xxhdpi/ic_drawer.png
new file mode 100644
index 0000000..f7e3b30
--- /dev/null
+++ b/android/VulpIRC/src/main/res/drawable-xxhdpi/ic_drawer.png
Binary files differ
diff --git a/android/VulpIRC/src/main/res/drawable-xxhdpi/ic_launcher.png b/android/VulpIRC/src/main/res/drawable-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..b7df51b
--- /dev/null
+++ b/android/VulpIRC/src/main/res/drawable-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/android/VulpIRC/src/main/res/drawable/window_status.xml b/android/VulpIRC/src/main/res/drawable/window_status.xml
new file mode 100644
index 0000000..232a645
--- /dev/null
+++ b/android/VulpIRC/src/main/res/drawable/window_status.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<level-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:minLevel="1" android:maxLevel="1">
+ <color android:color="#3fff" />
+ </item>
+ <item android:minLevel="2" android:maxLevel="2">
+ <color android:color="#fff" />
+ </item>
+</level-list> \ No newline at end of file
diff --git a/android/VulpIRC/src/main/res/layout/activity_login.xml b/android/VulpIRC/src/main/res/layout/activity_login.xml
new file mode 100644
index 0000000..d8e10bc
--- /dev/null
+++ b/android/VulpIRC/src/main/res/layout/activity_login.xml
@@ -0,0 +1,111 @@
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ tools:context=".LoginActivity">
+
+ <!-- Login progress -->
+ <LinearLayout
+ android:id="@+id/containerLoginStatus"
+ android:visibility="invisible"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:gravity="center_horizontal"
+ android:orientation="vertical">
+ <ProgressBar style="?android:attr/progressBarStyleLarge"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="8dp"/>
+ <TextView
+ android:id="@+id/loginStatus"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:fontFamily="sans-serif-light"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="16dp"
+ android:text="---" />
+ <Button
+ android:id="@+id/buttonCancel"
+ android:text="Cancel"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+ </LinearLayout>
+
+ <!-- Login form -->
+ <ScrollView
+ android:id="@+id/containerLoginForm"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:visibility="invisible"
+ android:padding="12dp">
+
+ <RelativeLayout
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <EditText
+ android:id="@+id/inputUsername"
+ android:singleLine="true"
+ android:maxLines="1"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:inputType="textNoSuggestions"
+ android:hint="Username" />
+
+ <EditText
+ android:id="@+id/inputPassword"
+ android:layout_below="@id/inputUsername"
+ android:layout_marginBottom="12dp"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:maxLines="1"
+ android:fontFamily="sans-serif"
+ android:hint="Password"
+ android:inputType="textPassword" />
+
+ <EditText
+ android:id="@+id/inputPort"
+ android:layout_below="@id/inputPassword"
+ android:layout_alignParentRight="true"
+ android:layout_width="120dp"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:maxLines="1"
+ android:hint="Port"
+ android:inputType="number"/>
+
+ <EditText
+ android:id="@+id/inputHostname"
+ android:layout_below="@id/inputPassword"
+ android:layout_toLeftOf="@id/inputPort"
+ android:layout_alignParentLeft="true"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:maxLines="1"
+ android:hint="Server address"
+ android:inputType=""/>
+
+ <CheckBox
+ android:id="@+id/checkUseTls"
+ android:layout_below="@id/inputHostname"
+ android:layout_marginBottom="12dp"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Use secured connection (SSL/TLS)"
+ />
+
+ <Button android:id="@+id/buttonConnect"
+ android:layout_below="@id/checkUseTls"
+ android:layout_alignParentRight="true"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Connect"
+ android:paddingLeft="32dp"
+ android:paddingRight="32dp" />
+
+ </RelativeLayout>
+
+ </ScrollView>
+</merge>
diff --git a/android/VulpIRC/src/main/res/layout/activity_main.xml b/android/VulpIRC/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..06c6ef5
--- /dev/null
+++ b/android/VulpIRC/src/main/res/layout/activity_main.xml
@@ -0,0 +1,40 @@
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ tools:context=".MainActivity"
+ tools:ignore="MergeRootFrame"
+ android:background="#000000">
+ <android.support.v4.widget.DrawerLayout
+ android:id="@+id/drawerLayout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ >
+ <android.support.v4.view.ViewPager
+ android:id="@+id/windowPager"
+ android:layout_width="300dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1">
+ </android.support.v4.view.ViewPager>
+ <ListView
+ android:id="@+id/windowList"
+ android:layout_width="160dp"
+ android:layout_height="match_parent"
+ android:layout_gravity="left"
+ >
+
+ </ListView>
+<!--
+ <ListView
+ android:id="@+id/messagesList"
+ android:layout_width="300dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ >
+
+ </ListView>
+ -->
+ </android.support.v4.widget.DrawerLayout>
+</LinearLayout>
diff --git a/android/VulpIRC/src/main/res/layout/window_list_entry.xml b/android/VulpIRC/src/main/res/layout/window_list_entry.xml
new file mode 100644
index 0000000..8fb74cf
--- /dev/null
+++ b/android/VulpIRC/src/main/res/layout/window_list_entry.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="horizontal"
+ android:layout_width="match_parent"
+ android:layout_height="32dp">
+
+ <ImageView
+ android:id="@+id/windowStatus"
+ android:layout_width="8dp"
+ android:layout_height="match_parent"
+ />
+
+ <TextView
+ android:id="@+id/windowName"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:paddingLeft="8dp"
+ android:text="test"
+ android:gravity="start|center_vertical"
+ />
+
+</LinearLayout> \ No newline at end of file
diff --git a/android/VulpIRC/src/main/res/menu/login.xml b/android/VulpIRC/src/main/res/menu/login.xml
new file mode 100644
index 0000000..debb9ef
--- /dev/null
+++ b/android/VulpIRC/src/main/res/menu/login.xml
@@ -0,0 +1,2 @@
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+</menu>
diff --git a/android/VulpIRC/src/main/res/menu/main.xml b/android/VulpIRC/src/main/res/menu/main.xml
new file mode 100644
index 0000000..6a0f783
--- /dev/null
+++ b/android/VulpIRC/src/main/res/menu/main.xml
@@ -0,0 +1,12 @@
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:id="@+id/actionLogOut"
+ android:title="Disconnect"
+ android:showAsAction="never" />
+
+
+ <item android:id="@+id/action_settings"
+ android:title="@string/action_settings"
+ android:orderInCategory="100"
+ android:showAsAction="never" />
+</menu>
diff --git a/android/VulpIRC/src/main/res/values-w820dp/dimens.xml b/android/VulpIRC/src/main/res/values-w820dp/dimens.xml
new file mode 100644
index 0000000..63fc816
--- /dev/null
+++ b/android/VulpIRC/src/main/res/values-w820dp/dimens.xml
@@ -0,0 +1,6 @@
+<resources>
+ <!-- Example customization of dimensions originally defined in res/values/dimens.xml
+ (such as screen margins) for screens with more than 820dp of available width. This
+ would include 7" and 10" devices in landscape (~960dp and ~1280dp respectively). -->
+ <dimen name="activity_horizontal_margin">64dp</dimen>
+</resources>
diff --git a/android/VulpIRC/src/main/res/values/dimens.xml b/android/VulpIRC/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..5d703a0
--- /dev/null
+++ b/android/VulpIRC/src/main/res/values/dimens.xml
@@ -0,0 +1,10 @@
+<resources>
+ <!-- Default screen margins, per the Android Design guidelines. -->
+ <dimen name="activity_horizontal_margin">16dp</dimen>
+ <dimen name="activity_vertical_margin">16dp</dimen>
+<dimen name="app_defaultsize_w">632.0dip</dimen>
+<dimen name="app_defaultsize_h">598.0dip</dimen>
+<dimen name="app_minimumsize_w">632.0dip</dimen>
+<dimen name="app_minimumsize_h">598.0dip</dimen>
+
+ </resources>
diff --git a/android/VulpIRC/src/main/res/values/strings.xml b/android/VulpIRC/src/main/res/values/strings.xml
new file mode 100644
index 0000000..49a1195
--- /dev/null
+++ b/android/VulpIRC/src/main/res/values/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="app_name">VulpIRC</string>
+ <string name="hello_world">Hello world!</string>
+ <string name="action_settings">Settings</string>
+ <string name="drawer_open">Open Drawer</string>
+ <string name="drawer_close">Close Drawer</string>
+
+</resources>
diff --git a/android/VulpIRC/src/main/res/values/styles.xml b/android/VulpIRC/src/main/res/values/styles.xml
new file mode 100644
index 0000000..c6ece80
--- /dev/null
+++ b/android/VulpIRC/src/main/res/values/styles.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Base application theme. -->
+ <style name="AppTheme" parent="android:Theme.Holo">
+ <!-- Customize your theme here. -->
+ </style>
+
+</resources>