bluetoothappprofiles/avrcp/remconbeareravrcp/src/avrcpoutgoingcommandhandler.cpp
changeset 0 f63038272f30
child 20 2f88a7d66f50
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bluetoothappprofiles/avrcp/remconbeareravrcp/src/avrcpoutgoingcommandhandler.cpp	Mon Jan 18 20:28:57 2010 +0200
@@ -0,0 +1,768 @@
+// Copyright (c) 2004-2009 Nokia Corporation and/or its subsidiary(-ies).
+// All rights reserved.
+// This component and the accompanying materials are made available
+// under the terms of "Eclipse Public License v1.0"
+// which accompanies this distribution, and is available
+// at the URL "http://www.eclipse.org/legal/epl-v10.html".
+//
+// Initial Contributors:
+// Nokia Corporation - initial contribution.
+//
+// Contributors:
+//
+// Description:
+//
+
+
+
+/**
+ @file
+ @internalComponent
+ @released
+*/
+
+#include <remconcoreapi.h>
+
+#include "avcpanel.h"
+#include "controlcommand.h"
+#include "avrcpoutgoingcommandhandler.h"
+#include "avrcplog.h"
+#include "avrcprouter.h"
+#include "avrcptimer.h"
+#include "avrcputils.h"
+#include "controlbearer.h"
+
+//---------------------------------------------------------------------
+// Construction/Destruction
+//---------------------------------------------------------------------
+
+/** Factory function.
+
+@param aBearer	The CRemConBearerAvrcp this is to handle commands for.
+@param aObserver The observer of the bearer. Used to aquire converters.
+@param aRouter	A CRcpRouter to use for communication with remote devices.
+@param aTimer	CDeltaTimer to use for queuing timed events.
+@return A fully constructed CRcpOutgoingCommandHandler.
+@leave System wide error codes.
+*/
+CRcpOutgoingCommandHandler* CRcpOutgoingCommandHandler::NewL(CRemConBearerAvrcp& aBearer, 
+	MRemConBearerObserver& aObserver,
+	CRcpRouter& aRouter,
+	CDeltaTimer& aTimer)
+	{
+	LOG_STATIC_FUNC
+	CRcpOutgoingCommandHandler* handler = new(ELeave)CRcpOutgoingCommandHandler(aBearer, aObserver, aRouter, aTimer);
+	return handler;
+	}
+
+/** Constructor.
+
+@param aBearer	The CRemConBearerAvrcp this is to handle commands for.
+@param aObserver The observer of the bearer. Used to aquire converters.
+@param aRouter	A CRcpRouter to use for communication with remote devices.
+@param aTimer	CDeltaTimer to use for queuing timed events.
+@return A partially constructed CRcpIncomingCommandHandler.
+@leave System wide error codes.
+*/		
+CRcpOutgoingCommandHandler::CRcpOutgoingCommandHandler(CRemConBearerAvrcp& aBearer, 
+	MRemConBearerObserver& aObserver,
+	CRcpRouter& aRouter,
+	CDeltaTimer& aTimer) : iCommandQueue(_FOFF(CControlCommand, iHandlingLink)),
+	iNotifyCommandQueue(_FOFF(CControlCommand, iHandlingLink)),
+	iBearer(aBearer), iObserver(aObserver), iRouter(aRouter), iTimer(aTimer)
+	{
+	LOG_FUNC
+	}
+
+/** Destructor.
+*/
+CRcpOutgoingCommandHandler::~CRcpOutgoingCommandHandler()
+	{
+	LOG_FUNC
+
+	ClearQueue(iCommandQueue);
+	ClearQueue(iNotifyCommandQueue);
+	}	
+
+void CRcpOutgoingCommandHandler::ClearQueue(TDblQue<CControlCommand>& aQue)
+	{
+	while(!aQue.IsEmpty())
+		{
+		CControlCommand* command = aQue.First();
+		command->CancelTimer(iTimer);
+		command->iHandlingLink.Deque();
+		command->DecrementUsers();
+		}
+	}
+//---------------------------------------------------------------------
+// Called from the bearer
+//---------------------------------------------------------------------
+
+/** Tell the handler to gracefully shutdown.
+
+@param aClearQueue Whether to clear the queue without handling the things 
+					on it.  If this is true the commands will be deleted.
+					If this is false then pending commands will have responses
+					generated to RemCon.
+*/
+void CRcpOutgoingCommandHandler::Disconnect(TBool aClearQueue)
+	{
+	LOG_FUNC	
+	ProcessDisconnect(iCommandQueue, aClearQueue);
+	ProcessDisconnect(iNotifyCommandQueue, aClearQueue);
+	}
+
+void CRcpOutgoingCommandHandler::ProcessDisconnect(TDblQue<CControlCommand>& aQue, TBool aClearQueue)
+	{
+	while(!aQue.IsEmpty())
+		{
+		CControlCommand* command = aQue.First();
+		iRouter.RemoveFromSendQueue(*command);
+		command->CancelTimer(iTimer);
+		
+		if(aClearQueue)
+			{
+			GenerateFailureResult(*command, KErrDisconnected);
+			}
+
+		command->iHandlingLink.Deque();
+		command->DecrementUsers();
+		}
+	}
+/** Sends a new command.
+
+@param aInterfaceUid	The RemCon client interface this command is from.
+@param aCommand			The operation id within aInterfaceUid.
+@param aId				A unique identifier provided by RemCon.
+@param aCommandData		Data associated with this command.
+@param aAddr			Bluetooth address of device to send this command to.
+@leave KErrNoMemory or system wide error code.
+@leave Command parsing error.
+*/
+void CRcpOutgoingCommandHandler::SendCommandL(TUid aInterfaceUid, 
+		TUint aCommand, 
+		TUint aId,  
+		RBuf8& aCommandData, 
+		const TBTDevAddr& aAddr)
+	{
+	LOG_FUNC
+	
+	if(aInterfaceUid.iUid == KRemConCoreApiUid)
+		{
+		// Passthrough commands are stateful, so we need to examine the
+		// history - we can't just blindly wham it on the queue.
+		HandleCoreApiCommandL(aCommand, aId, aCommandData, aAddr);
+		}
+	else
+		{
+		SendCommandL(aInterfaceUid, aCommand, aId, aCommandData, EFalse, aAddr, ETrue, EFalse);
+		}
+	}
+
+/** Sends a new notify command.
+
+@param aInterfaceUid	The RemCon client interface this command is from.
+@param aCommand			The operation id within aInterfaceUid.
+@param aId				A unique identifier provided by RemCon, the transaction ID.
+@param aCommandData		Data associated with this command.
+@param aAddr			Bluetooth address of device to send this command to.
+@leave KErrNoMemory or system wide error code.
+@leave Command parsing error.
+*/
+void CRcpOutgoingCommandHandler::SendNotifyCommandL(TUid aInterfaceUid, 
+		TUint aCommand, 
+		TUint aId,  
+		RBuf8& aCommandData, 
+		const TBTDevAddr& aAddr)
+	{
+	LOG_FUNC
+	SendCommandL(aInterfaceUid, aCommand, aId, aCommandData, EFalse, aAddr, ETrue, ETrue);
+	}
+	
+//---------------------------------------------------------------------
+// Data notifications from the router
+//---------------------------------------------------------------------
+
+/** Called by the router to provide a new response.
+
+@param aFrame The AV/C frame for this response. Ownership is taken.
+@param aTransLabel The AVCTP transaction id of this response. This is used
+				to match it with its command.
+@param aAddr The remote from which this response originated
+*/
+void CRcpOutgoingCommandHandler::ReceiveResponse(const TDesC8& aMessageInformation, 
+	SymbianAvctp::TTransactionLabel aTransLabel, 
+	TBool aIpidBitSet)
+	{
+	LOG_FUNC
+	
+	CAVCFrame* frame = NULL;
+	TInt err = KErrNone;
+	if(!aIpidBitSet)
+		{
+		TRAP(err, frame = CAVCFrame::NewL(aMessageInformation, AVC::EResponse));
+		}
+	
+	if(!err)
+		{
+		CControlCommand* command = NULL;
+		command = FindInQueue(iCommandQueue, aTransLabel);
+		if ( command != NULL )
+			{
+			//Found, so it is a normal command response.
+			ProcessReceiveResponse(frame, aIpidBitSet, command, EFalse);
+			}
+		else
+			{
+			//Try to find in the notify command queue.
+			command = FindInQueue(iNotifyCommandQueue, aTransLabel);
+			if( command != NULL )
+			    {
+			    //Found, so it is a notify command response.
+			    ProcessReceiveResponse(frame, aIpidBitSet, command, ETrue);
+			    }
+			}
+		
+		delete frame;
+		}
+	}
+
+CControlCommand* CRcpOutgoingCommandHandler::FindInQueue(TDblQue<CControlCommand>& aQue, 
+		SymbianAvctp::TTransactionLabel aTransLabel)
+	{	
+	CControlCommand* command = NULL;
+	TDblQueIter<CControlCommand> iter(aQue);
+	while (iter)
+		{
+		command = iter++;
+		if(command->TransactionLabel() == aTransLabel)
+			{
+			return command;
+			}
+		}
+	
+	return NULL;
+	}
+
+void CRcpOutgoingCommandHandler::ProcessReceiveResponse(CAVCFrame* aFrame, 
+		TBool aIpidBitSet,
+		CControlCommand* aCommand, 
+		TBool aNotify)
+	{
+	aCommand->CancelTimer(iTimer);
+	
+	TInt err = KErrNone;
+	// Inform the bearer if this is something it knows about
+	// ie not a click release
+	if(aCommand->KnownToBearer())
+		{
+		if(!aIpidBitSet)
+			{
+			if(aFrame->Data().Length() < KAVCFrameHeaderLength)
+				{
+				// Drop corrupt frames
+				return;
+				}
+
+			err = aCommand->ParseIncomingResponse(iObserver, *aFrame);
+			}
+		else
+			{
+			// If aIpidBitSet is true that means AVRCP is not supported
+			// by the remote end.  We handle this in the same way as not
+			// supported commands, passing them up to RemCon as not 
+			// supported, so just map the ctype here, rather than setting
+			// up another path for ipid handling, but we need pass as the
+			// frame the original because we don't get one from AVCTP if
+			// ipid is set.
+			aCommand->SetResponseType(KErrNotSupported);
+			err = aCommand->ParseIncomingResponse(iObserver, aCommand->Frame());
+			}
+		
+		if ( aNotify )
+		    {//This is a notify command
+		    iBearer.MrccciNewNotifyResponse(*aCommand);
+		    }
+		else
+			{
+			iBearer.MrccciNewResponse(*aCommand);
+			}
+		}
+
+	TBool doDeque = ETrue;
+	if ( (!aIpidBitSet) && (err == KErrNone) && (aNotify) && (aFrame->Type() == AVC::EInterim))
+		{
+		doDeque = EFalse;
+		}
+	
+	// If this a passthrough press that hasn't yet been released, we need 
+	// to wait for a release before getting rid of this, otherwise we're done.
+	if(aCommand == iUnreleasedCommand)
+		{
+		iUnreleasedHasResponse = ETrue;
+		StartReleaseTimer(*iUnreleasedCommand);
+		doDeque = EFalse;
+		}
+	
+	if ( doDeque )
+		{
+		aCommand->iHandlingLink.Deque();
+		aCommand->DecrementUsers();
+		}
+	}
+/** Called by the router to complete a send.
+
+@param aCommand The command which has been sent.
+@param aSendResult The result of the send. KErrNone if successful.
+*/	
+void CRcpOutgoingCommandHandler::MessageSent(CAvrcpCommand& aCommand, TInt aSendResult)
+	{
+	LOG_FUNC
+	
+	if(aSendResult == KErrNone)
+		{
+		// Set off response timer
+		StartResponseTimer(static_cast<CControlCommand&>(aCommand));
+		}
+	else
+		{
+		CControlCommand* command = FindInQueue(iNotifyCommandQueue, aCommand.TransactionLabel());
+		
+		if(command)
+			{
+			command->SetNotifyVolumeChangeResult(command->Frame());
+			iBearer.MrccciNewNotifyResponse(*command);
+			}
+		else
+			{
+			command = FindInQueue(iCommandQueue, aCommand.TransactionLabel());
+			
+			// Generate error response up to RemCon
+			// if this is a core command we can set the result, 
+			// otherwise we just return it as we got it.
+			if(command->Frame().Opcode() == AVC::EPassThrough)
+				{
+				// Need to insert before setting the button action so we have
+				// long enough data
+				if (!command->InsertCoreResult(aSendResult))
+					{
+					if(command->Click())
+						{
+						command->SetCoreButtonAction(ERemConCoreApiButtonClick, ETrue);
+						}
+					}
+				}
+			
+			iBearer.MrccciNewResponse(*command);
+			}
+		
+		command->iHandlingLink.Deque();
+		command->DecrementUsers();
+		}
+	}
+
+//---------------------------------------------------------------------
+// Internal Utility functions
+//---------------------------------------------------------------------
+
+void CRcpOutgoingCommandHandler::CleanupUnreleased()
+	{
+	iUnreleasedCommand->CancelTimer(iTimer);
+	iUnreleasedCommand->iHandlingLink.Deque();
+	iUnreleasedCommand->DecrementUsers();
+	iUnreleasedHasResponse = EFalse;
+	}
+	
+/** Handle a command that is part of the Core API.
+
+@param aCommand			The operation id within aInterfaceUid.
+@param aId				A unique identifier provided by RemCon.
+@param aCommandData		Data associated with this command.
+@param aAddr			Bluetooth address of device to send this command to.
+@leave KErrNoMemory or system wide error code.
+@leave Command parsing error.
+*/
+void CRcpOutgoingCommandHandler::HandleCoreApiCommandL(TUint aCommand, 
+		TUint aId,  
+		RBuf8& aCommandData, 
+		const TBTDevAddr& aAddr)
+	{
+	if(aCommandData.Length() < KRemConCoreApiButtonDataLength)
+   		{
+   		User::Leave(KErrCorrupt);
+   		}
+
+	TInt buttonData;
+	AvrcpUtils::ReadCommandDataToInt(aCommandData, 
+   		KRemConCoreApiButtonDataOffset, KRemConCoreApiButtonDataLength, buttonData);
+ 
+ 	// First check if there's anything we need to do before sending this command,
+ 	// mainly releasing a previous press.  	
+	if(iUnreleasedCommand)
+		{
+		TUint prevOpId = iUnreleasedCommand->RemConOperationId();
+		
+		if(aCommand == prevOpId)
+			{
+			// Either we've received a release, or we've refreshed the press.
+			// If the unreleased press has already been responded too we can
+			// dispose of it now.  
+			// If the unreleased press has not been responded too we can 
+			// treat it like a normal command on reception of response, so just
+			// set iUnreleased to NULL.
+			if(iUnreleasedHasResponse)
+				{
+				CleanupUnreleased();
+				}
+
+			iUnreleasedCommand = NULL;
+			}
+		else
+			{
+			// A new operation!
+			if(buttonData != ERemConCoreApiButtonRelease)
+				{
+				// Try and generate the release for the previous command, if
+				// if fails then the remote will just have to assume it.
+				// There's no point leaving this to the release timer, because
+				// we want to send another command now, so even if we send the
+				// release later the remote should ignore it.
+				TRAP_IGNORE(GenerateCommandL(*iUnreleasedCommand, ERemConCoreApiButtonRelease));
+				
+				if(iUnreleasedHasResponse)
+					{
+					CleanupUnreleased();
+					}
+
+				iUnreleasedCommand = NULL;
+				}
+			else
+				{
+				// A release for a command other than iUnreleased.  We can't 
+				// send this now.
+				User::Leave(KErrNotReady);
+				}
+			}
+		}
+	else if(buttonData == ERemConCoreApiButtonRelease)
+		{
+		// We don't have an unreleased command.  We must have already 
+		// released this, either via the timer, or because we've sent
+		// another command in the meantime.  We can't send this now.
+		// Leaving synchronously means we don't need to worry about generating
+		// a fake response, which may mislead the application.
+		User::Leave(KErrNotReady);
+		}
+   	
+   	if(buttonData == ERemConCoreApiButtonClick)
+   		{	
+		// aCommandData is still owned by RemCon until we return successfully.
+		// If we try the operations with the new data first we won't end up 
+		// in a situation where the new CControlCommand thinks that it owns 
+		// aCommandData, then the release operation leaves, so RemCon also
+		// thinks it owns aCommandData.
+		
+		RBuf8 pressBuf;
+		pressBuf.CreateL(aCommandData);
+		CleanupClosePushL(pressBuf);
+		
+		AvrcpUtils::SetCommandDataFromInt(pressBuf, 
+			KRemConCoreApiButtonDataOffset, KRemConCoreApiButtonDataLength, ERemConCoreApiButtonPress);
+		SendCommandL(TUid::Uid(KRemConCoreApiUid), aCommand, aId, pressBuf, ETrue, aAddr, ETrue, EFalse);
+		
+		// Data has been taken ownership of by SendCommandL, so can just let 
+		// pressbuf go out of scope.
+		CleanupStack::Pop(&pressBuf);
+		
+		AvrcpUtils::SetCommandDataFromInt(aCommandData, 
+			KRemConCoreApiButtonDataOffset, KRemConCoreApiButtonDataLength, ERemConCoreApiButtonRelease);
+		SendCommandL(TUid::Uid(KRemConCoreApiUid), aCommand, aId, aCommandData, ETrue, aAddr, EFalse, EFalse);
+		}
+	else if(buttonData == ERemConCoreApiButtonPress)
+		{
+		iUnreleasedCommand = &SendCommandL(TUid::Uid(KRemConCoreApiUid), aCommand, aId, aCommandData, EFalse, aAddr, ETrue, EFalse);
+		iReleaseTimerExpiryCount = 0;
+		}
+	else
+		{
+		// Must be release
+		__ASSERT_DEBUG(buttonData == ERemConCoreApiButtonRelease, AvrcpUtils::Panic(EAvrcpUnknownButtonAction));
+		SendCommandL(TUid::Uid(KRemConCoreApiUid), aCommand, aId, aCommandData, EFalse, aAddr, ETrue, EFalse);
+		}
+	}
+
+/** Creates a command from the RemCon data.
+
+This is an internal utility function.
+
+A CControlCommand will be created.  Calling ProcessOutgoingCommandL on
+this creates a CAVCFrame from the provided data.  If an AV/C frame
+can be created the command will be added to the outgoing queue to
+wait a response from the remote.  Otherwise this function will
+leave.
+
+@param aInterfaceUid	The RemCon client interface this command is from.
+@param aCommand			The operation id within aInterfaceUid.
+@param aId				A unique identifier provided by RemCon.
+@param aCommandData		Data associated with this command.
+@param aIsClick			Whether this is a button click.
+@param aAddr			Bluetooth address of device to send this command to.
+@return The generated command.
+@leave KErrNoMemory or system wide error code.
+@leave Command parsing error.
+*/
+CControlCommand& CRcpOutgoingCommandHandler::SendCommandL(TUid aInterfaceUid,
+	TUint aCommand, 
+	TUint aId, 
+	RBuf8& aCommandData, 
+	TBool aIsClick, 
+	const TBTDevAddr& aAddr,
+	TBool aKnownToBearer,
+	TBool aNotify)
+	{
+	LOG_FUNC
+	// Create a command and wham it on our queue, so we can match it up with its response
+	// CControlCommand::NewL takes ownership of the data in aCommandData then NULLs aCommandData
+	// so a leave later in the function won't cause double deletion.
+	CControlCommand* command = CControlCommand::NewL(aInterfaceUid, aCommand, aId, iCurrentTrans, 
+			aCommandData, aIsClick, aAddr, aKnownToBearer);
+	CleanupStack::PushL(command);	
+	
+	command->ProcessOutgoingCommandL(iObserver);
+	CleanupStack::Pop(command);
+	command->IncrementUsers();
+	
+	if ( aNotify )
+		{
+		iNotifyCommandQueue.AddLast(*command);
+		}
+	else
+		{
+		iCommandQueue.AddLast(*command);
+		}
+	
+	// Increment our transaction id
+	iCurrentTrans = (iCurrentTrans + 1) % SymbianAvctp::KMaxTransactionLabel;
+	
+	// Command stays on the queue till we've got the response
+	iRouter.AddToSendQueue(*command);
+	
+	return *command;
+	}
+
+/** Generate a failure response to RemCon.
+
+This sets the result for a passthrough command.
+It informs the bearer of the new response.
+
+@param aCommand The command to finish off.
+@param aResult The result (only valid for passthrough)
+*/
+void CRcpOutgoingCommandHandler::GenerateFailureResult(CControlCommand& aCommand, TInt aResult)
+	{
+	// Response is only necessary if the bearer knows about this command.
+	if(aCommand.KnownToBearer() && (aCommand.Frame().Opcode() == AVC::EPassThrough))
+		{
+		if (aCommand.InsertCoreResult(aResult) == KErrNone)
+			{
+			if(aCommand.Click())
+				{
+				aCommand.SetCoreButtonAction(ERemConCoreApiButtonClick, ETrue);
+				}
+			else if(aCommand.ButtonAct() == AVCPanel::EButtonPress)
+				{
+				aCommand.SetCoreButtonAction(ERemConCoreApiButtonPress, ETrue);
+				}
+			else
+				{
+				aCommand.SetCoreButtonAction(ERemConCoreApiButtonRelease, ETrue);
+				}
+
+			iBearer.MrccciNewResponse(aCommand);
+           }
+		}	
+	}
+	
+/** Generate a command to the remote.
+
+This is needed in situations where the application has not met the avrcp
+button refresh requirements so we need to internally generate something
+to stop the remote getting a bad impression of us.
+
+@param aCommand The command to be issue again.
+*/
+void CRcpOutgoingCommandHandler::GenerateCommandL(CControlCommand& aCommand, TRemConCoreApiButtonAction aButtonAct)
+	{
+	LOG_FUNC
+	
+	RBuf8 commandData;
+	commandData.CreateMaxL(KRemConCoreApiButtonDataLength);
+	
+	AvrcpUtils::SetCommandDataFromInt(commandData, 
+		KRemConCoreApiButtonDataOffset, KRemConCoreApiButtonDataLength, aButtonAct);
+
+	// This will not leave before taking ownership of commandData.
+	SendCommandL(aCommand.RemConInterfaceUid(), aCommand.RemConOperationId(), aCommand.RemConCommandId(), commandData, EFalse, 
+		aCommand.RemoteAddress(), EFalse, EFalse);
+	}
+
+//------------------------------------------------------------------------------------
+// Timer functions
+//------------------------------------------------------------------------------------
+
+/** Starts the response timer.
+
+AVRCP mandates a remote respond within 100ms of receiving a command.
+This is the timer for that, and is started when a command has 
+successfully been sent.
+
+@param aCommand The command to start the timer for.
+*/		
+void CRcpOutgoingCommandHandler::StartResponseTimer(CControlCommand& aCommand)
+	{
+	LOG_FUNC
+	// These use placement new, so cannot fail
+	TAvrcpTimerExpiryInfo* timerInfo = new(aCommand.TimerExpiryInfo())TAvrcpTimerExpiryInfo(this, aCommand);
+
+	TCallBack callback(ResponseExpiry, timerInfo);
+	TDeltaTimerEntry* timerEntry = new(aCommand.TimerEntry())TDeltaTimerEntry(callback);
+	
+	iTimer.Queue(KRcpResponseTimeOut, *timerEntry);
+	}
+	
+/** Callback when response timer expires.
+
+This is a static forwarding function.
+
+@param aExpiryInfo The information used by the real ResponseExpiry to
+					deal with the timer expiry.
+*/
+TInt CRcpOutgoingCommandHandler::ResponseExpiry(TAny* aExpiryInfo)
+	{
+	LOG_STATIC_FUNC
+	TAvrcpTimerExpiryInfo *timerInfo = reinterpret_cast<TAvrcpTimerExpiryInfo*>(aExpiryInfo);
+	(reinterpret_cast<CRcpOutgoingCommandHandler*>(timerInfo->iHandler))->ResponseExpiry(timerInfo->iCommand);
+	
+	return KErrNone;
+	}
+	
+/** Deals with response timeout.
+
+This sends a timeout response to RemCon.
+
+@param aCommand	The CControlCommand that has expired.
+*/
+void CRcpOutgoingCommandHandler::ResponseExpiry(CControlCommand& aCommand)
+	{
+	LOG_FUNC
+	
+	GenerateFailureResult(aCommand, KErrTimedOut);	
+	
+	// Failed to get a response to this, don't bother about trying
+	// to release it.
+	if(iUnreleasedCommand == &aCommand)
+		{
+		iUnreleasedCommand = NULL;
+		__ASSERT_DEBUG(!iUnreleasedHasResponse, AvrcpUtils::Panic(EAvrcpPressHasPhantomResponse));
+		}
+
+	aCommand.iHandlingLink.Deque();
+	aCommand.DecrementUsers();
+	}
+
+/** Starts the release timer.
+
+AVRCP requires a press to be refreshed less than 2s after the first
+press, if it is not to be assumed to have been released.  We pass 
+this requirement on to RemCon clients as we don't know when they might
+go away and we don't want to keep buttons pressed forever.  If the 
+release timer expires we will assume a release, and generate it to
+the remote.
+
+@param aCommand The command to start the timer for.
+*/	
+void CRcpOutgoingCommandHandler::StartReleaseTimer(CControlCommand& aCommand)
+	{
+	LOG_FUNC
+
+	// These cannot fail as we use placement new
+	TAvrcpTimerExpiryInfo* timerInfo = new(aCommand.TimerExpiryInfo())TAvrcpTimerExpiryInfo(this, aCommand);
+		
+	TCallBack callback(ReleaseExpiry, timerInfo);
+	TDeltaTimerEntry* timerEntry = new(aCommand.TimerEntry())TDeltaTimerEntry(callback);
+
+	iTimer.Queue(KRcpOutgoingButtonReleaseTimeout, *timerEntry);
+	}
+
+/** Callback when release timer expires.
+
+This is a static forwarding function.
+
+@param aExpiryInfo The information used by the real ReleaseExpiry to
+					deal with the timer expiry.
+*/	
+TInt CRcpOutgoingCommandHandler::ReleaseExpiry(TAny* aExpiryInfo)
+	{
+	LOG_STATIC_FUNC
+	TAvrcpTimerExpiryInfo *timerInfo = reinterpret_cast<TAvrcpTimerExpiryInfo*>(aExpiryInfo);
+	(reinterpret_cast<CRcpOutgoingCommandHandler*>(timerInfo->iHandler))->ReleaseExpiry(timerInfo->iCommand);
+	
+	return KErrNone;
+	}
+
+/** Deals with expiry of release timer.
+
+1) Generate release for this command.
+2) Send release to remote.
+
+@param aCommand	The CControlCommand that has expired.
+*/
+void CRcpOutgoingCommandHandler::ReleaseExpiry(CControlCommand& aCommand)
+	{
+	LOG_FUNC
+	__ASSERT_DEBUG((aCommand.ButtonAct() == AVCPanel::EButtonPress), AvrcpUtils::Panic(EAvrcpReleaseExpiryForRelease));
+	__ASSERT_DEBUG(&aCommand == iUnreleasedCommand, AvrcpUtils::Panic(EAvrcpReleaseExpiryForOldCommand));
+	__ASSERT_DEBUG(iUnreleasedHasResponse, AvrcpUtils::Panic(EAvrcpReleaseTimerStartedWithoutResponse));
+		
+	iReleaseTimerExpiryCount++;
+	
+	TBool commandCompleted = ETrue;
+	// If the client is not yet obliged to refresh this, then send another press.  Otherwise generate
+	// the release for them.
+	if((iReleaseTimerExpiryCount * KRcpOutgoingButtonReleaseTimeout) < KRemConCoreApiPressRefreshInterval)
+		{
+		// This will try and generate a press that is identical to the original
+		// aCommand, but with a new AVCTP transaction id.
+		TRAPD(err, GenerateCommandL(aCommand, ERemConCoreApiButtonPress));
+		
+		if(!err)
+			{
+			// Start the timer again on the original command
+			StartReleaseTimer(aCommand);
+			commandCompleted = EFalse;
+			}
+		}
+	else
+		{
+		// Try an generate a release, but if it fails we just have to let the 
+		// remote assume it.
+		TRAP_IGNORE(GenerateCommandL(aCommand, ERemConCoreApiButtonRelease));
+		}
+		
+	// This condition may be true because
+	// -  we have failed to generate a press, in which case the remote is entitled 
+	//    to assume this is released, so we just give up on it.
+	// or
+	// -  the client has not met its press refresh obligation (whether we've 
+	//    successfully generated a release or not.
+	// In either case we won't do anything more with this command. 
+	if(commandCompleted)
+		{				
+		aCommand.iHandlingLink.Deque();
+		aCommand.DecrementUsers();
+		
+		iUnreleasedCommand = NULL;
+		iUnreleasedHasResponse = EFalse;
+		iReleaseTimerExpiryCount = 0;
+		}
+	}