Java tutorial
/* * Copyright (C) 2016 Dennis Dast * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package de.ddast.xandra; import android.content.Context; import android.content.SharedPreferences; import android.os.AsyncTask; import android.os.CountDownTimer; import android.os.Handler; import android.os.PowerManager; import android.preference.PreferenceManager; import android.support.v4.view.MotionEventCompat; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.text.Editable; import android.text.TextWatcher; import android.util.Log; import android.view.MotionEvent; import android.view.View; import android.view.WindowManager; import android.view.inputmethod.InputMethodManager; import android.widget.Button; import android.widget.HorizontalScrollView; import junit.framework.Assert; import java.io.IOException; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.net.Socket; import java.net.UnknownHostException; import java.nio.ByteBuffer; public class MainActivity extends AppCompatActivity { private static final String TAG = "xandra"; private static final long HEARTBEAT_INTERVAL = 1000L; private static final byte HEARTBEAT = (byte) 0x00; private static final byte LEFTCLICK = (byte) 0x00; private static final byte RIGHTCLICK = (byte) 0x01; private static final byte WHEELUP = (byte) 0x02; private static final byte WHEELDOWN = (byte) 0x03; private static final byte CTRL = (byte) 0x04; private static final byte SUP = (byte) 0x05; private static final byte ALT = (byte) 0x06; private static final byte BACKSPACE = (byte) 0x07; private static final byte ESCAPE = (byte) 0x08; private static final byte TAB = (byte) 0x09; private static final byte LEFT = (byte) 0x0a; private static final byte DOWN = (byte) 0x0b; private static final byte UP = (byte) 0x0c; private static final byte RIGHT = (byte) 0x0d; private static final byte VOLDN = (byte) 0x0e; private static final byte VOLUP = (byte) 0x0f; private static final byte VOLTOG = (byte) 0x10; private static final byte INS = (byte) 0x11; private static final byte DEL = (byte) 0x12; private static final byte HOME = (byte) 0x13; private static final byte END = (byte) 0x14; private static final byte PGUP = (byte) 0x15; private static final byte PGDN = (byte) 0x16; private static final byte F1 = (byte) 0x17; private static final byte F2 = (byte) 0x18; private static final byte F3 = (byte) 0x19; private static final byte F4 = (byte) 0x1a; private static final byte F5 = (byte) 0x1b; private static final byte F6 = (byte) 0x1c; private static final byte F7 = (byte) 0x1d; private static final byte F8 = (byte) 0x1e; private static final byte F9 = (byte) 0x1f; private static final byte F10 = (byte) 0x20; private static final byte F11 = (byte) 0x21; private static final byte F12 = (byte) 0x22; private int mPort; private long mTapdelay; private float mTaptol, mSensitivity, mAcceleration, mScrollThreshold; private int mSpecialKeysVisibility = View.GONE; private NoCursorEditText mBufferEdit; private HorizontalScrollView mLayoutKeys; private Button mToggleButton; private String mServerAddr; private Socket mSocket; private OutputStream mOutput; private Handler mHandler; private Runnable mSendHeartbeat; private MouseGestureDetector mMouseGestureDetector; private PowerManager mPowerManager; @Override protected void onCreate(Bundle savedInstanceState) { SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); String theme = sharedPreferences.getString(this.getString(R.string.pref_theme), ""); if (theme.equals("dark")) { setTheme(R.style.DarkThemeNoActionbar); } else if (theme.equals("light")) { setTheme(R.style.LightThemeNoActionbar); } super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mPowerManager = (PowerManager) getSystemService(POWER_SERVICE); if (sharedPreferences.getBoolean(this.getString(R.string.pref_lock), false)) { getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); } mPort = Integer.valueOf(sharedPreferences.getString(this.getString(R.string.pref_port), "")); mTapdelay = Long.valueOf(sharedPreferences.getString(this.getString(R.string.pref_tapdelay), "")); mTaptol = Float.valueOf(sharedPreferences.getString(this.getString(R.string.pref_taptol), "")); mSensitivity = Float.valueOf(sharedPreferences.getString(this.getString(R.string.pref_sensitivity), "")); mAcceleration = Float.valueOf(sharedPreferences.getString(this.getString(R.string.pref_acceleration), "")); mScrollThreshold = Float .valueOf(sharedPreferences.getString(this.getString(R.string.pref_scrollthreshold), "")); mServerAddr = getIntent().getStringExtra(ConnectActivity.SERVERADDR); mBufferEdit = (NoCursorEditText) findViewById(R.id.buffer_edit); mHandler = new Handler(); mSendHeartbeat = new SendHeartbeat(); mBufferEdit.setHorizontallyScrolling(true); // does not work in xml for some reason mBufferEdit.addTextChangedListener(new AddedTextWatcher()); mMouseGestureDetector = new MouseGestureDetector(); initKeyboardButtons(); } private void initKeyboardButtons() { mLayoutKeys = (HorizontalScrollView) findViewById(R.id.layout_keys); Assert.assertNotNull(mLayoutKeys); mToggleButton = (Button) findViewById(R.id.button_togglekeys); Assert.assertNotNull(mToggleButton); mToggleButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mSpecialKeysVisibility = mLayoutKeys.getVisibility() == View.VISIBLE ? View.GONE : View.VISIBLE; mLayoutKeys.setVisibility(mSpecialKeysVisibility); } }); Button escButton = (Button) findViewById(R.id.button_esc); Assert.assertNotNull(escButton); escButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { sendSpecialKey(ESCAPE); } }); Button tabButton = (Button) findViewById(R.id.button_tab); Assert.assertNotNull(tabButton); tabButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { sendSpecialKey(TAB); } }); Button ctrlButton = (Button) findViewById(R.id.button_ctrl); Assert.assertNotNull(ctrlButton); ctrlButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { sendSpecialKey(CTRL); } }); Button supButton = (Button) findViewById(R.id.button_sup); Assert.assertNotNull(supButton); supButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { sendSpecialKey(SUP); } }); Button altButton = (Button) findViewById(R.id.button_alt); Assert.assertNotNull(altButton); altButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { sendSpecialKey(ALT); } }); Button leftButton = (Button) findViewById(R.id.button_left); Assert.assertNotNull(leftButton); leftButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { sendSpecialKey(LEFT); } }); Button downButton = (Button) findViewById(R.id.button_down); Assert.assertNotNull(downButton); downButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { sendSpecialKey(DOWN); } }); Button upButton = (Button) findViewById(R.id.button_up); Assert.assertNotNull(upButton); upButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { sendSpecialKey(UP); } }); Button rightButton = (Button) findViewById(R.id.button_right); Assert.assertNotNull(rightButton); rightButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { sendSpecialKey(RIGHT); } }); Button voldnButton = (Button) findViewById(R.id.button_voldn); Assert.assertNotNull(voldnButton); voldnButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { sendSpecialKey(VOLDN); } }); Button volupButton = (Button) findViewById(R.id.button_volup); Assert.assertNotNull(volupButton); volupButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { sendSpecialKey(VOLUP); } }); Button voltogButton = (Button) findViewById(R.id.button_voltog); Assert.assertNotNull(voltogButton); voltogButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { sendSpecialKey(VOLTOG); } }); Button insButton = (Button) findViewById(R.id.button_ins); Assert.assertNotNull(insButton); insButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { sendSpecialKey(INS); } }); Button delButton = (Button) findViewById(R.id.button_del); Assert.assertNotNull(delButton); delButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { sendSpecialKey(DEL); } }); Button homeButton = (Button) findViewById(R.id.button_home); Assert.assertNotNull(homeButton); homeButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { sendSpecialKey(HOME); } }); Button endButton = (Button) findViewById(R.id.button_end); Assert.assertNotNull(endButton); endButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { sendSpecialKey(END); } }); Button pgupButton = (Button) findViewById(R.id.button_pgup); Assert.assertNotNull(pgupButton); pgupButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { sendSpecialKey(PGUP); } }); Button pgdnButton = (Button) findViewById(R.id.button_pgdn); Assert.assertNotNull(pgdnButton); pgdnButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { sendSpecialKey(PGDN); } }); Button f1tButton = (Button) findViewById(R.id.button_f1); Assert.assertNotNull(f1tButton); f1tButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { sendSpecialKey(F1); } }); Button f2Button = (Button) findViewById(R.id.button_f2); Assert.assertNotNull(f2Button); f2Button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { sendSpecialKey(F2); } }); Button f3Button = (Button) findViewById(R.id.button_f3); Assert.assertNotNull(f3Button); f3Button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { sendSpecialKey(F3); } }); Button f4Button = (Button) findViewById(R.id.button_f4); Assert.assertNotNull(f4Button); f4Button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { sendSpecialKey(F4); } }); Button f5Button = (Button) findViewById(R.id.button_f5); Assert.assertNotNull(f5Button); f5Button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { sendSpecialKey(F5); } }); Button f6Button = (Button) findViewById(R.id.button_f6); Assert.assertNotNull(f6Button); f6Button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { sendSpecialKey(F6); } }); Button f7Button = (Button) findViewById(R.id.button_f7); Assert.assertNotNull(f7Button); f7Button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { sendSpecialKey(F7); } }); Button f8Button = (Button) findViewById(R.id.button_f8); Assert.assertNotNull(f8Button); f8Button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { sendSpecialKey(F8); } }); Button f9Button = (Button) findViewById(R.id.button_f9); Assert.assertNotNull(f9Button); f9Button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { sendSpecialKey(F9); } }); Button f10Button = (Button) findViewById(R.id.button_f10); Assert.assertNotNull(f10Button); f10Button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { sendSpecialKey(F10); } }); Button f11Button = (Button) findViewById(R.id.button_f11); Assert.assertNotNull(f11Button); f11Button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { sendSpecialKey(F11); } }); Button f12Button = (Button) findViewById(R.id.button_f12); Assert.assertNotNull(f12Button); f12Button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { sendSpecialKey(F12); } }); } @Override public boolean onTouchEvent(MotionEvent event) { if (mBufferEdit.isEnabled()) { return mMouseGestureDetector.processTouchEvent(event) || super.onTouchEvent(event); } else { return super.onTouchEvent(event); } } @Override protected void onPause() { super.onPause(); setUiToDisconnected(); mHandler.removeCallbacks(mSendHeartbeat); if (mSocket == null) { return; } try { Log.i(TAG, "Closing connection"); mSocket.close(); } catch (IOException e) { Log.e(TAG, "IO error while closing"); } } @Override protected void onResume() { super.onResume(); new ConnectToServer().execute(); } private void sendMouse(int distanceX, int distanceY) { boolean isNegX = distanceX < 0; boolean isNegY = distanceY < 0; distanceX = Math.abs(distanceX) & 0xfff; distanceY = Math.abs(distanceY) & 0xfff; byte[] bA = ByteBuffer.allocate(5).put((byte) (0xf8 | (isNegX ? 0x02 : 0x00) | distanceX >>> 11)) .put((byte) (0x80 | distanceX >>> 5 & 0x3f)) .put((byte) (0x80 | (distanceX & 0x1f) << 1 | (isNegY ? 0x01 : 0x00))) .put((byte) (0x80 | distanceY >>> 6)).put((byte) (0x80 | distanceY & 0x3f)).array(); new SendBytes().execute(bA); } private void sendUTF8(String s) { try { byte[] utf8Repr = s.getBytes("UTF8"); new SendBytes().execute(utf8Repr); } catch (UnsupportedEncodingException e) { Log.e(TAG, "Encoding failure"); } } private void sendSpecialKey(byte b) { new SendBytes().execute( new byte[] { (byte) 0xfc, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) (0x80 | b) }); } private void setUiToDisconnected() { mBufferEdit.setEnabled(false); mBufferEdit.setText(R.string.notconnected); mLayoutKeys.setVisibility(View.GONE); mToggleButton.setEnabled(false); } private void setUiToConnected() { mBufferEdit.setEnabled(true); mBufferEdit.setText(" "); showSoftKeyboard(mBufferEdit); mToggleButton.setEnabled(true); } private boolean isConnected() { return mBufferEdit.isEnabled(); } private void showSoftKeyboard(View view) { if (view.requestFocus()) { InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT); } if (mSpecialKeysVisibility == View.VISIBLE) { mLayoutKeys = (HorizontalScrollView) findViewById(R.id.layout_keys); Assert.assertNotNull(mLayoutKeys); mLayoutKeys.setVisibility(View.VISIBLE); } } private class SendBytes extends AsyncTask<byte[], Void, Boolean> { @Override protected Boolean doInBackground(byte[]... bA) { if (mOutput == null) { Log.e(TAG, "Tried to send, but not yet connected"); return false; } try { mOutput.write(bA[0]); } catch (IOException e) { Log.e(TAG, "IO error while sending"); return false; } return true; } @Override protected void onPostExecute(Boolean result) { if (!result) { setUiToDisconnected(); } } } private class ConnectToServer extends AsyncTask<Void, Void, Boolean> { @Override protected void onPreExecute() { Log.i(TAG, "Connecting to " + mServerAddr); setUiToDisconnected(); } @Override protected Boolean doInBackground(Void... params) { try { mSocket = new Socket(mServerAddr, mPort); mSocket.setTcpNoDelay(true); mOutput = mSocket.getOutputStream(); Log.i(TAG, "Connected to " + mServerAddr); return true; } catch (UnknownHostException e) { Log.e(TAG, "Unknown host: " + mServerAddr); return false; } catch (IOException e) { Log.e(TAG, "IO error while connecting"); return false; } } @Override protected void onPostExecute(Boolean result) { if (result) { setUiToConnected(); } else { setUiToDisconnected(); } mHandler.postDelayed(mSendHeartbeat, HEARTBEAT_INTERVAL); } } private class SendHeartbeat implements Runnable { @Override public void run() { if (mPowerManager.isScreenOn()) { new SendBytes().execute(new byte[] { HEARTBEAT }); } if (!isConnected()) { Log.e(TAG, "Heartbeat error, try to reconnect"); new ConnectToServer().execute(); } else { mHandler.postDelayed(mSendHeartbeat, HEARTBEAT_INTERVAL); } } } private class AddedTextWatcher implements TextWatcher { private boolean ignore = false; @Override public void afterTextChanged(Editable s) { // s must not be empty. It always contains one space to detect further backspace input. if (s.toString().contains("\n") || s.length() == 0) { ignore = true; s.replace(0, s.length(), " ", 0, 1); } } @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { if (ignore) { ignore = false; return; } if (before == 0) { sendUTF8(s.subSequence(start, start + count).toString()); } else if (count == 0) { for (int i = 0; i < before; ++i) { sendSpecialKey(BACKSPACE); } } } } private class MouseGestureDetector { private int mPointerID1 = MotionEvent.INVALID_POINTER_ID; private int mPointerID2 = MotionEvent.INVALID_POINTER_ID; private float initX, initY, mOldX, mOldY, mOldY2; private double accumulatedDiffY; private long mDownEventTime, oldTime; boolean isMultiTouchGesture; private double acceleratedMouseMovement(float len, long time) { double velocity = (len < 0.0f ? -1.0 : 1.0) * Math.pow(Math.abs(10.0 * len / time), mAcceleration); return velocity * time / 10.0; } private int calcMouseMovement(float len, long time) { return (int) Math.round(mSensitivity * acceleratedMouseMovement(len, time)); } private void initFirstPointer(MotionEvent event) { final int pointerIndex = event.getActionIndex(); mPointerID1 = event.getPointerId(pointerIndex); initX = mOldX = event.getX(pointerIndex); initY = mOldY = event.getY(pointerIndex); mDownEventTime = oldTime = event.getEventTime(); } private void initSecondPointer(MotionEvent event) { final int pointerIndex = event.getActionIndex(); mPointerID2 = event.getPointerId(pointerIndex); mOldY2 = event.getY(pointerIndex); accumulatedDiffY = 0.0; } private void sendMouseOrScrollEvent(MotionEvent event) { final int pointerIndex = event.findPointerIndex(mPointerID1); float diffX = event.getX(pointerIndex) - mOldX; float diffY = event.getY(pointerIndex) - mOldY; mOldX = event.getX(pointerIndex); mOldY = event.getY(pointerIndex); long diffT = event.getEventTime() - oldTime; oldTime = event.getEventTime(); if (event.getPointerCount() == 1) { sendMouse(calcMouseMovement(diffX, diffT), calcMouseMovement(diffY, diffT)); } else if (event.getPointerCount() == 2) { final int pointerIndex2 = event.findPointerIndex(mPointerID2); float diffY2 = event.getY(pointerIndex2) - mOldY2; mOldY2 = event.getY(pointerIndex2); float maxDiffY = Math.abs(diffY) > Math.abs(diffY2) ? diffY : diffY2; accumulatedDiffY += acceleratedMouseMovement(maxDiffY, diffT); while (accumulatedDiffY < -mScrollThreshold) { sendSpecialKey(WHEELUP); accumulatedDiffY += mScrollThreshold; } while (accumulatedDiffY > mScrollThreshold) { sendSpecialKey(WHEELDOWN); accumulatedDiffY -= mScrollThreshold; } } } private void rearrangePointerIDs(MotionEvent event) { final int pointerIndex = event.getActionIndex(); final int pointerId = event.getPointerId(pointerIndex); if (pointerId != mPointerID1 && pointerId != mPointerID2) { return; } if (pointerId == mPointerID1) { mPointerID1 = mPointerID2; final int pointerIndex1 = event.findPointerIndex(mPointerID1); mOldX = event.getX(pointerIndex1); mOldY = event.getY(pointerIndex1); mPointerID2 = MotionEvent.INVALID_POINTER_ID; } if (event.getPointerCount() > 2) { final int pointerIndex1 = event.findPointerIndex(mPointerID1); int newPointerIndex = MotionEvent.INVALID_POINTER_ID; for (int i = 0; i < event.getPointerCount(); ++i) { if (i != pointerIndex && i != pointerIndex1) { newPointerIndex = i; break; } } mPointerID2 = event.getPointerId(newPointerIndex); mOldY2 = event.getY(newPointerIndex); } } private boolean isSingleTouchTap(MotionEvent event) { final int pointerIndex = event.findPointerIndex(mPointerID1); return (!isMultiTouchGesture && (Math.abs(event.getX(pointerIndex) - initX) < mTaptol) && (Math.abs(event.getY(pointerIndex) - initY) < mTaptol) && (event.getEventTime() - mDownEventTime < mTapdelay)); } private boolean singleTouchHasNotMoved() { return (!isMultiTouchGesture && (Math.abs(mOldX - initX) < mTaptol) && (Math.abs(mOldY - initY) < mTaptol)); } private CountDownTimer rightClickCountDown = new CountDownTimer(2 * mTapdelay, 3 * mTapdelay) { @Override public void onTick(long millisUntilFinished) { } @Override public void onFinish() { if (singleTouchHasNotMoved()) { sendSpecialKey(RIGHTCLICK); } } }; private boolean processTouchEvent(MotionEvent event) { int action = MotionEventCompat.getActionMasked(event); switch (action) { case (MotionEvent.ACTION_DOWN): { isMultiTouchGesture = false; initFirstPointer(event); rightClickCountDown.start(); return true; } case (MotionEvent.ACTION_POINTER_DOWN): { isMultiTouchGesture = true; if (event.getPointerCount() == 2) { initSecondPointer(event); } return true; } case (MotionEvent.ACTION_MOVE): { sendMouseOrScrollEvent(event); return true; } case (MotionEvent.ACTION_POINTER_UP): { rearrangePointerIDs(event); return true; } case (MotionEvent.ACTION_UP): { rightClickCountDown.cancel(); if (isSingleTouchTap(event)) { sendSpecialKey(LEFTCLICK); } mPointerID1 = MotionEvent.INVALID_POINTER_ID; return true; } case (MotionEvent.ACTION_CANCEL): { rightClickCountDown.cancel(); mPointerID1 = MotionEvent.INVALID_POINTER_ID; mPointerID2 = MotionEvent.INVALID_POINTER_ID; return true; } default: return false; } } } }