Reputation: 11164
This is quite a conundrum:
I open my app. It launches an activity which acts as a splashscreen (ASplashscreen
) in which I load some JSON
data from local storage (raw
folder) and store it in memory in a singleton object
(static). After this process is done it automatically moves along to the main activity (AMain
)
I exit the app by pressing the home button
and run other applications, games, etc. When I reopen my app, the app crashes inside the onCreate
method of the AMain
because it tries to use some of the data inside the singleton object
but the data is null
. So it throws a NullPointerException
when it does so.
It appears that it restarts the AMain
instead of ASplashscreen
so the singleton
doesn't have a chance to reinitialize.
This happens randomly across multiple such tries...
I have two presumptions ...
My first presumption, and from what I know about the Android OS, is that while I was running those other applications (especially the games) one of them required a lot of memory so the OS released my app from memory to make room, so the singleton data
was garbage collected
.
I also presume that while the gc
removed my singleton from memory, the OS still kept some data relating to the "state" of the current running activity, so it knew at least that it had the AMain
activity opened before i closed the app. This would explain why it reopened the AMain
activity instead of the ASplashscreen
.
Am I right? Or is there another explanation why I get this exception? Any suggestions/clarifications are welcomed.
Also, what would be the best approach to handle this? My approach is to check the existence of he singleton data whenever I try to use it and if it's null then just basically restart the app. This makes it go through the ASplashscreen
so the JSON
gets initialized and everything is ok.
EDIT As requested, here's my AndroidManifest
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="com.android.vending.BILLING"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<application
android:name=".global.App"
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:largeHeap="true"
android:theme="@style/AppTheme">
<!--SPLASH SCREEN-->
<activity
android:name=".activities.ASplashscreen"
android:label="@string/app_name"
android:launchMode="singleTask"
android:screenOrientation="portrait"
android:theme="@style/AppTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!--MAIN-->
<activity
android:name=".activities.AMain"
android:label="@string/app_name"
android:launchMode="singleTask"
android:screenOrientation="portrait"
android:theme="@style/AppTheme"/>
<!--MENU-->
<activity
android:name=".activities.AMenu"
android:label="@string/app_name"
android:launchMode="singleTask"
android:screenOrientation="portrait"
android:theme="@style/AppTheme"/>
<!--HELP-->
<activity
android:name=".activities.AHelp"
android:label="@string/app_name"
android:launchMode="singleTask"
android:screenOrientation="portrait"
android:theme="@style/AppTheme"/>
<!--ADMOB-->
<activity
android:name="com.google.android.gms.ads.AdActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|uiMode|screenSize|smallestScreenSize"
android:theme="@android:style/Theme.Translucent"/>
<!--FACEBOOK LOGIN ACTIVITY (SDK)-->
<activity
android:name="com.facebook.LoginActivity"
android:label="@string/app_name"
android:screenOrientation="portrait"
android:theme="@style/AppTheme"/>
<!--This meta-data tag is required to use Google Play Services.-->
<meta-data
android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version"/>
<!--FACEBOOK STUFF-->
<meta-data
android:name="com.facebook.sdk.ApplicationId"
android:value="@string/facebook_app_id"/>
<!--GOOGLE PLUS-->
<meta-data
android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version"/>
<!--CRASHLYTICS-->
<meta-data
android:name="com.crashlytics.ApiKey"
android:value="9249....."/>
</application>
If you guys really want it, here's the content of the ASplashscreen
/**
* @author MAB
*/
public class ASplashscreen extends ABase implements IIosLikeDialogListener {
private final float SHEEP_WIDTH_FRAC = 0.8f;
private final int SPLASHSCREEN_DELAY_MS = 500;
//View references
private View sheep_image;
/** The timestamp recorded when this screen came into view. We'll used this to determine how much we'll need to keep the splash screen awake */
private long mStartTimestamp;
private IosLikeDialog mDialog;
private IabHelper mIabHelper;
// Listener that's called when we finish querying the items and subscriptions we own
IabHelper.QueryInventoryFinishedListener mGotInventoryListener = new IabHelper.QueryInventoryFinishedListener() {
public void onQueryInventoryFinished(IabResult result, Inventory inventory) {
// Have we been disposed of in the meantime? If so, quit.
if (mIabHelper == null) {
System.out.println("=== IAB INVENTORY PROBLEM :: WE'VE BEEN DISPOSED");
displayAppStoreUnavailableDialog();
return;
}
// Is it a failure?
if (result.isFailure()) {
displayAppStoreUnavailableDialog();
System.out.println("=== IAB INVENTORY PROBLEM :: FAILED TO QUERY INVENTORY :: " + result);
return;
}
//Sync our static stuff with the app store
HSounds.instance().populate(ASplashscreen.this, inventory);
HLights.instance().populate(ASplashscreen.this, inventory);
//Store the stuff locally just to be sure
HStorage.persistObjectToFile(ASplashscreen.this, HVersions.SOUNDS);
HStorage.persistObjectToFile(ASplashscreen.this, HVersions.LIGHTS);
System.out.println("=== SUCCESSFULLY SYNCED WITH STORE !");
jumpToMainActivity();
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.a_splashscreen);
init();
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mIabHelper != null) {
mIabHelper.dispose();
}
mIabHelper = null;
}
@Override
public void onIosLikeDialogBtnsClick(int btnStringResID) {
if (btnStringResID == IosLikeDialog.BTN_OK) {
jumpToMainActivity();
}
}
private void init() {
//Get view references
sheep_image = findViewById(R.id.splashscreen_sheep);
mStartTimestamp = System.currentTimeMillis();
VersionTracking.setVersions(this);
//Set the width of the sheep
RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) sheep_image.getLayoutParams();
params.width = (int) ((float) UScreen.getScreenWidthInPortrait(this) * SHEEP_WIDTH_FRAC);
sheep_image.setLayoutParams(params);
mDialog = new IosLikeDialog()
.with(findViewById(R.id.ios_like_dialog_main_container))
.listen(this);
new Thread(new Runnable() {
@Override
public void run() {
parseJsons();
//Get the filler bar values from shared prefs
HBrightness.instance().retrieveFromPersist(ASplashscreen.this);
HSensorAndTimer.instance().retrieveFromPersist(ASplashscreen.this);
WsBuilder.build(ASplashscreen.this).getGift(new ResponseListener<EGift>() {
@Override
public void onSuccess(EGift gifts) {
long now = System.currentTimeMillis();
SimpleDateFormat fmt = new SimpleDateFormat(HJsonDataBase.GIFT_DATE_FORMAT);
Date start;
Date end;
//Handle the gifts
if (gifts != null && gifts.data != null && gifts.responseOK()) {
//Go through the SOUNDS and check if we need to set them as gifts, if not reset them
for (ESound sound : HSounds.instance().getValues().getSounds()) {
String sku = sound.getSku(ASplashscreen.this);
sound.giftStart = null;
sound.giftEnd = null;
for (String giftSku : gifts.data.inapps) {
if (giftSku.equals(sku)) {
sound.giftStart = gifts.data.start_date;
sound.giftEnd = gifts.data.end_date;
break;
}
}
//Check if redeemed gift expired and if so, reset the dates
checkSoundGiftExpired(sound, fmt, now);
}
//Go through the LIGHTS and check if we need to set them as gifts, if not reset them
for (ELight light : HLights.instance().getValues().getLights()) {
String sku = light.getSku(ASplashscreen.this);
light.giftStart = null;
light.giftEnd = null;
for (String giftSku : gifts.data.inapps) {
if (giftSku.equals(sku)) {
light.giftStart = gifts.data.start_date;
light.giftEnd = gifts.data.end_date;
break;
}
}
//Check if redeemed gift expired and if so, reset the dates
checkLightGiftExpired(light, fmt, now);
}
//Persist the data in the local storage
HStorage.persistObjectToFile(ASplashscreen.this, HVersions.SOUNDS);
HStorage.persistObjectToFile(ASplashscreen.this, HVersions.LIGHTS);
}
//Run the IAB helper now
runIabHelper();
}
@Override
public void onErrorResponse(VolleyError error) {
//This might mean we're in offline mode, so check if the gifts expired
checkAllLightsGiftExpired();
checkAllSoundsGiftExpired();
//Run the IAB helper now
runIabHelper();
}
}, getPackageName());
}
});
}
/**
* This is run on a non-UI thread !!
*/
private void parseJsons() {
/**
* Versions
*/
parseVersions();
/**
* BACKGROUND
*/
parseBackgrounds();
try {
validateBackgrounds();
} catch (NullPointerException e) {
removeBackgroundsFile();
parseBackgrounds();
}
/**
* LIGHTS
*/
parseLights();
try {
validateLights();
} catch (NullPointerException e) {
removeLightsFile();
parseLights();
}
/**
* SOUNDS
*/
parseSounds();
try {
validateSounds();
} catch (NullPointerException e) {
removeSoundsFile();
parseSounds();
}
}
private void parseVersions() {
InputStream in = getResources().openRawResource(R.raw.versions);
EVersions versions = null;
try {
versions = UGson.jsonToObject(in, EVersions.class);
} catch (Exception e) {
System.out.println("==== PARSE ERROR :: VERSIONS :: " + e.getMessage());
e.printStackTrace();
return;
}
HVersions.instance().setValues(this, versions);
}
private void parseBackgrounds() {
//Get the version of he JSONS at which we've last updated them from the "raw" folder
int lastVersionBckgnds = UPersistent.getInt(ASplashscreen.this, HVersions.SHARED_PREF_LAST_JSONS_VERSION_BCKGNDS, 0);
InputStream in;
//If there are no files in local storage OR there's a new version of the JSON files that we need to retrieve
if (!HStorage.fileExists(ASplashscreen.this, HStorage.FILE_JSON_BACKGROUNDS) ||
HVersions.instance().shouldUpdateFromResources(HVersions.BACKGROUNDS, lastVersionBckgnds)) { //Update from raw folder
in = getResources().openRawResource(R.raw.backgrounds);
} else { //Update from local storage
in = HStorage.getInputStreamForFile(ASplashscreen.this, HStorage.FILE_JSON_BACKGROUNDS);
}
EBackgrounds bckgnds = null;
try {
bckgnds = UGson.jsonToObject(in, EBackgrounds.class);
} catch (Exception e) {
System.out.println("==== PARSE ERROR :: BACKGROUNDS :: " + e.getMessage());
e.printStackTrace();
}
HBackgrounds.instance().setValues(this, bckgnds);
}
private void parseLights() {
//Get the version of he JSONS at which we've last updated them from the "raw" folder
int lastVersionLights = UPersistent.getInt(ASplashscreen.this, HVersions.SHARED_PREF_LAST_JSONS_VERSION_LIGHTS, 0);
InputStream in;
//If there are no files in local storage OR there's a new version of the JSON files that we need to retrieve
if (!HStorage.fileExists(ASplashscreen.this, HStorage.FILE_JSON_LIGHTS) ||
HVersions.instance().shouldUpdateFromResources(HVersions.LIGHTS, lastVersionLights)) { //Update from raw folder
in = getResources().openRawResource(R.raw.lights);
} else { //Update from local storage
in = HStorage.getInputStreamForFile(ASplashscreen.this, HStorage.FILE_JSON_LIGHTS);
}
ELights lights = null;
try {
lights = UGson.jsonToObject(in, ELights.class);
} catch (Exception e) {
System.out.println("==== PARSE ERROR :: LIGHTS :: " + e.getMessage());
e.printStackTrace();
}
if (lights != null) {
HLights.instance().setValues(this, lights);
}
}
private void parseSounds() {
int lastVersionSounds = UPersistent.getInt(ASplashscreen.this, HVersions.SHARED_PREF_LAST_JSONS_VERSION_SOUNDS, 0);
InputStream in;
//If there are no files in local storage OR there's a new version of the JSON files that we need to retrieve
if (!HStorage.fileExists(ASplashscreen.this, HStorage.FILE_JSON_SOUNDS) ||
HVersions.instance().shouldUpdateFromResources(HVersions.SOUNDS, lastVersionSounds)) { //Update from raw folder
in = getResources().openRawResource(R.raw.sounds);
} else { //Update from local storage
in = HStorage.getInputStreamForFile(ASplashscreen.this, HStorage.FILE_JSON_SOUNDS);
}
ESounds sounds = null;
try {
sounds = UGson.jsonToObject(in, ESounds.class);
} catch (Exception e) {
System.out.println("==== PARSE ERROR :: SOUNDS" + e.getMessage());
}
if (sounds != null) {
HSounds.instance().setValues(this, sounds);
}
}
private void validateBackgrounds() throws NullPointerException {
if (HBackgrounds.instance().getValues() == null) {
throw new NullPointerException();
}
if (HBackgrounds.instance().getValues().getBackgrounds() == null) {
throw new NullPointerException();
}
}
private void validateLights() throws NullPointerException {
if (HLights.instance().getValues() == null) {
throw new NullPointerException();
}
if (HLights.instance().getValues().getLights() == null) {
throw new NullPointerException();
}
}
private void validateSounds() throws NullPointerException {
if (HSounds.instance().getValues() == null) {
throw new NullPointerException();
}
if (HSounds.instance().getValues().getSounds() == null) {
throw new NullPointerException();
}
}
private void removeBackgroundsFile() {
HStorage.deleteFile(this, HStorage.FILE_JSON_BACKGROUNDS);
}
private void removeLightsFile() {
HStorage.deleteFile(this, HStorage.FILE_JSON_LIGHTS);
}
private void removeSoundsFile() {
HStorage.deleteFile(this, HStorage.FILE_JSON_SOUNDS);
}
private void runIabHelper() {
//If there's no network connection, then ... sorry
if (!UNetwork.isNetworkAvailable(this)) {
displayAppStoreUnavailableDialog();
System.out.println("=== IAB ERROR :: NO NETWORK");
return;
}
try {
mIabHelper = new IabHelper(ASplashscreen.this, CIab.IAB_PUBLIC_KEY);
mIabHelper.startSetup(new IabHelper.OnIabSetupFinishedListener() {
@Override
public void onIabSetupFinished(IabResult result) {
if (!result.isSuccess()) {
// Oh noes, there was a problem.
System.out.println("=== IAB ERROR :: CONNECTION :: " + result);
displayAppStoreUnavailableDialog();
return;
}
//Obtain and create the list of skus from both the LIGHTS and the SOUNDS handlers
List<String> skus = new ArrayList<String>();
skus.addAll(HSounds.instance().createSkuList(ASplashscreen.this, true));
skus.addAll(HLights.instance().createSkuList(ASplashscreen.this, true));
//Get the inventory
try {
mIabHelper.queryInventoryAsync(true, skus, mGotInventoryListener, new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread thread, Throwable ex) {
// Crashlytics.logException(ex);
System.out.println("=== IAB ERROR :: query inventory crashed :: " + ex.getMessage());
displayAppStoreUnavailableDialog();
}
});
} catch (IllegalStateException e) {
displayAppStoreUnavailableDialog();
}
}
});
} catch (NullPointerException e1) {
// Crashlytics.logException(e1);
System.out.println("=== IAB ERROR :: query inventory crashed :: " + e1.getMessage());
displayAppStoreUnavailableDialog();
} catch (IllegalArgumentException e2) {
// Crashlytics.logException(e2);
System.out.println("=== IAB ERROR :: query inventory crashed :: " + e2.getMessage());
displayAppStoreUnavailableDialog();
}
}
private void displayAppStoreUnavailableDialog() {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (mDialog == null) {
return;
}
mDialog.reset()
.header(R.string.inapp_store_unavailable_header)
.subheader(R.string.inapp_store_unavailable_subheader)
.btnOK()
.show();
}
});
}
private void jumpToMainActivity() {
int timePassed = (int) (System.currentTimeMillis() - mStartTimestamp);
int delay = (timePassed > SPLASHSCREEN_DELAY_MS) ? 0 : (SPLASHSCREEN_DELAY_MS - timePassed);
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
//In case we need to display the tutorial, then do so
if (AHelp.shouldDisplayTutorial(ASplashscreen.this)) {
CrashReport.log("ASplashscreen -> AHelp");
Intent i = new Intent(ASplashscreen.this, AHelp.class);
i.putExtra(AHelp.BUNDLE_SHOW_TUTORIAL, true);
startActivity(i);
finish();
overridePendingTransition(R.anim.anim_slide_in_from_bottom, R.anim.anim_stay_put);
return;
} else { //Otherwise continue with normal flow
CrashReport.log("ASplashscreen -> AMain");
Intent i = new Intent(ASplashscreen.this, AMain.class);
i.putExtra(AMain.BUNDLE_DEBUGGING_CAME_FROM_SPLASHSCREEN, true);
startActivity(i);
finish();
}
}
}, delay);
}
private void checkAllSoundsGiftExpired() {
SimpleDateFormat fmt = new SimpleDateFormat(HJsonDataBase.GIFT_DATE_FORMAT);
long now = System.currentTimeMillis();
for (ESound sound : HSounds.instance().getValues().getSounds()) {
if (sound != null) {
checkSoundGiftExpired(sound, fmt, now);
}
}
}
private void checkAllLightsGiftExpired() {
SimpleDateFormat fmt = new SimpleDateFormat(HJsonDataBase.GIFT_DATE_FORMAT);
long now = System.currentTimeMillis();
for (ELight light : HLights.instance().getValues().getLights()) {
if (light != null) {
checkLightGiftExpired(light, fmt, now);
}
}
}
private void checkSoundGiftExpired(ESound sound, SimpleDateFormat fmt, long now) {
if (UString.stringsExist(sound.giftExpireStart, sound.giftExpireEnd)) {
try {
Date start = fmt.parse(sound.giftExpireStart);
Date end = fmt.parse(sound.giftExpireEnd);
if (now < start.getTime() || end.getTime() < now) {
sound.giftExpireStart = null;
sound.giftExpireEnd = null;
}
} catch (ParseException e) {
//Do nothin'
}
}
}
private void checkLightGiftExpired
(ELight light, SimpleDateFormat fmt, long now) {
if (UString.stringsExist(light.giftExpireStart, light.giftExpireEnd)) {
try {
Date start = fmt.parse(light.giftExpireStart);
Date end = fmt.parse(light.giftExpireEnd);
if (now < start.getTime() || end.getTime() < now) {
light.giftExpireStart = null;
light.giftExpireEnd = null;
}
} catch (ParseException e) {
//Do nothin'
}
}
}
}
Upvotes: 2
Views: 4325
Reputation: 15535
I guess your JSON data is in following format.
{
a : "A",
b : "B",
c : "C"
}
Now you can have a class called JsonData
which is structured as follows,
public class JsonData {
public String a;
public String b;
public String c;
}
Now you can convert your json data as Java object using gson library.
Now crate a class like ObjectHolder
, which is structured as follows.
public class ObjectHolder {
public static JsonData jsonData;
}
Store your converted object in ObjectHolder.jsonData
. Now you can access this object throughout project anytime.
Note: this object will becomes null
when you clear your app from "recent apps" list.
Upvotes: 2
Reputation: 95618
This is pretty much standard Android behaviour. When your app is in the background it can be killed at any time, for any reason. Android simply kills the OS process hosting your app.
When the user returns to your app (or restarts your app), Android realizes that it had killed your app previously, so it creates a new OS process to host your app, then it instantiates the Application
instance for your app, then it instantiates the topmost Activity
in the task stack (ie: the Activity
that was on screen at the time your app went into the background), then it calls onCreate()
on that Activity
so that the Activity
can restore itself. Android passes the most recently saved instaces state of the Activity
as the Bundle
parameter to onCreate()
. In this way, the Activity
has a chance to restore itself.
Your Activity
is crashing because it is relying on data that should have been set up previously. In the case where Android kills and then recreates the app's OS process, this data is gone.
There are various ways to work around this, one of which you have already used:
in onCreate()
of all activities, check if the "app initialization" has been performed by using a public static
variable or singleton. If the initialization has not been done, you know that your app's process was killed and recreated and you need to either redirect the user to your root Activity
(ie: start the app all over again) or do the initialization immediately in the onCreate()
of the Activity
.
Save the data you need in onSaveInstanceState()
and restore it in onCreate()
and/or onRestoreInstanceState()
or both.
Don't keep this data in memory. Keep it in a database or other non-memory-based persistent structure.
NOTE: In general you should not be using launchMode="singleTask"
. For most situations this is unnecessary, and usually causes more problems than it solves. This has nothing to do with the process kill/recreate problem you are having, but you should still avoid using the special launch modes singleTask
and singleInstance
. These should only be necessary if you are creating a HOME screen replacement.
Upvotes: 14
Reputation: 11597
Solution:
you should not set splash activity as singleTask
that means root of activity stack, instead your MainActivity should set singleTask
as root.
when your app back to foreground, in onCreate(...)
, you should check your static refrences of singleton class if not null before using them, if null then jump back to your splash activity (means reload and restore data into static references). In other word, if static references are recycled by system, then just restart your app.
Hope this help!
Upvotes: 3
Reputation: 2446
While using singleton there should be some getInstane
method with just return instance
, so you can place your check inside it like following:
public static SingletonClass getInstance() {
if(instance == null) {
instance = StaticMethodToLoadInstance();
}
return instance;
}
I guess you can place all your data loading code inside static StaticMethodToLoadInstance()
.
UPDATED
Yes, loading data can spend much time, so it could be made with another method. First of all, create your own interface:
public static interface OnInstanceLoadedListener {
public void onIntsanceLoaded(SingletonClass instance);
}
Then change getInstance
by following:
public static void getInstance(final OnInstanceLoadedListener listener, Activity context) {
final ProgressDialog dialog = null;
if(instance == null) {//if there should be loading
dialog = StaticMethodToCreateProgressDialog(context);
dialog.show();
}
new Thread(new Runnable() {
@Override
public void run() {
if(instance == null) {
instance = StaticMethodToLoadInstance();
}
context.runOnUiThread(new Runnable() {
@Override
public void run() {
listener.onIntsanceLoaded(instance);
if(dialog != null && dialog.isShowing())
dialog.dismiss();
}
});
}
}).start();
}
And your getInstance
usage changes from
SingletonClass object = SingletonClass.getInstance();
String data = object.getData();
to
getInstance(new OnInstanceLoadedListener() {
@Override
public void onIntsanceLoaded(SingletonClass instance) {
String data = instance.getData();
}
}, YourActivityClass.this);
In this way your data will load asynchronously. Yes, it looks much more difficult, but it can show progress dialog - user can see app is still working.
Upvotes: 7
Reputation: 6462
Well, in my opinion there are two ways to improve your current restart-app approach (your approach is ok but a little dirty):
1) Get rid of ASplashscreen
, move loading logic to some helper class and schedule loading of your data from MainActivity
while showing your splash on top of Activity
layout. If you use RelativeLayout
for your MainActivity
you may easily achieve this by adding invisible view with "match_parent"
params on the bottom of view hierarchy (to overlap other views) and by making it visible when necessary. This is cleaner compared to two Activities
and app restarting.
2) Make your singleton Parcelable
and store it in onSaveInstanceState()
in MainActivity
. (of course your singleton won't be a singleton any more with this approach). In this case Android will save your data along with MainActivity
and after restoring it in OnCreate()
everything will be in place. This is a little cleaner than just saving it to SharedPreferences
.
Upvotes: 2