content: package me.tagavari.airmessage.connection.comm5;
  
  import android.os.Build;
  
  import androidx.annotation.NonNull;
  
  import io.reactivex.rxjava3.core.Observable;
  import me.tagavari.airmessage.MainApplication;
  import me.tagavari.airmessage.common.Blocks;
  import me.tagavari.airmessage.connection.DataProxy;
  import me.tagavari.airmessage.connection.MassRetrievalParams;
  import me.tagavari.airmessage.connection.encryption.EncryptionAES;
  import me.tagavari.airmessage.connection.exception.AMRequestException;
  import me.tagavari.airmessage.connection.exception.LargeAllocationException;
  import me.tagavari.airmessage.constants.MIMEConstants;
  import me.tagavari.airmessage.data.SharedPreferencesManager;
  import me.tagavari.airmessage.enums.*;
  import me.tagavari.airmessage.flavor.CrashlyticsBridge;
  import me.tagavari.airmessage.helper.LookAheadStreamIterator;
  import me.tagavari.airmessage.helper.StandardCompressionHelper;
  import me.tagavari.airmessage.helper.StringHelper;
  import me.tagavari.airmessage.redux.ReduxEventAttachmentUpload;
  import me.tagavari.airmessage.util.CompoundErrorDetails;
  import me.tagavari.airmessage.util.ConversationTarget;
  
  import java.io.*;
  import java.nio.BufferOverflowException;
  import java.nio.BufferUnderflowException;
  import java.security.DigestInputStream;
  import java.security.GeneralSecurityException;
  import java.security.MessageDigest;
  import java.security.NoSuchAlgorithmException;
  import java.util.ArrayList;
  import java.util.Collection;
  import java.util.List;
  import java.util.zip.DeflaterInputStream;
  import java.util.zip.InflaterOutputStream;
  
  public class ClientProtocol4 extends ProtocolManager<EncryptedPacket> {
  	private static final String hashAlgorithm = "MD5";
  	private static final String platformID = "android";
  	
  	private static final int attachmentChunkSize = 1024 * 1024; //1 MB
  	
  	//Top-level net header type values
  	private static final int nhtClose = 0;
  	private static final int nhtPing = 1;
  	private static final int nhtPong = 2;
  	
  	private static final int nhtAuthentication = 101;
  	
  	private static final int nhtMessageUpdate = 200;
  	private static final int nhtTimeRetrieval = 201;
  	private static final int nhtIDRetrieval = 202;
  	private static final int nhtMassRetrieval = 203;
  	private static final int nhtMassRetrievalFile = 204;
  	private static final int nhtMassRetrievalFinish = 205;
  	private static final int nhtConversationUpdate = 206;
  	private static final int nhtModifierUpdate = 207;
  	private static final int nhtAttachmentReq = 208;
  	private static final int nhtAttachmentReqConfirm = 209;
  	private static final int nhtAttachmentReqFail = 210;
  	private static final int nhtIDUpdate = 211;
  	
  	private static final int nhtLiteConversationRetrieval = 300;
  	private static final int nhtLiteThreadRetrieval = 301;
  	
  	private static final int nhtSendResult = 400;
  	private static final int nhtSendTextExisting = 401;
  	private static final int nhtSendTextNew = 402;
  	private static final int nhtSendFileExisting = 403;
  	private static final int nhtSendFileNew = 404;
  	private static final int nhtCreateChat = 405;
  	
  	//Net return codes
  	private static final int nrcSharedOK = 0;
  	
  	//Item sub-types
  	private static final int nstMessageStateIdle = 0;
  	private static final int nstMessageStateSent = 1;
  	private static final int nstMessageStateDelivered = 2;
  	private static final int nstMessageStateRead = 3;
  	
  	private static final int nstItemMessage = 0;
  	private static final int nstItemGroupAction = 1;
  	private static final int nstItemChatRename = 2;
  	
  	private static final int nstGroupActionUnknown = 0;
  	private static final int nstGroupActionJoin = 1;
  	private static final int nstGroupActionLeave = 2;
  	
  	private static final int nstModifierActivity = 0;
  	private static final int nstModifierSticker = 1;
  	private static final int nstModifierTapback = 2;
  	
  	private short lastMassRetrievalRequestID = -1;
  	
  	ClientProtocol4(ClientComm5 communicationsManager, DataProxy<EncryptedPacket> dataProxy) {
  		super(communicationsManager, dataProxy);
  	}
  	
  	private boolean queueHeaderOnlyPacket(int header, boolean shouldEncrypt) {
  		try(AirPacker packer = AirPacker.get()) {
  			packer.packInt(header);
  			
  			dataProxy.send(new EncryptedPacket(packer.toByteArray(), shouldEncrypt));
  			
  			return true;
  		} catch(BufferOverflowException exception) {
  			exception.printStackTrace();
  			
  			return false;
  		}
  	}
  	
  	@Override
  	void processData(byte[] data, boolean wasEncrypted) {
  		//Wrapping the data in an unpacker
  		AirUnpacker unpacker = new AirUnpacker(data);
  		try {
  			//Reading the message type
  			int messageType = unpacker.unpackInt();
  			
  			//Checking if this data is insecure
  			if(!wasEncrypted) {
  				processDataInsecure(messageType, unpacker);
  			} else {
  				//This data is always encrypted
  				processDataSecure(messageType, unpacker);
  			}
  		} catch(BufferUnderflowException | LargeAllocationException | IllegalArgumentException exception) {
  			exception.printStackTrace();
  		}
  	}
  	
  	@Override
  	boolean sendPing() {
  		return queueHeaderOnlyPacket(nhtPing, false);
  	}
  	
  	/**
  	 * Process data that doesn't need to be encrypted
  	 * @param messageType The message header
  	 * @param unpacker The message data
  	 * @return TRUE if this message was consumed
  	 */
  	private boolean processDataInsecure(int messageType, AirUnpacker unpacker) {
  		switch(messageType) {
  			case nhtClose:
  				//Closing the connection
  				communicationsManager.getHandler().post(() -> communicationsManager.disconnect(ConnectionErrorCode.connection));
  				break;
  			case nhtPing:
  				//Replying with a pong
  				queueHeaderOnlyPacket(nhtPong, false);
  				break;
  			case nhtAuthentication:
  				handleMessageAuthentication(unpacker);
  				break;
  			default:
  				//Message not consumed
  				return false;
  		}
  		
  		return true;
  	}
  	
  	/**
  	 * Process data that does need to be encrypted
  	 * This method also invokes {@link #processDataInsecure(int, AirUnpacker)} method
  	 * @param messageType The message header
  	 * @param unpacker The message data
  	 * @return TRUE if this message was consumed
  	 */
  	private boolean processDataSecure(int messageType, AirUnpacker unpacker) {
  		if(processDataInsecure(messageType, unpacker)) return true;
  		
  		switch(messageType) {
  			case nhtMessageUpdate:
  			case nhtTimeRetrieval:
  			case nhtIDRetrieval:
  				handleMessageMessageUpdate(unpacker);
  				break;
  			case nhtMassRetrieval:
  				handleMessageMassRetrieval(unpacker);
  				break;
  			case nhtMassRetrievalFinish:
  				handleMessageMassRetrievalFinish(unpacker);
  				break;
  			case nhtMassRetrievalFile:
  				handleMessageMassRetrievalFile(unpacker);
  				break;
  			case nhtConversationUpdate:
  				handleMessageConversationUpdate(unpacker);
  				break;
  			case nhtModifierUpdate:
  				handleMessageModifierUpdate(unpacker);
  				break;
  			case nhtAttachmentReq:
  				handleMessageAttachmentRequest(unpacker);
  				break;
  			case nhtAttachmentReqConfirm:
  				handleMessageAttachmentRequestConfirm(unpacker);
  				break;
  			case nhtAttachmentReqFail:
  				handleMessageAttachmentRequestFail(unpacker);
  				break;
  			case nhtIDUpdate:
  				handleMessageIDUpdate(unpacker);
  				break;
  			case nhtSendResult:
  				handleMessageSendResult(unpacker);
  				break;
  			case nhtCreateChat:
  				handleMessageCreateChat(unpacker);
  				break;
  			default:
  				//Message not consumed
  				return false;
  		}
  		
  		return true;
  	}
  	
  	private void handleMessageAuthentication(AirUnpacker unpacker) throws BufferUnderflowException, LargeAllocationException {
  		//Stopping the authentication timer
  		communicationsManager.stopTimeoutTimer();
  		
  		//Reading the result code
  		int resultCode = unpacker.unpackInt();
  		
  		//Checking if the result was successful
  		if(resultCode == nrcSharedOK) {
  			//Reading the server's information
  			String installationID = unpacker.unpackString();
  			String deviceName = unpacker.unpackString();
  			String systemVersion = unpacker.unpackString();
  			String softwareVersion = unpacker.unpackString();
  			
  			//Finishing the connection establishment
  			communicationsManager.getHandler().post(() -> communicationsManager.onHandshake(installationID, deviceName, systemVersion, softwareVersion, null, false));
  		} else {
  			//Otherwise terminating the connection
  			communicationsManager.getHandler().post(() -> communicationsManager.disconnect(mapNRCAuthenticationCode(resultCode)));
  		}
  	}
  	
  	private void handleMessageMessageUpdate(AirUnpacker unpacker) throws BufferUnderflowException, LargeAllocationException {
  		List<Blocks.ConversationItem> conversationItems = unpackConversationItems(unpacker);
  		communicationsManager.runListener(listener -> listener.onMessageUpdate(conversationItems));
  	}
  	
  	private void handleMessageMassRetrieval(AirUnpacker unpacker) {
  		//Reading the packet information
  		short requestID = unpacker.unpackShort();
  		int packetIndex = unpacker.unpackInt();
  		
  		//Checking if this is the first packet
  		if(packetIndex == 0) {
  			//Reading the conversation list and message count
  			List<Blocks.ConversationInfo> conversations = unpackConversations(unpacker);
  			int messageCount = unpacker.unpackInt();
  			
  			//Registering the mass retrieval manager
  			communicationsManager.runListener(listener -> listener.onMassRetrievalStart(requestID, conversations, messageCount));
  			
  			//Recording the request ID (to simulate a newer protocol version for future updates)
  			lastMassRetrievalRequestID = requestID;
  		} else {
  			//Reading the item list
  			List<Blocks.ConversationItem> conversationItems = unpackConversationItems(unpacker);
  			
  			//Processing the packet
  			communicationsManager.runListener(listener -> listener.onMassRetrievalUpdate(requestID, packetIndex, conversationItems));
  		}
  	}
  	
  	private void handleMessageMassRetrievalFinish(AirUnpacker unpacker) {
  		//Finishing the mass retrieval
  		communicationsManager.runListener(listener -> listener.onMassRetrievalComplete(lastMassRetrievalRequestID));
  	}
  	
  	private void handleMessageMassRetrievalFile(AirUnpacker unpacker) {
  		//Reading the data
  		short requestID = unpacker.unpackShort();
  		int requestIndex = unpacker.unpackInt();
  		String fileName, downloadFileName, downloadFileType;
  		if(requestIndex == 0) {
  			fileName = unpacker.unpackString();
  			downloadFileName = unpacker.unpackNullableString();
  			downloadFileType = unpacker.unpackNullableString();
  		} else {
  			fileName = downloadFileName = downloadFileType = null;
  		}
  		boolean isLast = unpacker.unpackBoolean();
  		
  		String fileGUID = unpacker.unpackString();
  		byte[] fileData = unpacker.unpackPayload();
  		
  		//Processing the data
  		communicationsManager.runListener(listener -> {
  			if(requestIndex == 0) listener.onMassRetrievalFileStart(requestID, fileGUID, fileName, downloadFileName, downloadFileType, InflaterOutputStream::new);
  			listener.onMassRetrievalFileProgress(requestID, requestIndex, fileGUID, fileData);
  			if(isLast) listener.onMassRetrievalFileComplete(requestID, fileGUID);
  		});
  	}
  	
  	private void handleMessageConversationUpdate(AirUnpacker unpacker) {
  		//Reading the data
  		List<Blocks.ConversationInfo> conversations = unpackConversations(unpacker);
  		
  		//Processing the conversations
  		communicationsManager.runListener(listener -> listener.onConversationUpdate(conversations));
  	}
  	
  	private void handleMessageModifierUpdate(AirUnpacker unpacker) {
  		//Reading the data
  		List<Blocks.ModifierInfo> modifiers = unpackModifiers(unpacker);
  		
  		//Processing the conversations
  		communicationsManager.runListener(listener -> listener.onModifierUpdate(modifiers));
  	}
  	
  	private void handleMessageAttachmentRequest(AirUnpacker unpacker) {
  		//Reading the data
  		short requestID = unpacker.unpackShort();
  		int requestIndex = unpacker.unpackInt();
  		
  		final String downloadFileName;
  		final String downloadFileType;
  		final long fileLength;
  		if(requestIndex == 0) {
  			downloadFileName = unpacker.unpackNullableString();
  			downloadFileType = unpacker.unpackNullableString();
  			fileLength = unpacker.unpackLong();
  		} else {
  			downloadFileName = null;
  			downloadFileType = null;
  			fileLength = -1;
  		}
  		
  		boolean isLast = unpacker.unpackBoolean();
  		
  		byte[] fileData = unpacker.unpackPayload();
  		
  		//Forwarding the data to the listeners
  		communicationsManager.runListener(listener -> {
  			if(requestIndex == 0) listener.onFileRequestStart(requestID, downloadFileName, downloadFileType, fileLength, InflaterOutputStream::new);
  			listener.onFileRequestData(requestID, requestIndex, fileData);
  			if(isLast) listener.onFileRequestComplete(requestID);
  		});
  	}
  	
  	private void handleMessageAttachmentRequestConfirm(AirUnpacker unpacker) {
  		//Reading the request ID
  		short requestID = unpacker.unpackShort();
  	}
  	
  	private void handleMessageAttachmentRequestFail(AirUnpacker unpacker) {
  		//Reading the data
  		short requestID = unpacker.unpackShort();
  		int errorCode = mapNRCAttachmentReqCode(unpacker.unpackInt());
  		
  		communicationsManager.runListener(listener -> listener.onFileRequestFail(requestID, errorCode));
  	}
  	
  	private void handleMessageIDUpdate(AirUnpacker unpacker) {
  		//Reading the data
  		long messageID = unpacker.unpackLong();
  		
  		communicationsManager.runListener(listener -> listener.onIDUpdate(messageID));
  	}
  	
  	private void handleMessageSendResult(AirUnpacker unpacker) {
  		//Reading the data
  		short requestID = unpacker.unpackShort();
  		int resultCode = unpacker.unpackInt();
  		String details = unpacker.unpackNullableString();
  		
  		if(resultCode == nrcSharedOK) {
  			communicationsManager.runListener(listener -> listener.onSendMessageSuccess(requestID));
  		} else {
  			communicationsManager.runListener(listener -> listener.onSendMessageFail(requestID, new CompoundErrorDetails.MessageSend(mapNRCSendCode(resultCode), details)));
  		}
  	}
  	
  	private void handleMessageCreateChat(AirUnpacker unpacker) {
  		//Reading the data
  		short requestID = unpacker.unpackShort();
  		int resultCode = unpacker.unpackInt();
  		String details = unpacker.unpackNullableString();
  		
  		communicationsManager.runListener(listener -> {
  			if(details == null) {
  				//This shouldn't happen
  				listener.onCreateChatError(requestID, new CompoundErrorDetails.ChatCreate(ChatCreateErrorCode.network, null));
  			} if(resultCode == nrcSharedOK) {
  				listener.onCreateChatSuccess(requestID, details);
  			} else {
  				listener.onCreateChatError(requestID, new CompoundErrorDetails.ChatCreate(mapNRCCreateChatCode(resultCode), details));
  			}
  		});
  	}
  	
  	@Override
  	boolean requestChatCreation(short requestID, String[] chatMembers, String service) {
  		//Returning false if there is no connection thread
  		if(!communicationsManager.isConnectionOpened()) return false;
  		
  		//Sending the message
  		try(AirPacker packer = AirPacker.get()) {
  			packer.packInt(nhtCreateChat);
  			
  			packer.packShort(requestID);
  			packer.packArrayHeader(chatMembers.length);
  			for(String item : chatMembers) packer.packString(item);
  			packer.packString(service);
  			
  			dataProxy.send(new EncryptedPacket(packer.toByteArray(), true));
  			return true;
  		} catch(BufferOverflowException exception) {
  			exception.printStackTrace();
  			CrashlyticsBridge.recordException(exception);
  			return false;
  		}
  	}
  	
  	@Override
  	boolean requestRetrievalAll(short requestID, MassRetrievalParams params) {
  		//Returning false if there is no connection thread
  		if(!communicationsManager.isConnectionOpened()) return false;
  		
  		//Sending the message
  		try(AirPacker packer = AirPacker.get()) {
  			packer.packInt(nhtMassRetrieval);
  			
  			packer.packShort(requestID); //Request ID
  			
  			packer.packBoolean(params.restrictMessages); //Whether or not to time-restrict messages
  			if(params.restrictMessages) packer.packLong(params.timeSinceMessages); //Messages time since
  			packer.packBoolean(params.downloadAttachments); //Whether or not to download attachments
  			if(params.downloadAttachments) {
  				packer.packBoolean(params.restrictAttachments); //Whether or not to time-restrict attachments
  				if(params.restrictAttachments) packer.packLong(params.timeSinceAttachments); //Attachments time since
  				packer.packBoolean(params.restrictAttachmentSizes); //Whether or not to size-restrict attachments
  				if(params.restrictAttachmentSizes) packer.packLong(params.attachmentSizeLimit); //Attachment size limit
  				
  				packer.packArrayHeader(params.attachmentFilterWhitelist.size()); //Attachment type whitelist
  				for(String filter : params.attachmentFilterWhitelist) packer.packString(filter);
  				packer.packArrayHeader(params.attachmentFilterBlacklist.size()); //Attachment type blacklist
  				for(String filter : params.attachmentFilterBlacklist) packer.packString(filter);
  				packer.packBoolean(params.attachmentFilterDLOutside); //Whether or not to download "other" items
  			}
  			
  			dataProxy.send(new EncryptedPacket(packer.toByteArray(), true));
  			return true;
  		} catch(BufferOverflowException exception) {
  			exception.printStackTrace();
  			CrashlyticsBridge.recordException(exception);
  			return false;
  		}
  	}
  	
  	@Override
  	boolean sendAuthenticationRequest(AirUnpacker unpacker) {
  		//Returning false if there is no connection thread
  		if(!communicationsManager.isConnectionOpened()) return false;
  		
  		//Assembling the device information
  		String installationID = SharedPreferencesManager.getInstallationID(MainApplication.getInstance());
  		String clientName = Build.MANUFACTURER + ' ' + Build.MODEL;
  		String platformID = ClientProtocol4.platformID;
  		
  		//Checking if the current protocol requires authentication
  		if(unpacker.unpackBoolean()) {
  			//Checking if we don't have a password to use
  			if(communicationsManager.getPassword() == null) {
  				//Failing the connection
  				communicationsManager.getHandler().post(() -> communicationsManager.disconnect(ConnectionErrorCode.unauthorized));
  				return true;
  			}
  			
  			//Telling the data proxy that encrypted messages should be used
  			dataProxy.setServerRequestsEncryption(true);
  			
  			//Reading the transmission check
  			byte[] transmissionCheck;
  			try {
  				transmissionCheck = unpacker.unpackPayload();
  			} catch(BufferUnderflowException | LargeAllocationException exception) {
  				exception.printStackTrace();
  				return false;
  			}
  			
  			//Writing back the transmission check and information about this device
  			try(AirPacker packer = AirPacker.get()) {
  				packer.packInt(nhtAuthentication);
  				
  				byte[] secureData;
  				{
  					AirPacker securePacker = new AirPacker(1024);
  					securePacker.packPayload(transmissionCheck); //Transmission check
  					securePacker.packString(installationID); //Installation ID
  					securePacker.packString(clientName); //Client name
  					securePacker.packString(platformID); //Platform ID
  					
  					//Encrypting the data
  					secureData = new EncryptionAES(communicationsManager.getPassword()).encrypt(securePacker.toByteArray());
  				}
  				
  				packer.packPayload(secureData);
  				
  				dataProxy.send(new EncryptedPacket(packer.toByteArray(), false));
  				return true;
  			} catch(BufferOverflowException | GeneralSecurityException exception) {
  				exception.printStackTrace();
  				CrashlyticsBridge.recordException(exception);
  				return false;
  			}
  		} else {
  			//Telling the data proxy that encrypted messages should not be used
  			dataProxy.setServerRequestsEncryption(false);
  			
  			//Writing back the device information
  			try(AirPacker packer = AirPacker.get()) {
  				packer.packInt(nhtAuthentication);
  				packer.packString(installationID); //Installation ID
  				packer.packString(clientName); //Client name
  				packer.packString(platformID); //Platform ID
  				
  				dataProxy.send(new EncryptedPacket(packer.toByteArray(), false));
  				return true;
  			} catch(BufferOverflowException exception) {
  				exception.printStackTrace();
  				CrashlyticsBridge.recordException(exception);
  				return false;
  			}
  		}
  	}
  	
  	@Override
  	boolean sendMessage(short requestID, ConversationTarget conversation, String message) {
  		//Returning false if there is no open connection
  		if(!communicationsManager.isConnectionOpened()) return false;
  		
  		try(AirPacker packer = AirPacker.get()) {
  			//Message type
  			if(conversation instanceof ConversationTarget.AppleLinked) {
  				packer.packInt(nhtSendTextExisting);
  			} else if(conversation instanceof ConversationTarget.AppleUnlinked) {
  				packer.packInt(nhtSendTextNew);
  			}
  			
  			packer.packShort(requestID); //Request ID
  			
  			if(conversation instanceof ConversationTarget.AppleLinked) {
  				packer.packString(((ConversationTarget.AppleLinked) conversation).getGuid()); //Chat GUID
  			} else if(conversation instanceof ConversationTarget.AppleUnlinked) {
  				ConversationTarget.AppleUnlinked unlinked = ((ConversationTarget.AppleUnlinked) conversation);
  				packer.packArrayHeader(unlinked.getMembers().size()); //Members
  				for(String item : unlinked.getMembers()) packer.packString(item);
  				packer.packString(unlinked.getService()); //Service
  			}
  			
  			packer.packString(message); //Message
  			
  			dataProxy.send(new EncryptedPacket(packer.toByteArray(), true));
  			return true;
  		} catch(BufferOverflowException exception) {
  			exception.printStackTrace();
  			CrashlyticsBridge.recordException(exception);
  			return false;
  		}
  	}
  	
  	@Override
  	public Observable<ReduxEventAttachmentUpload> sendFile(short requestID, ConversationTarget conversation, File file) {
  		return Observable.create((emitter) -> {
  			try {
  				MessageDigest messageDigest = MessageDigest.getInstance(hashAlgorithm);
  				try(InputStream inputStream = new DeflaterInputStream(new DigestInputStream(new BufferedInputStream(new FileInputStream(file)), messageDigest))) {
  					long totalLength = inputStream.available();
  					long totalBytesRead = 0;
  					int requestIndex = 0;
  					
  					for(LookAheadStreamIterator iterator = new LookAheadStreamIterator(attachmentChunkSize, inputStream); iterator.hasNext();) {
  						LookAheadStreamIterator.ForwardsStreamData data = iterator.next();
  						
  						//Adding to the total bytes read
  						totalBytesRead += data.getLength();
  						
  						//Uploading the file part
  						try(AirPacker packer = AirPacker.get()) {
  							//Message type
  							if(conversation instanceof ConversationTarget.AppleLinked) {
  								packer.packInt(nhtSendFileExisting);
  							} else if(conversation instanceof ConversationTarget.AppleUnlinked) {
  								packer.packInt(nhtSendFileNew);
  							}
  							
  							packer.packShort(requestID); //Request identifier
  							packer.packInt(requestIndex); //Request index
  							packer.packBoolean(data.isLast()); //Is last message
  							
  							if(conversation instanceof ConversationTarget.AppleLinked) {
  								packer.packString(((ConversationTarget.AppleLinked) conversation).getGuid()); //Chat GUID
  							} else if(conversation instanceof ConversationTarget.AppleUnlinked) {
  								ConversationTarget.AppleUnlinked unlinked = ((ConversationTarget.AppleUnlinked) conversation);
  								packer.packArrayHeader(unlinked.getMembers().size()); //Members
  								for(String item : unlinked.getMembers()) packer.packString(item);
  							}
  							
  							packer.packPayload(data.getData(), data.getLength()); //File bytes
  							if(requestIndex == 0) {
  								packer.packString(file.getName()); //File name
  								if(conversation instanceof ConversationTarget.AppleUnlinked) {
  									packer.packString(((ConversationTarget.AppleUnlinked) conversation).getService()); //Service
  								}
  							}
  							
  							dataProxy.send(new EncryptedPacket(packer.toByteArray(), true));
  						}
  						
  						//Incrementing the index
  						requestIndex++;
  						
  						//Updating the progress
  						emitter.onNext(new ReduxEventAttachmentUpload.Progress(totalBytesRead, totalLength));
  					}
  				}
  				
  				//Finishing
  				byte[] checksum = messageDigest.digest();
  				emitter.onNext(new ReduxEventAttachmentUpload.Complete(checksum));
  				emitter.onComplete();
  			} catch(IOException exception) {
  				exception.printStackTrace();
  				throw new AMRequestException(MessageSendErrorCode.localIO, exception);
  			} catch(NoSuchAlgorithmException | BufferOverflowException exception) {
  				exception.printStackTrace();
  				CrashlyticsBridge.recordException(exception);
  				throw new AMRequestException(MessageSendErrorCode.localInternal, exception);
  			}
  		});
  	}
  	
  	@Override
  	public boolean requestAttachmentDownload(short requestID, String attachmentGUID) {
  		//Returning false if there is no open connection
  		if(!communicationsManager.isConnectionOpened()) return false;
  		
  		try(AirPacker packer = AirPacker.get()) {
  			packer.packInt(nhtAttachmentReq);
  			
  			packer.packShort(requestID); //Request ID
  			packer.packInt(attachmentChunkSize); //Chunk size
  			packer.packString(attachmentGUID); //File GUID
  			
  			dataProxy.send(new EncryptedPacket(packer.toByteArray(), true));
  			return true;
  		} catch(BufferOverflowException exception) {
  			exception.printStackTrace();
  			CrashlyticsBridge.recordException(exception);
  			return false;
  		}
  	}
  	
  	@Override
  	public boolean requestConversationInfo(Collection<String> conversations) {
  		//Returning false if there is no connection thread
  		if(!communicationsManager.isConnectionOpened()) return false;
  		
  		try(AirPacker packer = AirPacker.get()) {
  			packer.packInt(nhtConversationUpdate);
  			
  			packer.packArrayHeader(conversations.size());
  			for(String item : conversations) packer.packString(item);
  			
  			dataProxy.send(new EncryptedPacket(packer.toByteArray(), true));
  			return true;
  		} catch(BufferOverflowException exception) {
  			exception.printStackTrace();
  			CrashlyticsBridge.recordException(exception);
  			return false;
  		}
  	}
  	
  	@Override
  	boolean requestRetrievalTime(long timeLower, long timeUpper) {
  		//Returning false if there is no connection thread
  		if(!communicationsManager.isConnectionOpened()) return false;
  		
  		try(AirPacker packer = AirPacker.get()) {
  			packer.packInt(nhtTimeRetrieval);
  			
  			packer.packLong(timeLower);
  			packer.packLong(timeUpper);
  			
  			dataProxy.send(new EncryptedPacket(packer.toByteArray(), true));
  			return true;
  		} catch(BufferOverflowException exception) {
  			exception.printStackTrace();
  			CrashlyticsBridge.recordException(exception);
  			return false;
  		}
  	}
  	
  	@Override
  	boolean requestRetrievalID(long idSince, long timeLower, long timeUpper) {
  		//Returning false if there is no connection thread
  		if(!communicationsManager.isConnectionOpened()) return false;
  		
  		try(AirPacker packer = AirPacker.get()) {
  			packer.packInt(nhtIDRetrieval);
  			
  			packer.packLong(idSince);
  			packer.packLong(timeLower);
  			packer.packLong(timeUpper);
  			
  			dataProxy.send(new EncryptedPacket(packer.toByteArray(), true));
  			return true;
  		} catch(BufferOverflowException exception) {
  			exception.printStackTrace();
  			CrashlyticsBridge.recordException(exception);
  			return false;
  		}
  	}
  
  	@Override
  	boolean installSoftwareUpdate(int updateID) {
  		throw new UnsupportedOperationException("Not supported");
  	}
  
  	@Override
  	boolean requestFaceTimeLink() {
  		throw new UnsupportedOperationException("Not supported");
  	}
  	
  	@Override
  	boolean initiateFaceTimeCall(List<String> addresses) {
  		throw new UnsupportedOperationException("Not supported");
  	}
  	
  	@Override
  	boolean handleIncomingFaceTimeCall(@NonNull String caller, boolean accept) {
  		throw new UnsupportedOperationException("Not supported");
  	}
  	
  	@Override
  	boolean dropFaceTimeCallServer() {
  		throw new UnsupportedOperationException("Not supported");
  	}
  
  	@Override
  	boolean isFeatureSupported(@ConnectionFeature int featureID) {
  		return featureID == ConnectionFeature.idBasedRetrieval || featureID == ConnectionFeature.payloadPushNotifications;
  	}
  	
  	/**
  	 * Maps an NRC authentication code to a local result code
  	 */
  	@ConnectionErrorCode
  	private static int mapNRCAuthenticationCode(int code) {
  		switch(code) {
  			case 1:
  				return ConnectionErrorCode.unauthorized;
  			case 2:
  				return ConnectionErrorCode.badRequest;
  			default:
  				return ConnectionErrorCode.connection;
  		}
  	}
  	
  	/**
  	 * Maps an NRC send code to a local error code
  	 */
  	@MessageSendErrorCode
  	private static int mapNRCSendCode(int code) {
  		switch(code) {
  			case 1: //Some unknown AppleScript error
  				return MessageSendErrorCode.serverExternal;
  			case 2: //Invalid data received
  				return MessageSendErrorCode.serverBadRequest;
  			case 3: //System rejected request to send message
  				return MessageSendErrorCode.serverUnauthorized;
  			case 4: //A valid conversation wasn't found
  				return MessageSendErrorCode.serverNoConversation;
  			case 5: //Request timed out
  				return MessageSendErrorCode.serverRequestTimeout;
  			default:
  				return MessageSendErrorCode.serverUnknown;
  		}
  	}
  	
  	/**
  	 * Maps an NRC attachment request code to a local error code
  	 */
  	@AttachmentReqErrorCode
  	private static int mapNRCAttachmentReqCode(int code) {
  		switch(code) {
  			case 1: //File GUID not found
  				return AttachmentReqErrorCode.serverNotFound;
  			case 2: //File (on disk) not found
  				return AttachmentReqErrorCode.serverNotSaved;
  			case 3: //No access to file
  				return AttachmentReqErrorCode.serverUnreadable;
  			case 4: //I/O error
  				return AttachmentReqErrorCode.serverIO;
  			default:
  				return AttachmentReqErrorCode.unknown;
  		}
  	}
  	
  	/**
  	 * Maps an NRC chat creation code to a local error code
  	 */
  	@ChatCreateErrorCode
  	private static int mapNRCCreateChatCode(int code) {
  		switch(code) {
  			case 1: //Some unknown AppleScript error
  				return ChatCreateErrorCode.scriptError;
  			case 2: //Invalid data received
  				return ChatCreateErrorCode.badRequest;
  			case 3: //System rejected request
  				return ChatCreateErrorCode.unauthorized;
  			default:
  				return ChatCreateErrorCode.unknown;
  		}
  	}
  	
  	/**
  	 * Maps an NRC Apple message error code to a local error code
  	 */
  	@MessageSendErrorCode
  	private static int mapNRCAppleErrorCode(int code) {
  		switch(code) {
  			case 0:
  				return MessageSendErrorCode.none;
  			case 1: //Unknown error code
  			default:
  				return MessageSendErrorCode.appleUnknown;
  			case 2: //Network error
  				return MessageSendErrorCode.appleNetwork;
  			case 3: //Not registered with iMessage
  				return MessageSendErrorCode.appleUnregistered;
  		}
  	}
  	
  	/**
  	 * Maps an NST message state code to a local code
  	 */
  	@MessageState
  	private static int mapNSTMessageState(int code) {
  		switch(code) {
  			default:
  			case nstMessageStateIdle:
  				return MessageState.idle;
  			case nstMessageStateSent:
  				return MessageState.sent;
  			case nstMessageStateDelivered:
  				return MessageState.delivered;
  			case nstMessageStateRead:
  				return MessageState.read;
  		}
  	}
  	
  	/**
  	 * Maps an NST group action code to a local code
  	 */
  	@GroupAction
  	private static int mapNSTGroupAction(int code) {
  		switch(code) {
  			case nstGroupActionUnknown:
  			default:
  				return GroupAction.unknown;
  			case nstGroupActionJoin:
  				return GroupAction.join;
  			case nstGroupActionLeave:
  				return GroupAction.leave;
  		}
  	}
  	
  	/**
  	 * Maps an Apple tapback code to a local code
  	 */
  	@TapbackType
  	private static int mapTapbackType(int appleCode) {
  		//Returning the associated version
  		switch(appleCode) {
  			case 0:
  				return TapbackType.heart;
  			case 1:
  				return TapbackType.like;
  			case 2:
  				return TapbackType.dislike;
  			case 3:
  				return TapbackType.laugh;
  			case 4:
  				return TapbackType.exclamation;
  			case 5:
  				return TapbackType.question;
  			default:
  				return TapbackType.unknown;
  		}
  	}
  	
  	/**
  	 * Unpacks a list of conversations
  	 */
  	public static List<Blocks.ConversationInfo> unpackConversations(AirUnpacker unpacker) {
  		//Reading the count
  		int count = unpacker.unpackArrayHeader();
  		if(count > 8192) throw new LargeAllocationException(count, 8192);
  		
  		//Creating the list
  		List<Blocks.ConversationInfo> list = new ArrayList<>(count);
  		
  		//Iterating over the items
  		for(int i = 0; i < count; i++) {
  			String guid = unpacker.unpackString();
  			boolean available = unpacker.unpackBoolean();
  			if(available) {
  				String service = unpacker.unpackString();
  				String name = unpacker.unpackNullableString();
  				int memberCount = unpacker.unpackArrayHeader();
  				String[] members = new String[memberCount];
  				if(memberCount > 8192) throw new LargeAllocationException(memberCount, 8192);
  				for(int m = 0; m < members.length; m++) members[m] = unpacker.unpackString();
  				list.add(new Blocks.ConversationInfo(guid, service, name, members));
  			} else {
  				list.add(new Blocks.ConversationInfo(guid));
  			}
  		}
  		
  		//Returning the list
  		return list;
  	}
  	
  	/**
  	 * Unpacks a list of conversation items
  	 */
  	public static List<Blocks.ConversationItem> unpackConversationItems(AirUnpacker unpacker) {
  		//Reading the count
  		int count = unpacker.unpackArrayHeader();
  		if(count > 8192) throw new LargeAllocationException(count, 8192);
  		
  		//Creating the list
  		List<Blocks.ConversationItem> list = new ArrayList<>(count);
  		
  		//Iterating over the items
  		for(int i = 0; i < count; i++) {
  			int type = unpacker.unpackInt();
  			
  			long serverID = unpacker.unpackLong();
  			String guid = unpacker.unpackString();
  			String chatGuid = unpacker.unpackString();
  			long date = unpacker.unpackLong();
  			
  			switch(type) {
  				default:
  					throw new IllegalArgumentException("Invalid conversation item type: " + type);
  				case nstItemMessage: {
  					String text = StringHelper.nullifyEmptyString(unpacker.unpackNullableString());
  					String subject = StringHelper.nullifyEmptyString(unpacker.unpackNullableString());
  					String sender = StringHelper.nullifyEmptyString(unpacker.unpackNullableString());
  					List<Blocks.AttachmentInfo> attachments = unpackAttachments(unpacker);
  					List<Blocks.StickerModifierInfo> stickers = (List<Blocks.StickerModifierInfo>) (List<?>) unpackModifiers(unpacker);
  					List<Blocks.TapbackModifierInfo> tapbacks = (List<Blocks.TapbackModifierInfo>) (List<?>) unpackModifiers(unpacker);
  					String sendEffect = StringHelper.nullifyEmptyString(unpacker.unpackNullableString());
  					@MessageState int stateCode = mapNSTMessageState(unpacker.unpackInt());
  					@MessageSendErrorCode int errorCode = mapNRCAppleErrorCode(unpacker.unpackInt());
  					long dateRead = unpacker.unpackLong();
  					
  					list.add(new Blocks.MessageInfo(serverID, guid, chatGuid, date, text, subject, sender, attachments, stickers, tapbacks, sendEffect, stateCode, errorCode, dateRead));
  					break;
  				}
  				case nstItemGroupAction: {
  					String agent = unpacker.unpackNullableString();
  					String other = unpacker.unpackNullableString();
  					int groupActionType = mapNSTGroupAction(unpacker.unpackInt());
  					
  					list.add(new Blocks.GroupActionInfo(serverID, guid, chatGuid, date, agent, other, groupActionType));
  					break;
  				}
  				case nstItemChatRename: {
  					String agent = unpacker.unpackNullableString();
  					String newChatName = StringHelper.nullifyEmptyString(unpacker.unpackNullableString());
  					
  					list.add(new Blocks.ChatRenameActionInfo(serverID, guid, chatGuid, date, agent, newChatName));
  					break;
  				}
  			}
  		}
  		
  		//Returning the list
  		return list;
  	}
  	
  	/**
  	 * Unpacks a list of attachments
  	 */
  	public static List<Blocks.AttachmentInfo> unpackAttachments(AirUnpacker unpacker) {
  		//Reading the count
  		int count = unpacker.unpackArrayHeader();
  		if(count > 8192) throw new LargeAllocationException(count, 8192);
  		
  		//Creating the list
  		List<Blocks.AttachmentInfo> list = new ArrayList<>(count);
  		
  		//Iterating over the items
  		for(int i = 0; i < count; i++) {
  			String guid = unpacker.unpackString();
  			String name = unpacker.unpackString();
  			String type = StringHelper.defaultEmptyString(unpacker.unpackNullableString(), MIMEConstants.defaultMIMEType);
  			long size = unpacker.unpackLong();
  			byte[] checksum = unpacker.unpackNullablePayload();
  			long sort = unpacker.unpackLong();
  			
  			list.add(new Blocks.AttachmentInfo(guid, name, type, size, checksum, sort));
  		}
  		
  		//Returning the list
  		return list;
  	}
  	
  	/**
  	 * Unpacks a list of modifiers
  	 */
  	public static List<Blocks.ModifierInfo> unpackModifiers(AirUnpacker unpacker) {
  		//Reading the count
  		int count = unpacker.unpackArrayHeader();
  		if(count > 8192) throw new LargeAllocationException(count, 8192);
  		
  		//Creating the list
  		List<Blocks.ModifierInfo> list = new ArrayList<>(count);
  		
  		//Iterating over the items
  		for(int i = 0; i < count; i++) {
  			int type = unpacker.unpackInt();
  			
  			String message = unpacker.unpackString();
  			
  			switch(type) {
  				default:
  					throw new IllegalArgumentException("Invalid modifier type: " + type);
  				case nstModifierActivity: {
  					@MessageState int state = mapNSTMessageState(unpacker.unpackInt());
  					long dateRead = unpacker.unpackLong();
  					
  					list.add(new Blocks.ActivityStatusModifierInfo(message, state, dateRead));
  					break;
  				}
  				case nstModifierSticker: {
  					int messageIndex = unpacker.unpackInt();
  					String fileGuid = unpacker.unpackString();
  					String sender = unpacker.unpackNullableString();
  					long date = unpacker.unpackLong();
  					byte[] data = unpacker.unpackPayload();
  					String fileType = unpacker.unpackString();
  					
  					byte[] decompressedData;
  					try {
  						decompressedData = StandardCompressionHelper.decompressDeflate(data);
  					} catch(IOException exception) {
  						exception.printStackTrace();
  						
  						continue;
  					}
  					
  					list.add(new Blocks.StickerModifierInfo(message, messageIndex, fileGuid, sender, date, decompressedData, fileType));
  					break;
  				}
  				case nstModifierTapback: {
  					int messageIndex = unpacker.unpackInt();
  					String sender = unpacker.unpackNullableString();
  					boolean isAddition = unpacker.unpackBoolean();
  					int tapbackType = unpacker.unpackInt();
  					
  					list.add(new Blocks.TapbackModifierInfo(message, messageIndex, sender, isAddition, mapTapbackType(tapbackType)));
  					break;
  				}
  			}
  		}
  		
  		//Returning the list
  		return list;
  	}
  }