androidandroid-parser

How to use interface to communicate between activities after process death?


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.


Solution

  • 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.