content: package me.tagavari.airmessage.service;
  
  import android.app.Notification;
  import android.app.NotificationManager;
  import android.app.Service;
  import android.content.Intent;
  import android.os.Binder;
  import android.os.Handler;
  import android.os.IBinder;
  import android.os.Looper;
  import androidx.annotation.Nullable;
  import io.reactivex.rxjava3.core.Completable;
  import io.reactivex.rxjava3.disposables.CompositeDisposable;
  import io.reactivex.rxjava3.subjects.CompletableSubject;
  import me.tagavari.airmessage.connection.ConnectionManager;
  import me.tagavari.airmessage.connection.exception.AMRequestException;
  import me.tagavari.airmessage.enums.ConnectionErrorCode;
  import me.tagavari.airmessage.enums.ConnectionState;
  import me.tagavari.airmessage.enums.MessageSendErrorCode;
  import me.tagavari.airmessage.helper.NotificationHelper;
  import me.tagavari.airmessage.redux.ReduxEmitterNetwork;
  import me.tagavari.airmessage.util.ConversationTarget;
  
  import java.util.ArrayList;
  import java.util.List;
  
  public class ConnectionService extends Service {
  	//Creating the constants
  	public static final String selfIntentActionConnect = "connect";
  	public static final String selfIntentActionDisconnect = "disconnect";
  	public static final String selfIntentActionStop = "stop";
  	
  	public static final String selfIntentExtraConfig = "configuration_mode";
  	public static final String selfIntentExtraTemporary = "temporary_mode";
  	public static final String selfIntentExtraForeground = "foreground";
  	
  	private static final long temporaryModeExpiry = 10 * 1000; //10 seconds
  	
  	private static final List<PendingMessage> pendingMessageList = new ArrayList<>();
  	
  	//Creating the instance value
  	private static ConnectionService instanceReference = null;
  	
  	//Creating the binder
  	private final IBinder binder = new ConnectionBinder();
  	
  	//Creating the state values
  	private boolean isBound = false;
  	private boolean isForeground;
  	private boolean configurationMode = false;
  	private boolean temporaryMode = false;
  	private final Runnable temporaryModeRunnable = this::stopSelf;
  	
  	private boolean isInitialConnectionUpdate = true;
  	
  	//Creating the connection values
  	private ConnectionManager connectionManager;
  	
  	//Creating the disposable values
  	private final CompositeDisposable compositeDisposable = new CompositeDisposable();
  	
  	//Creating the handler
  	private final Handler handler = new Handler(Looper.getMainLooper());
  	
  	/**
  	 * Gets the current instance of the connection service
  	 */
  	@Nullable
  	public static ConnectionService getInstance() {
  		return instanceReference;
  	}
  	
  	/**
  	 * Gets the current instance of the connection service's connection manager
  	 */
  	@Nullable
  	public static ConnectionManager getConnectionManager() {
  		if(instanceReference == null) return null;
  		return instanceReference.connectionManager;
  	}
  	
  	/**
  	 * Add a message request that will be sent when the service connects
  	 * @param message The message text to send
  	 * @param conversationGUID The GUID of the conversation to send the message to
  	 * @return A completable to represent this task
  	 */
  	public static Completable addPendingMessage(String message, String conversationGUID) {
  		CompletableSubject subject = CompletableSubject.create();
  		pendingMessageList.add(new PendingMessage(subject, message, conversationGUID));
  		return subject;
  	}
  	
  	@Nullable
  	@Override
  	public IBinder onBind(Intent intent) {
  		//Updating the state value
  		isBound = true;
  		
  		//Disabling temporary mode and returning to the background
  		if(temporaryMode) {
  			setTemporaryMode(false);
  			setServiceForegroundState(false);
  		}
  		
  		//Returning access to the connection manager
  		return binder;
  	}
  	
  	@Override
  	public boolean onUnbind(Intent intent) {
  		//Updating the state value
  		isBound = false;
  		
  		//Just use onBind()
  		return false;
  	}
  	
  	@Override
  	public void onCreate() {
  		super.onCreate();
  		
  		//Initializing the connection manager
  		connectionManager = new ConnectionManager(this);
  		
  		//Setting the service
  		instanceReference = this;
  		
  		//Subscribing to updates in connection changes
  		compositeDisposable.addAll(
  				ReduxEmitterNetwork.getConnectionStateSubject().subscribe(event -> updateState(event.getState())),
  				ReduxEmitterNetwork.getConnectionConfigurationSubject().subscribe(this::setConfigurationMode)
  		);
  	}
  	
  	@Override
  	public int onStartCommand(Intent intent, int flags, int startId) {
  		if(intent != null) {
  			//Updating configuration mode
  			if(intent.hasExtra(selfIntentExtraConfig)) setConfigurationMode(intent.getBooleanExtra(selfIntentExtraConfig, false));
  			
  			//If the service is bound, don't enable temporary mode and just let Android take care of finishing the service
  			setTemporaryMode(intent.getBooleanExtra(selfIntentExtraTemporary, false) && !isBound);
  			
  			//Checking if a quit has been requested
  			if(selfIntentActionStop.equals(intent.getAction())) {
  				if(!isBound) {
  					//Shutting down the connection service
  					stopForeground(true);
  					stopSelf();
  				}
  				
  				//Returning not sticky
  				return START_NOT_STICKY;
  			} else if(selfIntentActionDisconnect.equals(intent.getAction())) {
  				//Disconnecting
  				connectionManager.disconnect(ConnectionErrorCode.user);
  			} else if(selfIntentActionConnect.equals(intent.getAction())) {
  				//Reconnecting
  				connectionManager.connect();
  			}
  			
  			//Starting the service in the foreground if required
  			if(intent.getBooleanExtra(selfIntentExtraForeground, false)) {
  				setServiceForegroundState(true);
  			}
  		}
  		
  		//Keep this service running
  		return START_STICKY;
  	}
  	
  	@Override
  	public void onDestroy() {
  		//Unsubscribing from connection updates
  		compositeDisposable.clear();
  		
  		//Cleaning up the connection manager
  		connectionManager.disconnect(ConnectionErrorCode.user);
  		connectionManager.close(this);
  		
  		//Removing the connection service reference
  		instanceReference = null;
  	}
  	
  	/**
  	 * Handles responses to a change in connection state
  	 */
  	private void updateState(@ConnectionState int state) {
  		//Updating the foreground notification
  		if(isForeground) {
  			Notification notification = getDisplayNotification(state, connectionManager.isUsingFallback());
  			NotificationManager notificationManager = getSystemService(NotificationManager.class);
  			notificationManager.notify(NotificationHelper.notificationIDConnectionService, notification);
  		}
  		
  		//Ignore connection updates triggered directly when the service attaches to the observable
  		if(!isInitialConnectionUpdate && !pendingMessageList.isEmpty()) {
  			if(state == ConnectionState.connected) {
  				//Sending any pending messages
  				for(PendingMessage pendingMessage : pendingMessageList) {
  					connectionManager.sendMessage(new ConversationTarget.AppleLinked(pendingMessage.conversationGUID), pendingMessage.message).subscribe(pendingMessage.completableSubject);
  				}
  				pendingMessageList.clear();
  			} else if(state == ConnectionState.disconnected) {
  				//Failing any pending messages
  				for(PendingMessage pendingMessage : pendingMessageList) {
  					pendingMessage.completableSubject.onError(new AMRequestException(MessageSendErrorCode.localNetwork));
  				}
  				pendingMessageList.clear();
  			}
  		}
  		
  		isInitialConnectionUpdate = false;
  	}
  	
  	/**
  	 * Updates whether this service is a foreground service or a background service
  	 */
  	public void setServiceForegroundState(boolean foreground) {
  		//Ignoring if no changes are to be made
  		if(isForeground == foreground) return;
  		
  		//Setting the value
  		isForeground = foreground;
  		
  		if(foreground) {
  			//Starting the foreground state
  			startForeground(NotificationHelper.notificationIDConnectionService, getDisplayNotification());
  		} else {
  			//Stopping the foreground state
  			stopForeground(true);
  		}
  	}
  	
  	/**
  	 * Sets if this service should update its notification to reflect configuration mode status
  	 */
  	public void setConfigurationMode(boolean configurationMode) {
  		if(this.configurationMode == configurationMode) return;
  		
  		//Updating the value
  		this.configurationMode = configurationMode;
  		
  		//Updating the foreground notification
  		if(isForeground) {
  			Notification notification = getDisplayNotification();
  			NotificationManager notificationManager = getSystemService(NotificationManager.class);
  			notificationManager.notify(NotificationHelper.notificationIDConnectionService, notification);
  		}
  	}
  	
  	/**
  	 * Sets if this service should automatically stop itself after a set amount of time
  	 */
  	public void setTemporaryMode(boolean temporaryMode) {
  		if(this.temporaryMode == temporaryMode) return;
  		
  		//Updating the value
  		this.temporaryMode = temporaryMode;
  		
  		//Updating the timer
  		if(temporaryMode) {
  			handler.postDelayed(temporaryModeRunnable, temporaryModeExpiry);
  		} else {
  			handler.removeCallbacks(temporaryModeRunnable);
  		}
  	}
  	
  	/**
  	 * Refreshes the temporary mode expiry clock
  	 */
  	public void refreshTemporaryMode() {
  		handler.removeCallbacks(temporaryModeRunnable);
  		handler.postDelayed(temporaryModeRunnable, temporaryModeExpiry);
  	}
  	
  	/**
  	 * Gets the notification to display based on the current state of the service
  	 * @return A notification to represent this service
  	 */
  	private Notification getDisplayNotification() {
  		return getDisplayNotification(connectionManager.getState(), connectionManager.isUsingFallback());
  	}
  	
  	/**
  	 * Gets the notification to display based on the current state of the service
  	 * @param connectionState The state of the connection
  	 * @param isFallback Whether the connection is a fallback connection
  	 * @return A notification to represent this service
  	 */
  	private Notification getDisplayNotification(@ConnectionState int connectionState, boolean isFallback) {
  		if(configurationMode) {
  			return NotificationHelper.getConnectionConfigurationNotification(this);
  		} if(temporaryMode) {
  			return NotificationHelper.getTemporaryModeNotification(this);
  		} else if(connectionState == ConnectionState.disconnected) {
  			return NotificationHelper.getConnectionOfflineNotification(this, false);
  		} else {
  			return NotificationHelper.getConnectionBackgroundNotification(this, connectionState == ConnectionState.connected, isFallback);
  		}
  	}
  	
  	public class ConnectionBinder extends Binder {
  		public ConnectionService getConnectionService() {
  			return ConnectionService.this;
  		}
  		
  		public ConnectionManager getConnectionManager() {
  			return connectionManager;
  		}
  	}
  	
  	private static class PendingMessage {
  		final CompletableSubject completableSubject;
  		final String message;
  		final String conversationGUID;
  		
  		public PendingMessage(CompletableSubject completableSubject, String message, String conversationGUID) {
  			this.completableSubject = completableSubject;
  			this.message = message;
  			this.conversationGUID = conversationGUID;
  		}
  	}
  }