Hi all any help would be appreciated. I'm trying to wrap an SDK for a card swiper. The sdk contains a jar, sample program and a pdf file. When I run the app with the wrapped version of the sdk, it locks when I try to call the start function. I can run the sample application and step through this call and it has no problems at all. There are confidentiality issues with exposing their documentation and code, but let me give you at least some bits.
The "start" function causes events to fire, and I really believe the problem is that I'm not doing something right to receive them.
I had to chop some of the Activity-related bits out to get this to post.
The following code is an Activity, but I'll post my port attempt after:
Here is my library port:
Here is my class library:
I appreciate any suggestions.
The "start" function causes events to fire, and I really believe the problem is that I'm not doing something right to receive them.
I had to chop some of the Activity-related bits out to get this to post.
The following code is an Activity, but I'll post my port attempt after:
B4X:
public class SwiperExample extends Activity {---------------------------------
private final static String INTENT_ACTION_CALL_STATE = "com.bbpos.swiper.CALL_STATE";
private SwiperController swiperController;
private SwiperStateChangedListener stateChangedListener;
private IncomingCallServiceReceiver incomingCallServiceReceiver;
private void setSwiperControllerValues() {
if (swiperController == null) return;
SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(this);
boolean setDetectDeviceChangePref = settings.getBoolean("setDetectDeviceChangePref", true);
boolean setFskRequiredPref = settings.getBoolean("setFskRequiredPref", false);
String setTimeoutlistPref = settings.getString("setTimeoutlistPref", "-1");
String setKsnChargeUplistPref = settings.getString("setKsnChargeUplistPref", "0.6");
String setSwipeChargeUplistPref = settings.getString("setSwipeChargeUplistPref", "0.6");
swiperController.setDetectDeviceChange(setDetectDeviceChangePref);
swiperController.setFskRequired(setFskRequiredPref);
swiperController.setSwipeTimeout(Double.parseDouble(setTimeoutlistPref));
swiperController.setChargeUpTime(Double.parseDouble(setSwipeChargeUplistPref));
swiperController.setKsnChargeUpTime(Double.parseDouble(setKsnChargeUplistPref));
}
private boolean isGetFirmwareVersion = false;
private class GetFirmwareTask extends AsyncTask<Void, Void, String> {
protected void onPreExecute() {
showProgress("loading...", null);
}
protected String doInBackground(Void... params) {
if (swiperController.getSwiperState() == SwiperController.SwiperControllerState.STATE_IDLE) {
return swiperController.getSwiperFirmwareVersion();
}
return "";
}
protected void onPostExecute(String firmware) {
dismissProgress();
SharedPreferences customSharedPreference = getSharedPreferences(SwiperPreferences.STORE_NAME, Activity.MODE_PRIVATE);
SharedPreferences.Editor editor = customSharedPreference.edit();
editor.putString("apiVersionPref", SwiperController.getSwiperAPIVersion());
editor.putString("firmwareVersionPref", firmware);
editor.commit();
if (isGetFirmwareVersion) {
resetUIControls();
outMsg.setText("Firmware Version: " + firmware);
}
else {
startActivityForResult(new Intent(SwiperExample.this, SwiperPreferences.class), REQ_SYSTEM_SETTINGS);
}
}
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.swiper_menu);
initViews();
startCallStateService();
stateChangedListener = new StateChangedListener();
swiperController = SwiperController.createInstance(this.getApplicationContext(), stateChangedListener);
setSwiperControllerValues();
}
private final int MENU_COPY = 1;
private final int MENU_FIRMWARE = 2;
private final int MENU_SETTINGS = 3;
private final int REQ_SYSTEM_SETTINGS = 0;
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQ_SYSTEM_SETTINGS) {
setSwiperControllerValues();
}
}
@Override
public void onDestroy() {
super.onDestroy();
if (swiperController != null)
swiperController.deleteSwiper();
endCallStateService();
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
if (swiperController != null) {
if (swiperController.getSwiperState() != SwiperControllerState.STATE_IDLE) {
swiperController.stopSwiper();
}
swiperController.deleteSwiper();
swiperController = null;
}
endCallStateService();
}
else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP || keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
return true;
}
return super.onKeyDown(keyCode, event);
}
// -----------------------------------------------------------------------
// Swiper API
// -----------------------------------------------------------------------
private class StateChangedListener implements SwiperStateChangedListener {
@Override
public void onCardSwipeDetected() {
//start = System.currentTimeMillis();
outMsg.setText("Reading card data...");
}
@Override
public void onDecodeCompleted(HashMap<String, String> decodeData) {
resetUIControls();
StringBuilder sb = new StringBuilder();
for (HashMap.Entry<String, String> entry : decodeData.entrySet()) {
sb.append(entry.getKey() + ": " + entry.getValue() + "\n");
}
String formatID = decodeData.get("formatID");
String encTrack = decodeData.get("encTrack");
String partialTrack = decodeData.get("partialTrack");
sb.append("packEncTrackData: " + SwiperController.packEncTrackData(formatID, encTrack, partialTrack));
outMsg.setText("Decode Completed");
outMsg2.setText(sb.toString());
}
@Override
public void onDecodeError(DecodeResult decodeResult) {
resetUIControls();
if (decodeResult == DecodeResult.DECODE_SWIPE_FAIL) {
outMsg.setText("Swipe fail");
} else if (decodeResult == DecodeResult.DECODE_TAP_FAIL) {
outMsg.setText("Tap fail");
} else if (decodeResult == DecodeResult.DECODE_CRC_ERROR) {
outMsg.setText("CRC error");
} else if (decodeResult == DecodeResult.DECODE_COMM_ERROR) {
outMsg.setText("Communication error");
} else if (decodeResult == DecodeResult.DECODE_CARD_NOT_SUPPORTED) {
outMsg.setText("Card not supported");
} else {
outMsg.setText("Unknown decode error");
}
}
@Override
public void onError(String message) {
resetUIControls();
outMsg.setText(message);
}
@Override
public void onGetKsnCompleted(String ksn) {
resetUIControls();
outMsg.setText("ksn: " + ksn);
}
@Override
public void onInterrupted() {
resetUIControls();
outMsg.setText("Interrupted");
}
@Override
public void onNoDeviceDetected() {
resetUIControls();
setToastMessage("swiper unplugged");
}
@Override
public void onTimeout() {
resetUIControls();
outMsg.setText("Timeout");
}
@Override
public void onWaitingForCardSwipe() {
outMsg.setText("Waiting card swipe...");
}
@Override
public void onWaitingForDevice() {
outMsg.setText("Waiting for device...");
}
@Override
public void onDevicePlugged() {
setToastMessage("Device plugged! Checking if it is Swiper...");
try {
if (swiperController.getSwiperState() == SwiperController.SwiperControllerState.STATE_IDLE) {
resetUIControls();
isSwiperHereButton.setEnabled(false);
swipeButton.setEnabled(false);
getKsnButton.setEnabled(false);
swiperController.isSwiperHere();
}
}
catch (IllegalStateException ex) {
outMsg.setText("Invalid state");
}
}
@Override
public void onDeviceUnplugged() {
setToastMessage("Swiper uplugged");
}
@Override
public void onSwiperHere(boolean isHere) {
resetUIControls();
if (isHere)
outMsg.setText("Swiper is here");
else
outMsg.setText("Swiper is not here");
}
}
private void startCallStateService() {
startService(new Intent(INTENT_ACTION_CALL_STATE));
if (incomingCallServiceReceiver == null) {
incomingCallServiceReceiver = new IncomingCallServiceReceiver();
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(SwiperCallStateService.INTENT_ACTION_INCOMING_CALL);
this.registerReceiver(incomingCallServiceReceiver, intentFilter);
}
}
private void endCallStateService() {
stopService(new Intent(INTENT_ACTION_CALL_STATE));
if (incomingCallServiceReceiver != null) {
this.unregisterReceiver(incomingCallServiceReceiver);
incomingCallServiceReceiver = null;
}
}
public void resetUIControls() {
outMsg.setText("");
outMsg2.setText("");
isSwiperHereButton.setEnabled(true);
isSwiperHereButton.setText("IS SWIPER HERE?");
swipeButton.setEnabled(true);
swipeButton.setText("SWIPE");
getKsnButton.setEnabled(true);
getKsnButton.setText("GET KSN");
}
swipeButton = (Button) findViewById(R.id.swipeButton);
swipeButton.setText("SWIPE");
swipeButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View arg0) {
try {
if (swipeButton.getText() == "SWIPE") {
resetUIControls();
swipeButton.setText("STOP");
getKsnButton.setEnabled(false);
isSwiperHereButton.setEnabled(false);
swiperController.startSwiper();
}
else if (swipeButton.getText() == "STOP") {
resetUIControls();
swipeButton.setText("SWIPE");
swiperController.stopSwiper();
}
}
catch (IllegalStateException ex) {
outMsg.setText("Invalid state");
ex.printStackTrace();
}
}
});
private class IncomingCallServiceReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals(SwiperCallStateService.INTENT_ACTION_INCOMING_CALL)) {
////log("received INTENT_ACTION_INCOMING_CALL");
outMsg.setText("Incoming call detected!");
try {
if (swiperController.getSwiperState() != SwiperControllerState.STATE_IDLE) {
swipeButton.setText("SWIPE");
swiperController.stopSwiper();
}
}
catch (IllegalStateException ex) {
outMsg.setText("Invalid state");
ex.printStackTrace();
}
}
} // end-of onReceive
}
}
Here is my library port:
B4X:
package ideasourceswiper;
import java.util.HashMap;
import java.util.Map.Entry;
import com.bbpos.swiper.SwiperController;
import com.bbpos.swiper.SwiperController.DecodeResult;
import com.bbpos.swiper.SwiperController.SwiperStateChangedListener;
import android.content.SharedPreferences;
import android.util.Log;
import anywheresoftware.b4a.BA;
import anywheresoftware.b4a.BA.Author;
import anywheresoftware.b4a.BA.DependsOn;
import anywheresoftware.b4a.BA.Events;
import anywheresoftware.b4a.BA.Permissions;
import anywheresoftware.b4a.BA.RaisesSynchronousEvents;
import anywheresoftware.b4a.BA.ShortName;
@ShortName("RoamPaySwiper") // this is the name as it appear on the IDE
@Permissions(values = { "android.permission.INTERNET"
, "android.permission.READ_PHONE_STATE"
, "android.permission.RECORD_AUDIO"
, "android.permission.MODIFY_AUDIO_SETTINGS"
})
@Author("Gary Kimble") // your name
@DependsOn(values = {"swiperapi-android-4.3.0"})
public class IdeaSourceSwiper {
private BA _ba;
private String _eventName;
private SwiperStateChangedListener stateChangedListener;
private SwiperController swiperController;
public String outMsg;
public String encryptedTrack;
public SharedPreferences settings;
// -----------------------------------------------------------------------
// Interface
// -----------------------------------------------------------------------
private void setSwiperControllerValues() {
if (swiperController == null) return;
/*
if (settings == null) settings = PreferenceManager.getDefaultSharedPreferences(BA.applicationContext);
boolean setDetectDeviceChangePref = settings.getBoolean("setDetectDeviceChangePref", true);
boolean setFskRequiredPref = settings.getBoolean("setFskRequiredPref", false);
String setTimeoutlistPref = settings.getString("setTimeoutlistPref", "-1");
String setKsnChargeUplistPref = settings.getString("setKsnChargeUplistPref", "0.6");
String setSwipeChargeUplistPref = settings.getString("setSwipeChargeUplistPref", "0.6");
*/
swiperController.setDetectDeviceChange(true);
swiperController.setFskRequired(false);
swiperController.setSwipeTimeout(-1);
swiperController.setChargeUpTime(.6);
swiperController.setKsnChargeUpTime(.6);
}
public IdeaSourceSwiper(){
}
public void Initialize(final BA ba, String eventname){
_ba = ba;
_eventName = eventname;
stateChangedListener = new StateChangedListener();
swiperController = SwiperController.createInstance(BA.applicationContext, stateChangedListener);
setSwiperControllerValues();
Log.d("B4A", "Initiliazed!");
}
public Boolean isInitialized(){
if (_ba == null)
return false;
else
return true;
}
public Boolean checkSwiper(){
return swiperController.isDevicePresent();
}
public Boolean isFskRequired(){
return swiperController.isFskRequired();
}
@RaisesSynchronousEvents
public void deleteSwiper(){
swiperController.deleteSwiper();
}
public String QuerySwiperFirmwareVersion(){
return swiperController.getSwiperFirmwareVersion();
}
@RaisesSynchronousEvents
public void QuerySwiperKSN(){
swiperController.getSwiperKsn();
}
public void isSwiperHere(){
swiperController.isSwiperHere();
}
public String QuerySwiperState(){
return swiperController.getSwiperState().toString();
}
@RaisesSynchronousEvents
public void StartSwiper(){
Log.d("B4A", "Starting Swiper");
SwiperController tmpSwipe = SwiperController.getInstance();
tmpSwipe.startSwiper();
//swiperController.startSwiper();
Log.d("B4A", "After Starting Swiper");
}
@RaisesSynchronousEvents
public void StopSwiper(){
swiperController.stopSwiper();
}
@RaisesSynchronousEvents
public void WaitSwiper() throws InterruptedException{
swiperController.wait();
}
@RaisesSynchronousEvents
public String QuerySwiperAPIVersion(){
return SwiperController.getSwiperAPIVersion();
}
///////////////////////////////////////////////////////////////////////////
@ShortName("SwiperHandler") // this is the name as it appear on the IDE
@Permissions(values = { "android.permission.INTERNET"
, "android.permission.READ_PHONE_STATE"
, "android.permission.RECORD_AUDIO"
, "android.permission.MODIFY_AUDIO_SETTINGS"
})
@Author("Gary Kimble") // your name
@Events(values={ "oncardswipedetected(message as String)",
"ondecodecompleted(outParams() as String)",
"ondecodeerror(DecResult as String)",
"onerror(message as String)",
"ongetksncompleted(ksn as String)",
"oninterrupted(message as String)",
"onnodevicedetected(message as String)",
"ontimeout(message as String)",
"onwaitingforcardswipe(message as String)",
"onwaitingfordevice(message as String)",
"ondeviceplugged(message as String)",
"ondeviceunplugged(message as String)",
"onswiperhere(isHere as Boolean)"
})
public class StateChangedListener implements SwiperStateChangedListener {
@Override
public void onCardSwipeDetected() {
if (_ba.subExists(_eventName + "_oncardswipedetected")) {
_ba.raiseEvent(this, _eventName + "_oncardswipedetected", "Card Swipe Detected");
}
}
@Override
public void onDecodeCompleted(HashMap<String, String> decodeData) {
StringBuilder sb = new StringBuilder();
for (Entry<String, String> entry : decodeData.entrySet()) {
sb.append(entry.getKey() + ": " + entry.getValue() + "\n");
}
String formatID = decodeData.get("formatID");
String encTrack = decodeData.get("encTrack");
String partialTrack = decodeData.get("partialTrack");
sb.append("packEncTrackData: " + SwiperController.packEncTrackData(formatID, encTrack, partialTrack));
outMsg = "Card Read";
encryptedTrack = sb.toString();
String [] bMap = convertToMap(decodeData);
if (_ba.subExists(_eventName + "_ondecodecompleted")) {
_ba.raiseEvent(this, _eventName + "_ondecodecompleted", new Object[]{bMap});
}
}
@Override
public void onDecodeError(DecodeResult decodeResult) {
if (_ba.subExists(_eventName + "_ondecodeerror")) {
_ba.raiseEvent(this, _eventName + "_ondecodeerror", decodeResult.toString());
}
}
@Override
public void onError(String message) {
if (_ba.subExists(_eventName + "_onerror")) {
_ba.raiseEvent(this, _eventName + "_onerror", message);
}
}
@Override
public void onGetKsnCompleted(String ksn) {
if (_ba.subExists(_eventName + "_ongetksncompleted")) {
_ba.raiseEvent(this, _eventName + "_ongetksncompleted", ksn);
}
}
@Override
public void onInterrupted() {
if (_ba.subExists(_eventName + "_oninterrupted")) {
_ba.raiseEvent(this, _eventName + "_oninterrupted", "Card Read Interuppted");
}
}
@Override
public void onNoDeviceDetected() {
if (_ba.subExists(_eventName + "_onnodevicedetected")) {
_ba.raiseEvent(this, _eventName + "_onnodevicedetected", "Swiper Unplugged");
}
}
@Override
public void onTimeout() {
if (_ba.subExists(_eventName + "_ontimeout")) {
_ba.raiseEvent(this, _eventName + "_ontimeout", "Swiper Timed Out");
}
}
@Override
public void onWaitingForCardSwipe() {
if (_ba.subExists(_eventName + "_onwaitingforcardswipe")) {
_ba.raiseEvent(this, _eventName + "_onwaitingforcardswipe", "Waiting card swipe...");
}
}
@Override
public void onWaitingForDevice() {
if (_ba.subExists(_eventName + "_onwaitingforcardswipe")) {
_ba.raiseEvent(this, _eventName + "_onwaitingforcardswipe", "Waiting for device...");
}
}
@Override
public void onDevicePlugged() {
String msg = "";
try {
if (swiperController.getSwiperState() == SwiperController.SwiperControllerState.STATE_IDLE) {
swiperController.isSwiperHere();
msg = "Checking for Swiper...";
}
}
catch (IllegalStateException ex) {
msg = "Invalid state for device...";
}
if (_ba.subExists(_eventName + "_ondeviceplugged")) {
_ba.raiseEvent(this, _eventName + "_ondeviceplugged", msg);
}
}
@Override
public void onDeviceUnplugged() {
if (_ba.subExists(_eventName + "_ondeviceunplugged")) {
_ba.raiseEvent(this, _eventName + "_ondeviceunplugged", "Swiper Unplugged");
}
}
@Override
public void onSwiperHere(boolean isHere) {
Log.d("B4A", "onSwiperHere: " + isHere);
if (_ba.subExists(_eventName + "_onswiperhere")) {
_ba.raiseEvent(this, _eventName + "_onswiperhere", isHere);
}
}
private String[] convertToMap(HashMap<String, String> javaMap) {
String[] m = new String[javaMap.size()];
Integer i = 0;
for (Entry<String, String> e : javaMap.entrySet()) {
m[i] = e.getKey().toString()+"="+e.getValue().toString();
i++;
}
return m;
}
}
}
Here is my class library:
B4X:
'Class module
#RaisesSynchronousEvents: swiper_onwaitingforcardswipe
Sub Class_Globals
Private objRoamPay As RoamPayBridge
Private objSwiper As RoamPaySwiper
Private strActivationURL As String
Dim blnInSession = False As Boolean
Dim blnSuccessfulPing = False As Boolean
Dim blnSuccessfulProcess = False As Boolean
Dim blnProcessingPayment = False As Boolean
Dim blnLoggingIn = False As Boolean
Dim swiperksn = "" As String
End Sub
'Initializes the object. You can add parameters to this method if needed.
Public Sub Initialize(ActivationURL As String)
If Not(objRoamPay.isInitialized) Then
objRoamPay.Initialize("roampay")
End If
If Not(objSwiper.isInitialized) Then
objSwiper.Initialize("swiper")
End If
strActivationURL = ActivationURL
objSwiper.isSwiperHere
End Sub
Public Sub DeviceisPresent() As Boolean
Return objSwiper.checkSwiper
End Sub
Public Sub PingSwiper
objSwiper.isSwiperHere
End Sub
Sub Login(username As String, password As String)
blnLoggingIn = True
objRoamPay.Login(username, password)
End Sub
Sub Ping()
objRoamPay.Ping
End Sub
Sub ProcessCreditSale(Amount As String, CardNum As String, cvs As String, exp As String)
blnProcessingPayment = True
objRoamPay.KeyedCreditSale(Amount, CardNum, cvs, exp)
End Sub
Sub roampay_initiatesessionresponse(success As Boolean, outParams() As String)
blnLoggingIn = False
If success Then
If outParams(0).Contains("0000") Then
blnInSession = True
Log("Session was successfully created.")
CallSub2(Main, "WriteToLog", "Connected to Roam")
Else
blnInSession = False
Log("Session was NOT successfully created.")
CallSub2(Main, "WriteToLog", "NOT CONNECTED TO ROAM")
End If
Else
blnInSession = False
CallSub2(Main, "WriteToLog", "NOT CONNECTED TO ROAM")
Log("Session was not successfully created.")
End If
CallSub(ps_service, "DetermineScreen")
End Sub
Sub roampay_pingresponse(outParams() As String)
Log("Ping Response: " & outParams)
If outParams(0).Contains("0000") Then
blnSuccessfulPing = True
Else
blnSuccessfulPing = False
End If
Log("There are: " & outParams.Length & " values.")
For v = 0 To outParams.Length -1
Log("Ping Response Entry " & v & ":" & outParams(v))
Next
End Sub
Sub roampay_roampayapiresponse(success As Boolean, outParams() As String)
'Todo Check values in the params and make decisions.
blnProcessingPayment = False
Dim thisString As StringBuilder
thisString.Initialize
For v = 0 To outParams.Length -1
Log("API Response Entry " & v & ":" & outParams(v))
Dim tmp = outParams(v) As String
Select v
Case 0
thisString.Append(tmp.SubString(tmp.IndexOf("=")+1)).Append(CRLF)
Case 2
thisString.Append(tmp.SubString(tmp.IndexOf("=")+1)).Append(CRLF)
Case 3
thisString.Append("TransactionID: ").Append(tmp.SubString(tmp.IndexOf("=")+1)).Append(CRLF)
Case 8
thisString.Append("AuthCode: ").Append(thisString.Append(tmp.SubString(tmp.IndexOf("=")+1)))
Case 6
thisString.Append("Amount: ").Append(thisString.Append(tmp.SubString(tmp.IndexOf("=")+1)).Append(CRLF))
Case 1
If tmp.Contains("0000") Then
blnSuccessfulProcess = True
Else
blnSuccessfulProcess = False
End If
End Select
Next
Log("RoamPayMessageString: " & thisString.ToString)
CallSub2(Main, "UpdateRoam", thisString.ToString)
End Sub
Sub startSwiper()
If objSwiper.QuerySwiperState.Contains("IDLE") Then
objSwiper.startSwiper
Else
stopSwiper
End If
End Sub
Sub stopSwiper()
objSwiper.stopSwiper
End Sub
Sub deleteSwiper()
objSwiper.deleteSwiper
End Sub
Sub swiper_ongetksncompleted(ksn As String)
Log("Ksn received: " & ksn)
swiperksn = ksn
End Sub
Sub swiper_oncardswipedetected(message As String)
Log("Swiping")
End Sub
Sub swiper_onswiperhere(isHere As Boolean)
If isHere Then
Log("Swiper is present!")
objSwiper.QuerySwiperKSN
Else
Log("Swiper is missing!")
End If
End Sub
Sub swiper_ondecodecompleted(outParams() As String)
objRoamPay.SwipedCreditSale(ps_service.myMeter.currentTotal, swiperksn, objSwiper.encryptedTrack)
End Sub
Sub swiper_ondecodeerror(DecResult As String)
'TODO implement Error Handling
Log("Swiper decode Error: " & DecResult)
End Sub
Sub swiper_onerror(message As String)
'TODO implement Error Handling
Log("Swiper Error was: " & message)
End Sub
Sub swiper_oninterrupted(message As String)
'TODO implement Handler
Log("Swiper Interupted!!!")
End Sub
Sub swiper_onwaitingforcardswipe(message As String)
'TODO implement Handler
Log("Swiper is ready for card swipe!")
'ProgressDialogHide
If ps_service.myMeter.MeterStatus = 3 Then
'ProgressDialogShow("Ready! Please swipe when you are ready.")
End If
End Sub
Sub swiper_ontimeout(message As String)
'TODO implement handler
Log("Swiper timed out!")
'ProgressDialogHide
'ProgressDialogShow("Timed out! Please wait a half sec...")
objSwiper.stopSwiper
objSwiper.startSwiper
End Sub
Sub swiper_onwaitingfordevice(message As String)
'TODO implement handler
Log("Waiting for device fired: " & message)
End Sub
I appreciate any suggestions.