org.sleepydragon.rgbclient.NetworkClientFragment.java Source code

Java tutorial

Introduction

Here is the source code for org.sleepydragon.rgbclient.NetworkClientFragment.java

Source

/*
 * Copyright 2015 Denver Coneybeare <denver@sleepydragon.org>
 *
 * 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 org.sleepydragon.rgbclient;

import android.app.Fragment;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.net.ConnectivityManager;
import android.net.LinkProperties;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkInfo;
import android.net.NetworkRequest;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.content.LocalBroadcastManager;

import java.util.ArrayDeque;
import java.util.List;
import java.util.Queue;
import java.util.UUID;

/**
 * A non-UI fragment that manages the network connection with the server.
 */
public class NetworkClientFragment extends Fragment {

    private static final Logger LOG = new Logger("NetworkClientFragment");

    /**
     * A suggested tag to use for this fragment with the FragmentManager.
     */
    public static final String TAG = "NetworkClient";

    private final BroadcastReceiver mRestartBroadcastReceiver = new RestartBroadcastReceiver();
    private final ConnectivityManager.NetworkCallback mNetworkConnectionListener = new NetworkConnectionListener();
    private final ClientConnection.Callback mClientConnectionCallback = new ClientConnectionCallback();

    private static final int MAX_COMMAND_HISTORY_SIZE = 1000;
    private final Queue<ColorCommand> mCommands = new ArrayDeque<>();

    private LoadSettingsAsyncTask mLoadSettingsAsyncTask;
    private SharedPreferences mSharedPreferences;
    private LocalBroadcastManager mLocalBroadcastManager;
    private Handler mHandler;
    private ConnectivityManager mConnectivityManager;
    private TargetFragmentCallbacks mTargetFragmentCallbacks;

    private final Object mClientConnectionThreadMutex = new Object();
    private volatile ClientConnectionThread mClientConnectionThread;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        LOG.v("onCreate()");
        super.onCreate(savedInstanceState);

        // ensure that this fragment doesn't get destroyed due to configuration changes, as that
        // would cause the network connection to have to be re-established every time
        setRetainInstance(true);

        mHandler = new Handler(new MainHandlerCallback());

        mConnectivityManager = (ConnectivityManager) getActivity().getSystemService(Context.CONNECTIVITY_SERVICE);
        final NetworkRequest.Builder networkRequestBuilder = new NetworkRequest.Builder();
        networkRequestBuilder.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
        final NetworkRequest networkRequest = networkRequestBuilder.build();
        mConnectivityManager.registerNetworkCallback(networkRequest, mNetworkConnectionListener);

        mLocalBroadcastManager = LocalBroadcastManager.getInstance(getActivity());
        final IntentFilter restartReceiverFilter = new IntentFilter();
        restartReceiverFilter.addAction(Settings.ACTION_SERVER_INFO_CHANGED);
        mLocalBroadcastManager.registerReceiver(mRestartBroadcastReceiver, restartReceiverFilter);

        mLoadSettingsAsyncTask = new LoadSettingsAsyncTask(getActivity());
        mLoadSettingsAsyncTask.execute();
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        LOG.v("onActivityCreated()");
        super.onActivityCreated(savedInstanceState);
        mTargetFragmentCallbacks = (TargetFragmentCallbacks) getTargetFragment();
    }

    @Override
    public void onDestroy() {
        LOG.v("onDestroy()");
        super.onDestroy();

        stopClient();
        mLoadSettingsAsyncTask.cancel(false);
        mLocalBroadcastManager.unregisterReceiver(mRestartBroadcastReceiver);
        mHandler.removeMessages(R.id.MSG_START_CLIENT);
        mHandler.removeMessages(R.id.MSG_STOP_CLIENT);
        mConnectivityManager.unregisterNetworkCallback(mNetworkConnectionListener);
    }

    @Override
    public void onDetach() {
        super.onDetach();
        mTargetFragmentCallbacks = null;
    }

    private void startClient() {
        LOG.d("startClient()");

        final Context context = getActivity();
        if (context == null) {
            LOG.w("startClient(): getActivity() returned null; aborting");
            return;
        }

        final SharedPreferences prefs = mSharedPreferences;
        if (prefs == null) {
            LOG.w("startClient(): mSharedPreferences==null; aborting");
            return;
        }

        final String hostKey = Settings.getServerHostKey(context);
        final String host = prefs.getString(hostKey, null);
        if (host == null) {
            LOG.w("startClient(): server host name not set in SharedPreferences; aborting");
            if (mTargetFragmentCallbacks != null) {
                mTargetFragmentCallbacks.showSetServerDialog();
            }
            return;
        }

        final String portKey = Settings.getServerPortKey(context);
        final int port = prefs.getInt(portKey, -1);
        if (port == -1) {
            if (mTargetFragmentCallbacks != null) {
                mTargetFragmentCallbacks.showSetServerDialog();
            }
            LOG.w("startClient(): server port not set in SharedPreferences; aborting");
            return;
        }

        final NetworkInfo networkInfo = mConnectivityManager.getActiveNetworkInfo();
        if (networkInfo == null) {
            LOG.w("startClient(): no network connection available; aborting");
            return;
        } else if (!networkInfo.isConnected()) {
            LOG.w("startClient(): the default network is not currently connected; aborting");
            return;
        }

        synchronized (mClientConnectionThreadMutex) {
            if (mClientConnectionThread != null) {
                final ClientConnection connection = mClientConnectionThread.getConnection();
                if (!connection.isStopRequested() && host.equals(connection.getHost())
                        && port == connection.getPort()) {
                    if (connection.isConnected()) {
                        LOG.w("startClient(): already connected to the server; aborting");
                    } else {
                        LOG.w("startClient(): establishing connection to the server; "
                                + "will check back again shortly to make sure it succeeded");
                        mHandler.removeMessages(R.id.MSG_START_CLIENT);
                        mHandler.sendEmptyMessageDelayed(R.id.MSG_START_CLIENT, 1000);
                    }
                    return;
                }
                connection.requestStop();
                mClientConnectionThread.interrupt();
                mClientConnectionThread = null;
            }

            final ClientConnection connection = new ClientConnection(host, port, mClientConnectionCallback);
            final ClientConnectionThread thread = new ClientConnectionThread(connection);
            thread.start();

            mClientConnectionThread = thread;
        }

        LOG.d("startClient(): started connection with server " + host + ":" + port);
    }

    private void stopClient() {
        LOG.d("stopClient()");

        final ClientConnectionThread thread;
        synchronized (mClientConnectionThreadMutex) {
            thread = mClientConnectionThread;
            mClientConnectionThread = null;
        }
        if (thread != null) {
            final ClientConnection connection = thread.getConnection();
            connection.requestStop();
            thread.interrupt();
        }
    }

    private void scheduleStartClient() {
        mHandler.removeMessages(R.id.MSG_START_CLIENT);
        mHandler.removeMessages(R.id.MSG_STOP_CLIENT);
        mHandler.sendEmptyMessage(R.id.MSG_START_CLIENT);
    }

    public void restart() {
        mHandler.removeMessages(R.id.MSG_START_CLIENT);
        mHandler.removeMessages(R.id.MSG_STOP_CLIENT);
        stopClient();
        startClient();
    }

    public void getCommandsSince(@Nullable UUID id, @NonNull List<ColorCommand> commands) {
        boolean idFound = false;
        synchronized (mCommands) {
            if (id != null) {
                for (final ColorCommand command : mCommands) {
                    if (idFound) {
                        commands.add(command);
                    } else if (command.id.equals(id)) {
                        idFound = true;
                    }
                }
            }

            // if the given ID was not found, then it must have fallen off the end; so treat all
            // command as being new
            if (!idFound) {
                commands.addAll(mCommands);
            }
        }
    }

    private class LoadSettingsAsyncTask extends Settings.GetSharedPreferencesAsyncTask {

        public LoadSettingsAsyncTask(@NonNull Context context) {
            super(context);
        }

        @Override
        protected void onPostExecute(SharedPreferences sharedPreferences) {
            LOG.d("LoadSettingsAsyncTask.onPostExecute() SharedPreferences loaded");
            if (isCancelled()) {
                return;
            }
            mSharedPreferences = sharedPreferences;
            scheduleStartClient();
        }

    }

    /**
     * The broadcast receiver that is registered with LocalBroadcastManager to restart the server
     * when the settings change.
     */
    private class RestartBroadcastReceiver extends BroadcastReceiver {

        @Override
        public void onReceive(Context context, Intent intent) {
            LOG.d("RestartBroadcastReceiver.onReceive() intent=" + intent);
            if (intent == null) {
                return;
            }
            final String action = intent.getAction();
            if (Settings.ACTION_SERVER_INFO_CHANGED.equals(action)) {
                scheduleStartClient();
            }
        }

    }

    private class MainHandlerCallback implements Handler.Callback {

        @Override
        public boolean handleMessage(Message msg) {
            switch (msg.what) {
            case R.id.MSG_START_CLIENT:
                startClient();
                return true;
            case R.id.MSG_STOP_CLIENT:
                stopClient();
                return true;
            default:
                return false;
            }
        }

    }

    private class NetworkConnectionListener extends ConnectivityManager.NetworkCallback {

        @Override
        public void onAvailable(Network network) {
            LOG.d("NetworkConnectionListener.onAvailable() network=" + network);
            scheduleStartClient();
        }

        @Override
        public void onLosing(Network network, int maxMsToLive) {
            LOG.d("NetworkConnectionListener.onLosing() network=" + network + " maxMsToLive=" + maxMsToLive);
        }

        @Override
        public void onLost(Network network) {
            LOG.d("NetworkConnectionListener.onLost() network=" + network);
        }

        @Override
        public void onCapabilitiesChanged(Network network, NetworkCapabilities networkCapabilities) {
            LOG.d("NetworkConnectionListener.onCapabilitiesChanged() network=" + network + " networkCapabilities="
                    + networkCapabilities);
            scheduleStartClient();
        }

        @Override
        public void onLinkPropertiesChanged(Network network, LinkProperties linkProperties) {
            LOG.d("NetworkConnectionListener.onLinkPropertiesChanged() network=" + network + " linkProperties="
                    + linkProperties);
            scheduleStartClient();
        }

    }

    private static class ClientConnectionThread extends Thread {

        @NonNull
        private final ClientConnection mConnection;

        public ClientConnectionThread(@NonNull ClientConnection connection) {
            super(connection);
            mConnection = connection;
        }

        @NonNull
        public ClientConnection getConnection() {
            return mConnection;
        }

    }

    /**
     * An interface to be implemented by the target fragment of this fragment to allow this fragment
     * to make demands on it.
     */
    public interface TargetFragmentCallbacks {

        /**
         * Show the dialog that allows the user to enter the server information.
         */
        void showSetServerDialog();

        /**
         * Process a command received from the server.
         * @param command the command that was received; will never be null.
         */
        void onCommandReceived(@NonNull ColorCommand command);

    }

    private class ClientConnectionCallback implements ClientConnection.Callback {

        @Override
        public void connectionStateChanged(@NonNull ClientConnection connection, boolean connected) {
            LOG.d("ClientConnectionCallback: connectionStateChanged() connected=" + connected);
            if (!connected) {
                clearConnection(connection);
            }
        }

        @Override
        public void connectionError(@NonNull ClientConnection connection, @NonNull ConnectionError error,
                @NonNull String message) {
            LOG.w("ClientConnectionCallback: connectionError() error=" + error + " message=" + message);
            clearConnection(connection);
        }

        @Override
        public void commandReceived(@NonNull ClientConnection connection, @NonNull ColorCommand command) {
            if (isActiveConnection(connection)) {
                LOG.d("ClientConnectionCallback: commandReceived() command=" + command);
                final TargetFragmentCallbacks cb = mTargetFragmentCallbacks;
                if (cb != null) {
                    cb.onCommandReceived(command);
                }
                synchronized (mCommands) {
                    mCommands.offer(command);
                    if (mCommands.size() > MAX_COMMAND_HISTORY_SIZE) {
                        mCommands.poll();
                    }
                }
            }
        }

        private boolean isActiveConnection(@NonNull ClientConnection connection) {
            synchronized (mClientConnectionThreadMutex) {
                return (mClientConnectionThread != null && mClientConnectionThread.getConnection() == connection);
            }
        }

        private void clearConnection(@NonNull ClientConnection connection) {
            connection.requestStop();
            synchronized (mClientConnectionThreadMutex) {
                if (mClientConnectionThread != null && mClientConnectionThread.getConnection() == connection) {
                    mClientConnectionThread = null;
                }
            }
        }

    }

}