Activity to Android Library communication options (IPC) - android

Lets say I have a SDK in form of Android Library (aar) that offers some basic media processing (it has its own UI as a single activity). Currently, any client Android app, when invoking my SDK sends required data via Bundle.
Now, for various reasons some extra info for the data being sent may be required after my SDK is invoked so I would need a two-way communication with the caller app.
In short, from within the SDK I need to be able to check if the client app has implemented some interface so that SDK can use it to communicate with the client app (the client may choose not to provide the implementation in which case the SDK will fallback to internal, the default implementation..).
Anyway, the way I've done it initialy, is as following:
Within SDK I have exposed the data provider interface:
public interface ISDKDataProvider {
void getMeSomething(Params param, Callback callback);
SomeData getMeSomethingBlocking(Params param);
}
a Local binder interface that should return an instance of the implemented interface:
public interface LocalBinder {
ISDKDataProvider getService();
}
Then, on the client side, an application using the SDK, must provide a service that does the job and implements those interfaces:
public class SDKDataProviderService extends Service implements ISDKDataProvider {
private final IBinder mBinder = new MyBinder();
#Override
public IBinder onBind(Intent intent) {
return mBinder;
}
#Override
public void getMeSomething(Params param, Callback callback) {
// ... do something on another thread
// once done, invoke callback and return result to the SDK
}
#Override
public SomeData getMeSomethingBlocking(Params param);{
// do something..
// return SomeData
}
public class MyBinder extends Binder implements LocalBinder {
#Override
public ISDKDataProvider getService() {
return ISDKDataProvider.this;
}
}
}
Additionally, when invoking SDK, the clinet app passes the ComponentName via bundle options:
sdkInvokationOptions.put("DATA_PROVIDER_EXTRAS", new ComponentName(getPackageName(), SDKDataProviderService.class.getName()));
..from the SDK, I then check whether the service exists and whether we can bind to it:
final ComponentName componentName = // get passed componentname "DATA_PROVIDER_EXTRAS"
if (componentName != null) {
final Intent serviceIntent = new Intent(componentName.getClassName());
serviceIntent.setComponent(componentName);
bindService(serviceIntent, mConnection, Context.BIND_AUTO_CREATE);
}
where mConnection is:
private boolean mBound;
private ISDKDataProvider mService;
private ServiceConnection mConnection = new ServiceConnection() {
#Override
public void onServiceConnected(ComponentName name, IBinder service) {
final LocalBinder binder = (LocalBinder) service;
mService = binder.getService();
mBound = true;
}
#Override
public void onServiceDisconnected(ComponentName name) {
mBound = false;
}
};
This seem to work ok and it looks clean but my question is there a better way\practice to accomplish the same type of a communication?

Your API should be simple, for example a static class/singleton:
MyAPI.start()
MyAPI.stop()
MyAPI.sendMessgage(mgs,callback)
MyAPI.setCallback(callback)
About the service, I think you should decide who is in charge of it.
If its the user - leave him the implementation, just give the API.
If you always want your API to run as a service, implement it yourself and inside the singleton handle the messaging (you can do so with intents, for example).
I used this architecture for image processing service too :)
My API wrapping class looked like:
public class MyAPI {
public static final String TAG = "MyAPI";
public MyAPI() {
}
public static MyAPI.Result startMyAPI(ScanParams scanParams) {
try {
Log.d("MyAPI", "in startMyAPI");
if (scanParams.ctx == null || scanParams.appID == null || scanParams.api_key == null) {
Log.d("MyAPI", "missing parameters");
return MyAPI.Result.FAILED;
}
if (scanParams.userID == null) {
scanParams.userID = "no_user";
}
if (scanParams.minBatteryThreshold == null) {
scanParams.minBatteryThreshold = Consts.DEFAULT_BATTERY_THRESHOLD;
}
if (scanParams.minCpuThreshold == null) {
scanParams.minCpuThreshold = Consts.DEFAULT_CPU_THRESHOLD;
}
if (!DeviceUtils.checkBatteryLevel(scanParams.ctx, (float)scanParams.minBatteryThreshold)) {
ReportUtils.error("low battery");
return MyAPI.Result.FAILED;
}
if (MyAPIUtils.isRunning(scanParams.ctx)) {
return MyAPI.Result.FAILED;
}
Intent intent = new Intent(scanParams.ctx, MyAPIService.class);
ServiceParams serviceParams = new ServiceParams(scanParams.appID, scanParams.api_key, scanParams.userID, scanParams.minBatteryThreshold, scanParams.minCpuThreshold);
intent.putExtra("SERVICE_PARAMS", serviceParams);
scanParams.ctx.startService(intent);
} catch (Exception var3) {
var3.printStackTrace();
}
return MyAPI.Result.SUCCESS;
}
public static void getBestCampaignPrediction(Context ctx, String apiKey, String appID, String creativeID, AppInterface appInterface) {
try {
String deviceID = DeviceUtils.getDeviceID(ctx);
GetBestCampaignTask getBestCampaignTask = new GetBestCampaignTask(ctx, apiKey, deviceID, appID, creativeID, appInterface);
getBestCampaignTask.execute(new Void[0]);
} catch (Exception var7) {
var7.printStackTrace();
}
}
public static boolean sendAdEvent(Context ctx, String apiKey, Event event) {
boolean res = false;
try {
boolean isValid = Utils.getIsValid(ctx);
if (isValid) {
Long timeStamp = System.currentTimeMillis();
event.setTimeStamp(BigDecimal.valueOf(timeStamp));
event.setDeviceID(DeviceUtils.getDeviceID(ctx));
(new SendEventTask(ctx, apiKey, event)).execute(new Void[0]);
}
} catch (Exception var6) {
var6.printStackTrace();
}
return res;
}
public static enum PredictionLevel {
MAIN_CATEGORY,
SUB_CATEGORY,
ATTRIBUTE;
private PredictionLevel() {
}
}
public static enum Result {
SUCCESS,
FAILED,
LOW_BATTERY,
LOW_CPU,
NOT_AUTHENTICATED;
private Result() {
}
}
}
You can see that startMyAPI actually starts a service and getBestCampaignPrediction runs an async task that communicates with the service behind the scenes and returns its result to appInterface callback. This way the user get a very simple API

Related

Understanding Android Binders

I'm trying to understand a piece of code that uses Android Binders and Messages to perform IPC. I've read a few articles, papers and slides on binders, but I'm still confused and have a hard time understanding the code, since I didn't encounter any tutorials/examples of actually implementing binders in java code. I am listing down the sanitized/abbreviated code below as well as my understanding of what it does and where it gets confusing. I would appreciate if someone could help me understand this, especially pertaining to the flow of Parcels and Messages.
MyService.java
public class MyService extends Service
{
private final IMyService.Stub mBinder;
private Context _mContext;
private MyServiceHandler mHandler;
private HanderThread mHandlerThread;
private IHelperService mHelperService;
public MyService() {
this.mBinder = new IMyService.Stub() {
public void start_data(final String data) {
try {
if (MyService.this.mHelperService == null) {
return;
}
Message msg = MyService.this.getHandler().obtainMessage(3, (Object)data);
MyService.this.getHandler().sendMessage(msg);
} catch (SecurityException ex) {
// do some logging
}
};
}
private void startMessageProcess(final String message) {
// Process the message argument
final byte[] dataArray = this.getByteArray(message);
byte[] returnData;
try {
returnData = this.mHelperService.foo(dataArray);
} catch (Exception e) {
// do some logging
}
Message msg = this.getHandler().obtainMessage(3, (Object)message);
this.getHandler.sendMessage(msg);
}
private String getServerData(int id) {
// builds a Uri based on id and posts it, retrieves String data from HTTP entity and returns it
}
private boolean postDataToServer(final byte[] arrayData) {
// builds a Uri, post data to it, gets the status code, returns true if successful
}
private void sendData(final byte[] arrayData) {
final Intent intent = new Intent();
intent.setAction("com.my.service.intent.action.MY_RESULT");
intent.putExtra("com.my.service.intent.extra.RESULT", 0);
intent.putExtra("com.my.service.intent.extra.MY_DATA", arrayData);
this._mContext.sendBroadcast(intent, "com.my.service.permission.MY_PERMISSION");
}
public Handler getHandler() {
return this.mHandler;
}
public IBinder onBind(final Intent intent) {
if (intent != null) {
final String action = intent.getAction();
if (action != null && action.equals("com.my.service.intent.action.BIND_MY_SERVICE")) {
this._mContext = this.getApplicationContext();
return (IBinder)this.mBinder;
}
}
return null;
}
public void onCreate() {
super.onCreate();
try {
final Class<?> serviceManager = Class.forName("android.os.ServiceManager");
this.mHelperService = mHelperService.Stub.asInterface((IBinder)serviceManager.getMethod("getService", String.class).invoke(serviceManager, "helperservice"));
if (this.mHelperService == null) {
return;
}
}
catch (ClassNotFoundException ex2) {
Log.e("MyService", "Helper service ClassNotFoundException !!!");
}
catch (Exception ex) {
Log.e("MyService", "onCreate() Exception: " + Log.getStackTraceString((Throwable)ex));
}
this.mHandlerThread = new HandlerThread("MyService");
this.mHandlerThread.start();
this.mHandler = new MyServiceHandler(this.mHandlerThread.getLooper());
}
public int onStartCommand(final Intent intent, final int flags, final int startId) {
if (intent != null) {
super.onStartCommand(intent, flags, startId);
}
return START_NOT_STICKY;
}
private final class MyService Handler extends Handler
{
public MyServiceHandler(final Looper looper) {
super(looper);
}
public void handleMessage(final Message message) {
if (message != null) {
switch (message.what) {
case 2: {
final String data = (String)message.obj;
if (data != null) {
MyService.this.startMessageProcess(data);
return
}
break;
}
case 3: {
MyService.this.sendData((byte[])message.obj);
}
}
}
}
}
}
IMyService.java
public interface IMyService extends IInterface
{
void start_data(final String p0) throws RemoteException;
public abstract static class Stub extends Binder implements IMyService
{
public Stub() {
this.attachInterface((IInterface)this, "com.my.service.IMyService");
}
public static IMyService asInterface(final IBinder binder) {
if (binder == null) {
return null;
}
final IInterface queryLocalInterface = binder.queryLocalInterface("com.my.service.IMyService");
if (queryLocalInterface != null && queryLocalInterface instanceof IMyService) {
return (IMyService)queryLocalInterface;
}
return new Proxy(binder);
}
public IBinder asBinder() {
return (IBinder)this;
}
public boolean onTransact(final int code, final Parcel data, final Parcel reply, final int flags) throws RemoteException {
switch (code) {
case 1: {
data.enforceInterface("com.my.service.IMyService");
this.start_data(data.readString());
reply.writeNoException();
return true;
}
case 1000000000: {
reply.writeString("com.my.service.IMyService");
return true;
}
default: {
return super.onTransact(n, data, reply, flags);
}
}
}
private static class Proxy implements IMyService
{
private IBinder mRemote;
Proxy(final IBinder mRemote) {
this.mRemote = mRemote;
}
public IBinder asBinder() {
return this.mRemote;
}
public String getInterfaceDescriptor() {
return "com.my.service.IMyService";
}
#Override
public void start_data(final String data) throws RemoteException {
final Parcel data = Parcel.obtain();
final Parcel reply = Parcel.obtain();
try {
data.writeInterfaceToken("com.my.service.IMyService");
data.writeString(data);
this.mRemote.transact(1, data, reply, 0);
reply.readException();
}
finally {
reply.recycle();
data.recycle();
}
}
}
}
}
IHelperService.java
public interface IHelperService extends IInterface
{
// a bunch of method declarations here
public abstract static class Stub extends Binder implements IHelperService
{
public Stub() {
this.attachInterface((IInterface)this, "android.service.helper.IHelperService");
}
public static IHelperService asInterface(final IBinder binder) {
if (binder == null) {
if (queryLocalInterface != null && queryLocalInterface instanceof IHelperService) {
return (IHelperService)queryLocalInterface;
}
return new Proxy(binder);
}
}
public IBinder asBinder() {
return (IBinder)this;
}
public boolean onTransact(int code, final Parcel data, final Parcle reply, int flags) throws RemoteException {
switch (code) {
// does a bunch of bunch of stuff here based on the incoming code
}
}
}
private static class Proxy implements IHelperService
{
private IBinder mRemote;
Proxy(final IBinder mRemote) {
this.mRemote = mRemote;
}
#Override
// a bunch of method definitions here, everything that was declared above
}
}
I'm having a hard time tracing who sends what to who. For example, in the Proxy of IMyService, start_data calls transact(mRemote), but who is mRemote? Also, in the MyService() constructor as well as startMessageProcess, there are calls to sendMessage, who is it sending to?
Then, there are private methods in MyService that don't seem to be called locally, such as startMessageProcess and getServerData. Who else can call these methods if they're private?

How to access Appication objects from Sync Adapter

I am working on an IM application with open fire server. I'm implementing Sync Adapter for managing the contacts.
From my sync adapter's onPerformSync() if I access the connection object which I kept in my Application class it returns null.
What am I doing wrong? How should I do that?
My Application class
public class MyApp extends Application {
private Connection connection;
private static MyApp instance;
public static MyApp getInstance() {
return instance;
}
public static void setInstance(MyApp instance) {
MyApp.instance = instance;
}
#Override
public void onCreate() {
super.onCreate();
instance = new MyApp();
}
public Connection getAuthenticatedConnection() throws NullPointerException {
if (instance.connection != null
&& instance.connection.isAuthenticated()) {
return instance.connection;
} else {
// Calling service which will create connection and update the object
Intent intent = new Intent(IM_Service_IntentMessaging.ACTION);
intent = new Intent(getContext(), IM_Service_IntentMessaging.class);
Bundle b = new Bundle();
b.putInt("key", IM_Service_IntentMessaging.KEY_CONNECTION);
intent.putExtras(b);
this.context.startService(intent);
throw new NullPointerException();
}
//service calls this method to update the object
public void setConnection(Connection connection) {
instance.connection = connection;
}
}
And my onPerformSync method is as follows...
#Override
public void onPerformSync(Account account, Bundle bundle, String authority,
ContentProviderClient provider, SyncResult syncResult) {
try {
this.connection = MyApp.getInstance()
.getAuthenticatedConnection();
this.roster = this.connection.getRoster();
} catch (NullPointerException e) {
//this line executes always
Log.e(TAG, "connection null...ending....");
return;
}
this.mAccount = account;
Log.d(TAG, "...onPerformSync...");
getAllContacts();
}
When I tried with creating break point in Application's getAuthenticatedConnection() method it didn't get triggered but the service IM_Service_IntentMessaging which i called from there to create connection is working.
I found the solution to my problem is i should remove the android:process=":sync" from the sync adapter's manifest file declaration. Because of that it treats like different process.

Broadcast not received when intent contains parcelable extra

I have an IntentService that is making a network call and receiving back some JSON data. I package this response data in custom object that implements parcelable. If I add this parcelable object to an intent as an extra and then launch an activity using that intent, everything seems to work as expected, i.e. I can retrieve the parcelable from the intent in the newly created activity. However, if I create the intent from within the onHandleIntent() method of my IntentService and then use sendBroadcast(), the broadcast receiver's onReceive() method never fires. If I don't add the parcelable to the intent, though, the onReceive() method fires as expected. Following are some relevant code snippets:
Parcelable Object:
public class JsonResponse implements Parcelable {
private int responseCode;
private String responseMessage;
private String errorMessage;
public JsonResponse() {
}
/*
/ Property Methods
*/
public void setResponseCode(int code) {
this.responseCode = code;
}
public void setResponseMessage(String msg) {
this.responseMessage = msg;
}
public void setErrorMessage(String msg) {
this.errorMessage = msg;
}
/*
/ Parcelable Methods
*/
public static final Creator<JsonResponse> CREATOR = new Creator<JsonResponse>() {
#Override
public JsonResponse createFromParcel(Parcel parcel) {
return new JsonResponse(parcel);
}
#Override
public JsonResponse[] newArray(int i) {
return new JsonResponse[i];
}
};
private JsonResponse(Parcel parcel) {
responseCode = parcel.readInt();
responseMessage = parcel.readString();
errorMessage = parcel.readString();
}
#Override
public void writeToParcel(Parcel parcel, int i) {
parcel.writeInt(responseCode);
parcel.writeString(responseMessage);
parcel.writeString(errorMessage);
}
#Override
public int describeContents() {
return 0;
}
}
onHandle() of IntentService:
protected void onHandleIntent(Intent intent) {
service = new LoginService();
service.login("whoever", "whatever");
JsonResponse response = new JsonResponse();
response.setResponseCode(service.responseCode);
response.setResponseMessage(service.responseMessage);
response.setErrorMessage(service.errorMessage);
Intent i = new Intent();
i.putExtra("jsonResponse", response);
i.setAction(ResultsReceiver.ACTION);
i.addCategory(Intent.CATEGORY_DEFAULT);
sendBroadcast(i);
}
Any ideas? Any insight would be greatly appreciated.
It appears that the problem has to do with the size of the object being added as an extra. When one of the string properties of the response object grows too large, the broadcast apparently fails. I have no sources to confirm this, only some trial and error in manipulating one of the strings while leaving all other variables of the equation constant.

how to pass data between service and it's application in the right way?

i'm a newbie in android. In my app i create a many-to-many chat, and need to update from server a list of Messages. In order to do so, i created a service that updates every second from the server.
My problem is that i don't know how to pass data back to the application. I know that I should do it using intent and broadcast receiver, but in that I stuck with Bundle object that i have to serialize in order to pass it to the app, and it does not make sense to me, since this operation is not that efficient.
For now i'm using the ref to my application (i think it's not that good but don't know why), and after every update from server in the service i activate the application function, and updates it's fields directly. Moreover i think maybe my code will do some good for beginners as well :)
public class UpdateChatService extends Service {
private static final long DELAY_FOR_CHAT_TASK = 0;
private static final long PERIOD_FOR_CHAT_TASK = 1;
private static final TimeUnit TIME_UNIT_CHAT_TASK = TimeUnit.SECONDS;
//private Task retryTask; TODO: check this out
private ScheduledExecutorService scheduler;
private boolean timerRunning = false;
private long RETRY_TIME = 200000;
private long START_TIME = 5000;
#Override
public IBinder onBind(Intent intent) {
return null;
}
#Override
public void onCreate() {
super.onCreate();
scheduleChatUpdate();
}
private void scheduleChatUpdate() {
BiggerGameApp app = (BiggerGameApp) getApplication();
this.scheduler = Executors.newScheduledThreadPool(3);
this.scheduler.scheduleAtFixedRate(new UpdateChatTask(app),
DELAY_FOR_CHAT_TASK, PERIOD_FOR_CHAT_TASK,
TIME_UNIT_CHAT_TASK);
timerRunning = true;
}
#Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (!timerRunning) {
scheduleChatUpdate();
}
return super.onStartCommand(intent, flags, startId);
}
#Override
public void onDestroy() {
super.onDestroy();
if (scheduler != null) {
scheduler.shutdown();
}
timerRunning = false;
}
}
Here is the code of the asynchronous task the runs in the service.
Please tell me what i'm doing wrong, and how should pass data from the service to the application.
public void run() {
try {
if (this.app.getLastMsgFromServer() == null) {
this.app.setLastMsgFromServer(new Message(new Player(DEFAULT_EMAIL), "", -1));
this.app.getLastMsgFromServer().setMessageId(-1);
}
Gson gson = new GsonBuilder()
.registerTypeAdapter(DateTime.class, new DateTimeTypeConverter())
.create();
ServerHandler serverHandler = new ServerHandler();
String jsonString = gson.toJson(this.app.getLastMsgFromServer());
// Sending player to servlet in server
String resultString = serverHandler.getResultFromServlet(jsonString, "GetListOfMessages");
if (resultString.contains("Error")) {
return;
}
// Parsing answer
JSONObject json = new JSONObject(resultString);
Status status = null;
String statusString = json.getString("status");
if (statusString == null || statusString.length() == 0)
return;
status = Status.valueOf(statusString);
if (Status.SUCCESS.equals(status)) {
ArrayList<Message> tempChat = null;
JSONArray jsonList = json.getJSONArray("data");
MyJsonParser jsonParser = new MyJsonParser();
tempChat = jsonParser.getListOfMessagesFromJson(jsonList.toString());
if (tempChat != null && tempChat.size() != 0) {
// After getting the chat from the server, it saves the last msg
// For next syncing with the server
this.app.setLastMsgFromServer(tempChat.get(LAST_MSG_INDEX));
tempChat.addAll(this.app.getChat());
if (tempChat.size() > SIZE_OF_USER_CHAT) {
tempChat = (ArrayList<Message>) tempChat.subList(0, SIZE_OF_USER_CHAT - 1);
}
this.app.setChat(tempChat);
this.app.updateViews(null);
}
}
return;
Is the Service local only (I'm going to assume "yes")?
Communication with a local-only service can be done by passing an instance of android.os.Binder back, as shown below:
public class UpdateChatService extends Service {
public static final class UpdateChat extends Binder {
UpdateChatService mInstance;
UpdateChat(UpdateChatService instance) {
mInstance = instance;
}
public static UpdateChat asUpdateChat(IBinder binder) {
if (binder instanceof UpdateChat) {
return (UpdateChat) binder;
}
return null;
}
public String pollMessage() {
// Takes a message from the list or returns null
// if the list is empty.
return mInstance.mMessages.poll();
}
public void registerDataSetObserver(DataSetObserver observer) {
mInstance.mObservable.registerObserver(observer);
}
public void unregisterDataSetObserver(DataSetObserver observer) {
mInstance.mObservable.unregisterObserver(observer);
}
}
private ScheduledExecutorService mScheduler;
private LinkedList<String> mMessages;
private DataSetObservable mObservable;
#Override
public IBinder onBind(Intent intent) {
return new UpdateChat(this);
}
#Override
public void onCreate() {
super.onCreate();
mObservable = new DataSetObservable();
mMessages = new LinkedList<String>();
mScheduler = Executors.newScheduledThreadPool(3);
mScheduler.scheduleAtFixedRate(new UpdateChatTask(), 0, 1, TimeUnit.SECONDS);
}
#Override
public void onDestroy() {
super.onDestroy();
mScheduler.shutdownNow();
mObservable.notifyInvalidated();
}
class UpdateChatTask implements Runnable {
int mN = 0;
public void run() {
// This example uses a list to keep all received messages, your requirements may vary.
mMessages.add("Message #" + (++mN));
mObservable.notifyChanged();
}
}
}
This example could be used to feed an Activity (in this case a ListActivity) like this:
public class ChattrActivity extends ListActivity implements ServiceConnection {
LinkedList<String> mMessages;
ArrayAdapter<String> mAdapter;
UpdateChat mUpdateChat;
DataSetObserver mObserver;
Runnable mNotify;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mMessages = new LinkedList<String>();
mNotify = new Runnable() {
public void run() {
mAdapter.notifyDataSetChanged();
}
};
mAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, mMessages);
getListView().setAdapter(mAdapter);
// Bind to the Service if you do not need it to persist when this Activity
// dies - otherwise you must call #startService(..) before!
bindService(new Intent(this, UpdateChatService.class), this, BIND_AUTO_CREATE);
}
/**
* #see android.app.ListActivity#onDestroy()
*/
#Override
protected void onDestroy() {
super.onDestroy();
if (mUpdateChat != null) {
mUpdateChat.unregisterDataSetObserver(mObserver);
unbindService(this);
}
}
public void onServiceConnected(ComponentName name, IBinder service) {
mUpdateChat = UpdateChat.asUpdateChat(service);
mObserver = new DataSetObserver() {
#Override
public void onChanged() {
String message;
while ((message = mUpdateChat.pollMessage()) != null) {
mMessages.add(message);
}
runOnUiThread(mNotify);
}
#Override
public void onInvalidated() {
// Service was killed - restart or handle this error somehow.
}
};
// We use a DataSetObserver to notify us when a message has been "received".
mUpdateChat.registerDataSetObserver(mObserver);
}
public void onServiceDisconnected(ComponentName name) {
mUpdateChat = null;
}
}
If you need to communicate across processes you should look into implementing an AIDL interface - but for "local" versions this pattern works just fine & doesn't involve abusing the global Application instance.
You can use a static memory shared between your service and rest of application (activities). If you do not plan to expose this service to external apps, then sharing static memory is better than serializing/deserializing data via bundles.
Bundles based approach is encouraged for components that are to be exposed to outside world. A typical app usually has just the primary activity exposed in app manifest file.
If your don't pulibc your service , the static memory and the callback function can do.
If not , you can send broadcast.

Timing issue in an Android JUnit test when observing an object modifed by another thread

My Android app should do the following:
The MainActivity launches another thread at the beginning called UdpListener which can receive UDP calls from a remote server. If it receives a packet with a content "UPDATE", the UdpListener should notify the MainActivity to do something.
(In the real app, the use case looks like this that my app listens on the remote server. If there is any new data available on the remote server, it notifies every client (app) by UDP, so the client knows that it can download the new data by using HTTP).
I tried to simulate this in an JUnit test. The test contains an inner class which mocks the MainActivity as well as it sends the UDP call to the UdpListener:
public class UdpListener extends Thread implements Subject {
private DatagramSocket serverSocket;
private DatagramPacket receivedPacket;
private boolean running = false;
private String sentence = "";
private Observer observer;
private static final String TAG = "UdpListener";
public UdpListener(Observer o) throws SocketException {
serverSocket = new DatagramSocket(9800);
setRunning(true);
observer = o;
}
#Override
public void run() {
setName(TAG);
while (isRunning()) {
byte[] receivedData = new byte[1024];
receivedPacket = new DatagramPacket(receivedData, receivedData.length);
try {
serverSocket.receive(receivedPacket);
}
catch (IOException e) {
Log.w(TAG, e.getMessage());
}
try {
sentence = new String(receivedPacket.getData(), 0, receivedPacket.getLength(), "UTF-8");
if ("UPDATE".equals(sentence)) {
notifyObserver();
}
}
catch (UnsupportedEncodingException e) {
Log.w(TAG, e.getMessage());
}
}
}
private boolean isRunning() {
return running;
}
public void setRunning(boolean running) {
this.running = running;
}
#Override
public void notifyObserver() {
observer.update();
}
}
This is the corresponding test:
#RunWith(RobolectricTestRunner.class)
public class UdpListenerTest {
private MainActivityMock mainActivityMock = new MainActivityMock();
#Before
public void setUp() throws Exception {
mainActivityMock.setUpdate(false);
}
#After
public void tearDown() throws Exception {
mainActivityMock.setUpdate(false);
}
#Test
public void canNotifyObserver() throws IOException, InterruptedException {
UdpListener udpListener = new UdpListener(mainActivityMock);
udpListener.setRunning(true);
udpListener.start();
InetAddress ipAddress = InetAddress.getByName("localhost");
DatagramSocket datagramSocket = new DatagramSocket();
DatagramPacket sendPacket = new DatagramPacket("UPDATE".getBytes(), "UPDATE".length(), ipAddress, 9800);
datagramSocket.send(sendPacket);
datagramSocket.close();
assertTrue(mainActivityMock.isUpdate());
udpListener.setRunning(false);
}
private class MainActivityMock implements Observer {
private boolean update = false;
#Override
public void update() {
update = true;
}
public boolean isUpdate() {
return update;
}
public void setUpdate(boolean update) {
this.update = update;
}
}
}
The good thing is that my concept works. But, this test doesn't. That means it only does when I stop with a breakpoint at this line datagramSocket.close(); and wait for about a second. Why this happens is clear. But how can I do that automatically? I thought about using wait() but I have to invoke notify() from the other thread for that. The same problem with CountDownLatch. I'm not sure how to solve that without changing UdpListener.
You could write a simple loop with a specified timeout.
try {
long timeout = 500; // ms
long lastTime = System.currentTimeMillis();
while(timeout > 0 && !mainActivityMock.isUpdate()) {
Thread.sleep(timeout);
timeout -= System.currentTimeMillis() - lastTime;
lastTime = System.currentTimeMillis();
}
} catch(InterruptedException e) {
} finally {
assertTrue(mainActivityMock.isUpdate());
}
By the way - you should declare your running attribute to volatile.
one solution would be to use a blocking queue with size 1 for storing your received results.
The request for isUpdate (which would take an element from the blocking queue) would block until the update package(or any other package) is put into the queue.
is case you want all your calls to be non-blocking you could use a Future for receiving your result. Future.get() would block ontil your result is received.

Categories

Resources