parse-community / Parse-SDK-Android

The Android SDK for Parse Platform
https://parseplatform.org/
Other
1.89k stars 735 forks source link

The `_Installation` object gets lost when having more than one processes #852

Open zeekhuge opened 6 years ago

zeekhuge commented 6 years ago

Version Parse SDK version : 'com.parse:parse-android:1.16.1` compileSdkVersion : 26 minSdkVersion 19

PROBLEM I have an app in which there are more than one processes. I am developing it, so I have to make changes, install and reinstall it on the AVD. I noticed that after a few installations, I was not able to grab the User object, although I had logged in already and a few other weird things ...

I dug deep and discovered that the _Installation class was completely missing from the ParseOfflineStore database file.

Went deeper and discovered that the _Installation object was there in the database at the start, and it was getting removed somehow after a few (most of the time, only 2) iterations of reinstalling it on the AVD. Further, discovered that it was because of having 2 different processes.

HOW TO REPRODUCE This test application has a main-activity and one service which is being started in a different Process (using android:process tag in the manifest).

TheApplication.java

public class TheApplication extends Application {
    private final String TAG = TheApplication.class.getSimpleName();
    @Override
    public void onCreate () {
        super.onCreate();
        Log.d(TAG, "inside onCreate");

        Parse.setLogLevel(Parse.LOG_LEVEL_VERBOSE);
        Parse.initialize(
                new Parse.Configuration.Builder(this)
                        .applicationId("xxxxxxxxxxxx")
                        .server("xxxxxxxxxxx")
                        .enableLocalDataStore()
                        .clientKey("")
                        .build()
        );
        ParseInstallation pi = ParseInstallation.getCurrentInstallation();
        pi.put("GCMSenderId","xxxxxxxx");
        pi.saveInBackground();
    }
}

MyService.java

public class MyService extends Service {
    private final String TAG = MyService.class.getSimpleName();

    public MyService () {
        Log.d(TAG, "inside constructor");
    }

    @Override
    public IBinder onBind (Intent intent) {
        // TODO: Return the communication channel to the service.
        throw new UnsupportedOperationException("Not yet implemented");
    }
}

MainActivity.java :

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate (Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Intent i = new Intent(MainActivity.this, MyService.class);
        MainActivity.this.startService(i);
    }
}

AndroidManifest.xml

Note that MyService is on a different process :test

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="xxxxxxxxxxxxxxxxxx">

    <application
            android:name=".TheApplication"
            android:allowBackup="true"
            android:icon="@mipmap/ic_launcher"
            android:label="@string/app_name"
            android:roundIcon="@mipmap/ic_launcher_round"
            android:supportsRtl="true"
            android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>

                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
        <service
                android:name=".MyService"
                android:enabled="true"
                android:process=":test"
                android:exported="true">
        </service>
    </application>
</manifest>

testScript.sh (extra fun)

LOOP=1
COUNT=1

APK="/path/to/project-debug.apk"
PKG="project.package.name"
ACTIVITY="MainActivity"

adb shell pm uninstall ${PKG}
while [[ $LOOP -ne 0 ]]; do
    echo -e ">> Attempt ${COUNT}"
    adb logcat -d > /tmp/last_log
    adb logcat -c 
    adb push  ${APK} /data/local/tmp/${PKG}
    adb shell pm install -t -r "/data/local/tmp/${PKG}"
    sleep 2
    adb shell am start -n "${PKG}/${PKG}.${ACTIVITY}" -a android.intent.action.MAIN -c android.intent.category.LAUNCHER
    sleep 5
    adb pull "/data/data/${PKG}/databases/ParseOfflineStore" /tmp/ParseOfflineStore
    CLASSES=$(sqlite3 /tmp/ParseOfflineStore "SELECT className FROM ParseObjects")
    LOOP=$(echo ${CLASSES} | grep "_Installation" | wc -l)
    COUNT=$(echo "${COUNT} + 1" | bc)
done

The script simply re-installs the app, starts it, waits for a few seconds, fetches the ParseOfflineStore file, checks if it has the _Installation class and loops over these steps till the _Installation object is there.

Compile the debug app and run the testScript.sh with right parameters. After a few iterations the script will stop which would mean that the _Installation object disappeared from the ParseOfflineStore

zeekhuge commented 6 years ago

Interestingly enough, replacing

ParseInstallation pi = ParseInstallation.getCurrentInstallation();
pi.put("GCMSenderId","xxxxxxxx");
pi.saveInBackground();

with ParseInstallation.getCurrentInstallation().saveInBackground(); kind of solves the problem.

I'd still consider it as a bug.

zeekhuge commented 6 years ago

I can work on a PR, with some guidance on what is happening and where to look for the bug in the SDK code.

zeekhuge commented 6 years ago

Obviously enough, the workaround would be something like this :

TheApplication.java

public class TheApplication extends Application {
private final String TAG = TheApplication.class.getSimpleName();
@Override
public void onCreate () {
    super.onCreate();
    Log.d(TAG, "inside onCreate");

    Parse.setLogLevel(Parse.LOG_LEVEL_VERBOSE);
    Parse.initialize(
            new Parse.Configuration.Builder(this)
                    .applicationId("xxxxxxxxxxxxxxxxxxxx")
                    .server("xxxxxxxxxxxxxxxxx")
                    .enableLocalDataStore()
                    .clientKey("")
                    .build()
    );

    ActivityManager am  = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
    int myPid = android.os.Process.myPid();
    String myProcessName = "";
    for (ActivityManager.RunningAppProcessInfo ri : am.getRunningAppProcesses()) {
        if (ri.pid == myPid) {
            myProcessName = ri.processName;
            break;
        }
    }

    if (!myProcessName.equals(getPackageName() + getString(R.string.test_process))) {
        Log.d(TAG, "Setting up parseInstallation for process " + myProcessName);
        ParseInstallation pi = ParseInstallation.getCurrentInstallation();
        pi.put("GCMSenderId","xxxxxx");
        pi.saveInBackground();
    }
}