3

I am building an SDK and need to implement callbacks between activities, without actually finish an activity. I previously used onActivityResult to provide results back to caller activity. However, this closes activity and I need to deliver callback, without finishing activity from SDK. My current implementation:

fun initializeSDK(){
    SDK.getInstance().initialize(resultsCallbackImpl)
}
val resultsCallbackImpl:ResultsCallback = object : ResultsCallback {
    override fun response1() {
        
    }

    override fun response2() {
        
    }
};

For example, the client calls initializeSDK() from his activity after the button click. Then the client passes interface as parameter, which is set as a property in SDK singleton. Then I use that interface to return results.

The problem occurs after process death. The interface becomes null, because it is not serialized and I can't return callback to client anymore. How should I edit my code to tackle this issue? Is it even possible?

I know that client can initialize SDK in the application class, then it will be re-set after process death. However, such an approach will result in difficulty for the client to communicate results back to activity from application class.

iknow
  • 8,358
  • 12
  • 41
  • 68
Viktor Vostrikov
  • 1,322
  • 3
  • 19
  • 36

4 Answers4

1

Update:

Do a right click on the project tree and add a new AIDL file called IMyAidlInterface.aidl:

package com.test.aidlsample;

import com.test.aidlsample.MyData;

interface IMyAidlInterface {
    List<MyData> getData(long id);
}

If you need to return objects to your client you need to declare and define them as parcelable and import them in aidl file too, here is the MyData.aidl that should be beside the other aidl file:

package com.test.aidlsample;

// Declare MyData so AIDL can find it and knows that it implements
// the parcelable protocol.
parcelable MyData;

and this is MyData.java in the java folder:

public class MyData implements Parcelable {
    private long productId;
    private String productName;
    private long productValue;

    public MyData(long productId, String productName, long productValue) {
        this.productId = productId;
        this.productName = productName;
        this.productValue = productValue;
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeLong(this.productId);
        dest.writeString(this.productName);
        dest.writeLong(this.productValue);
    }

    protected MyData(Parcel in) {
        this.productId = in.readLong();
        this.productName = in.readString();
        this.productValue = in.readLong();
    }

    public static final Parcelable.Creator<MyData> CREATOR = new Parcelable.Creator<MyData>() {
        @Override
        public MyData createFromParcel(Parcel source) {
            return new MyData(source);
        }

        @Override
        public MyData[] newArray(int size) {
            return new MyData[size];
        }
    };
}

Now build the project so Stub class gets built. After a successful build continue with the service:

public class SdkService extends Service {

    private IMyAidlInterface.Stub binder = new IMyAidlInterface.Stub() {
        @Override
        public List<MyData> getData(long id) throws RemoteException {
            //TODO: get data from db by id;
            List<MyData> data = new ArrayList<>();
            MyData aData = new MyData(1L, "productName", 100L);
            data.add(aData);
            return data;
        }
    };

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return binder;
    }
}

and add the service to the sdk manifest. If you are adding sdk as a dependency to the client like: implementation project(':sdk') you don't need to add AIDL files to client. If not, you have to add them and build the client application. Now, only remains to implement the client activity:

public class MainActivity extends AppCompatActivity {

    IMyAidlInterface mService;

    /**
     * Class for interacting with the main interface of the service.
     */
    private ServiceConnection mConnection = new ServiceConnection() {
        public void onServiceConnected(ComponentName className,
                                       IBinder service) {
            // This is called when the connection with the service has been
            // established, giving us the service object we can use to
            // interact with the service.  We are communicating with our
            // service through an IDL interface, so get a client-side
            // representation of that from the raw service object.
            mService = IMyAidlInterface.Stub.asInterface(service);

            try {
                List<MyData> data = mService.getData(1L);
                updateUi(data);
            } catch (RemoteException e) {
                // In this case the service has crashed before we could even
                // do anything with it; we can count on soon being
                // disconnected (and then reconnected if it can be restarted)
                // so there is no need to do anything here.
            }

        }

        public void onServiceDisconnected(ComponentName className) {
            // This is called when the connection with the service has been
            // unexpectedly disconnected -- that is, its process crashed.
            mService = null;
        }
    };

    private void updateUi(List<MyData> data) {
        //TODO: Update UI here
    }

    @Override
    protected void onResume() {
        if (mService == null) {
            Intent serviceIntent = new Intent();
            
            //CAREFUL: serviceIntent.setComponent(new ComponentName("your.client.package", "your.sdk.service.path"));
            serviceIntent.setComponent(new ComponentName("com.test.sampleclient", "com.test.aidlsample.SdkService"));
            bindService(serviceIntent, mConnection, Context.BIND_AUTO_CREATE);
        } else {
            try {
                updateUi(mService.getData(1L));
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
        super.onResume();
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

every time your client activity gets visibility, it gets data from sdk service. Just build your logic over this template. In sdk activity save data to a database and in service query them from database. I've used simple parameters in this sample.

I assumed your sdk is a library in the client app. If not, you need to do some small modifications maybe. And as I mentioned before you can find more details here: Android Interface Definition Language (AIDL). There are lots of samples and even more Q/A here in the SO on the subject. Good luck.

Original: You need to get callbacks from an activity that is currently invisible since your SDK activity is in front, right? To do that you can create a database for your SDK, persist data to your database and get data via an AIDL in the starting activity:

SdkService sdkService;
CallbackData callbackData

private ServiceConnection mConnection = new ServiceConnection() {
    // Called when the connection with the service is established
    public void onServiceConnected(ComponentName className, IBinder service) {
        sdkService = SdkService.Stub.asInterface(service);
    }

    // Called when the connection with the service disconnects unexpectedly
    public void onServiceDisconnected(ComponentName className) {
        Log.e(TAG, "Service has unexpectedly disconnected");
        sdkService = null;
    }
};

in onCreate:

Intent i = new Intent()
i.setClassName("your.sdk.packageName", "your.sdk.service.path.and.name");
bindService(i, mConnection, Context.BIND_AUTO_CREATE);

and in whenever needed:

if(sdkService != null){
    callbackData = sdkService.getCallbacks();
    updateUI();
}

Just be careful getting a binder is an async job so if you call bindService and right after call sdkService.getCallbackData you get a NullPointerException. So you might want to move getCallbacks and updateUI inside the onServiceConnected and call bindService in onResume so every time activity becomes visible you would check if there is CallbackData so you can update your UI or whatever.

Sina
  • 2,683
  • 1
  • 13
  • 25
  • Yes, I need exactly that, quote: "You need to get callbacks from an activity that is currently invisible since your SDK activity is in front, right". Thanks for your approach I haven't known this. Will this approach work even if process death occurs? Will my restored activity be able to communicate with client's activity after that? – Viktor Vostrikov Jul 13 '20 at 07:53
  • Yes, because after you return to the client activity, it gets recreated again. It gets a binder to the SDKService and will get the data from sdk database. Using AIDLs is a standard solution for communication in developing sdks. BTW, what you are mentioning as the process death is probably a scenario that client activity is destroyed by system but process is not killed since your sdk activity is alive and showing. – Sina Jul 13 '20 at 08:42
  • Yes, this is the scenario what I call process death. You said that client's activity gets recreated after I return to it. However, I ned somehow to recreate client activity without returning to it. Because, after process death occurs activity from SDK is present and client's activity is destroyed by the system without using AIDL. So you are saying that if I implement AIDLs, then client's activity will also be recreated automatically, even though activity which is precent to user is actually the one from SDK? – Viktor Vostrikov Jul 13 '20 at 10:19
  • No I mean if your client is not visible you should not really care about it. When sdk activity creates a data it saves it in the database. At a time, the client activity needs to become visible. Either it has been destroyed or not you call getBackupData and get the required data from the SDK database and make required changes in the client activity. – Sina Jul 13 '20 at 10:37
  • Thanks, just one more question, before accepting this answer. So by calling getBackupData I will reactivate the client ,without finish SDK's activity, and client will be able to get results even if it was previously destroyed yes? – Viktor Vostrikov Jul 14 '20 at 07:40
  • Sure. It gets whatever you wish from SDK to the client activity. You bind to the sdk service and call getData, and in sdk service you get data from database and return it. – Sina Jul 14 '20 at 07:56
  • Have you been successful with the AIDL implementation? – Sina Jul 16 '20 at 12:34
  • I have looked at it and got confused if I should rather use Messenger for communication. I don't need concurrency benefits using AIDL. Also, I could not find a decent example for Messenger communication between two activities. All examples were structured in a way that the activity instantiates service and receive callbacks from it. The issue in my case is that there are two activities: client's and our SDK activity. – Viktor Vostrikov Jul 17 '20 at 07:18
  • 1
    I know IPC via AIDL seems to be complicated, but it is quite simple, standard and easy to use. I try to update my answer with a complete sample. – Sina Jul 17 '20 at 07:28
  • Thanks, I awarded bounty to you! I am still working on finishing it in our project, but so far it seems promising! I will keep you updated – Viktor Vostrikov Jul 19 '20 at 14:19
  • I wish you good luck and looking forward to hearing form it. – Sina Jul 19 '20 at 14:36
0

You cannot use interfaces directly to communicate between activities.

As soon as you start a new activity and new activity becomes visible android OS can kill 1st activity anytime (you can try this with a flag inside developer option "Don't keep activities"). So user of your SDK will complain about certain random "null pointer exception".

So, Now if you want to share data between current and previous screen, you might have to rethought your solution using Fragments. Exposing your UI using a fragment and communicating back your result to activity which then would update proper fragment which needs the data.

I faced similar issue in one existing app which I was asked to fix. I switched entire app to fragments and single activity, first to release a hot fix.

Sandeep Dhull
  • 1,638
  • 2
  • 18
  • 30
  • Thanks for good overview! Yes I know about process death aka "Don't keep activities" and I just wanted to reassure myself if I my thinking is correct. I fought about fragments, but that mean that our SDK would not have even a single activity at all. It would be all fragments. So client then could initialise our SDK and add fragment. Would that be work correctly with "Don't keep activities"? However, if process death occurs fragment will be restored automatically by the system, but interface would still be null – Viktor Vostrikov Jul 12 '20 at 17:27
  • The problem statement is that you need to send some kind of events to parent continuously. That means Parent has to be alive. I can think of handling such situation with fragments only. Now if you want to handle death, then you need to properly save state of fragments and custom ui components if you have any. Proper handling of lifecycle callbacks is important in your case. – Sandeep Dhull Jul 12 '20 at 19:32
0

The problem occurs after process death. The interface becomes null, because it is not serialised and I can't return callback to client anymore. How should I edit my code to tackle this issue? Is it even possible?

This is not possible. If the client process dies, all of its executing code - including your SDK - gets wiped away.

I know that client can initialise SDK in the application class, then it will be re-set after process death. However, such approach will result in difficulty for client to communicate results back to activity from application class.

So what? If the client Activity is restarted, it should call the SDK again to set a new callback instance which you can use from that point forward.

dominicoder
  • 9,338
  • 1
  • 26
  • 32
  • The issue is that client has no way of determining if process death occurred and an activity from SDK has been restarted. How can client activity call SDK again and set a new callback after process death occurred? I can't find any way in which client can restart SDK initialisation, because it is not client activity, which becomes presented after process death it is the last visible activity, which is always the one from the SDK... :( – Viktor Vostrikov Jul 13 '20 at 04:47
  • Oh, so it's your Activity that is on top? In that case, why do you need to communicate back to the client activity? There's no guarantee that activity will exist if yours is in the foreground. Please clarify what you're trying to achieve. – dominicoder Jul 13 '20 at 04:57
  • Yes, my activity is on top. I wanted to hear other people opinions about this question. I also was hesitant to use this approach and wanted to get feedback if it is even possible to communicate to the client activity after process death occurred. By looking at yours and other question I feel that it is not possible to do that. Thanks! – Viktor Vostrikov Jul 13 '20 at 07:51
0

You can use a sharedviewmodel that is bound to both activities; have a mutablelivedata variable that you can observe from the two activities.

ideally on the first activity you can just put the value inside the mutablelivedata variable. Then on the second activity get the activity.

Follow the following link to give you a guideline.

ViewModel Overview

David Innocent
  • 606
  • 5
  • 16