2

I have an android app which has been released on the Google Play store for some time. It runs fine for 99.9% of users. However, the Google Play Console tells me that a few users are suffering from a crash caused by a NullPointerException in the onCreate routine for an activity. The particular line in question is:

final Spinner choose_music = findViewById(R.id.music_list);

The routine never seems to crash with any other call to findViewById in the routine, just the one for the spinner (and only very rarely). My layout file is listed below, although I doubt that it's relevant. I'm afraid I don't have any logcat output, since the Play Console doesn't provide that.

The onCreate routine is called from within an activity, when that activity launches. I've included the complete onCreate routine below, but the finding of the spinner is fairly early on.

Any ideas? Thanks

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical"
    android:background="@color/light_grey"
    tools:context=".PlayActivity" >

<RelativeLayout
    android:id="@+id/main_canvas"
    android:layout_weight="90"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:background="@color/colorAccent">
</RelativeLayout>

<RelativeLayout
    android:id="@+id/bottom_panel"
    android:layout_weight="90"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content">
    <RelativeLayout
        android:id="@+id/layout_choose"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignTop="@id/layout_volume"
        android:layout_alignBottom="@id/layout_volume"
        android:background="@color/mid_grey"
        android:layout_alignParentLeft="true">
        <TextView
            android:id="@+id/choose_music"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentTop="true"
            android:text="Music"
            android:padding="5sp"
            android:textSize="20sp"/>
        <Spinner
            android:id="@+id/music_list"
            android:layout_alignParentLeft="true"
            android:layout_width="wrap_content"
            android:layout_below="@id/choose_music"
            android:layout_height="wrap_content"></Spinner>
    </RelativeLayout>
    <TextView
        android:id="@+id/padding_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_toRightOf="@id/layout_choose"
        android:text="."
        android:textColor="@color/light_grey"
        android:padding="2sp"
        android:textSize="20sp"/>
    <RelativeLayout
        android:id="@+id/layout_volume"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@color/mid_grey"
        android:layout_alignParentRight="true"
        android:layout_toRightOf="@id/padding_text">
        <TextView
            android:id="@+id/choose_volume"
            android:layout_width="match_parent"
            android:gravity="center"
            android:layout_height="wrap_content"
            android:layout_alignParentTop="true"
            android:layout_alignParentRight="true"
            android:text="Relative volume"
            android:padding="0sp"
            android:textSize="20sp"/>
        <TextView
            android:id="@+id/volume_music"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_below="@id/choose_volume"
            android:layout_alignParentLeft="true"
            android:text="Music"
            android:padding="5sp"
            android:textSize="20sp"/>
        <TextView
            android:id="@+id/volume_speech"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_below="@id/choose_volume"
            android:layout_alignParentRight="true"
            android:text="Speech"
            android:padding="5sp"
            android:textSize="20sp"/>
        <android.support.v7.widget.AppCompatSeekBar
            android:id="@+id/relative_volume"
            android:padding="10sp"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_alignParentRight="true"
            android:layout_below="@id/volume_speech"/>
    </RelativeLayout>
</RelativeLayout>

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_play);
    my_view = (RelativeLayout) findViewById(R.id.main_canvas);
    DisplayMetrics displayMetrics = new DisplayMetrics();
    getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
    int height = displayMetrics.heightPixels;
    int width = displayMetrics.widthPixels;
    view = new CircularMusicController(PlayActivity.this, height, width);
    my_view.addView(view, new ActionBar.LayoutParams(
            RelativeLayout.LayoutParams.WRAP_CONTENT,
            RelativeLayout.LayoutParams.WRAP_CONTENT));
    globals.crash_log("Layout dimensions before: " + String.valueOf(my_view.getWidth() + ", "
            + String.valueOf(my_view.getHeight())));
    my_view.setLayoutParams(new LinearLayout.LayoutParams(view.get_width(), view.get_height()));

    // Get ListView object from xml
    final Spinner choose_music = findViewById(R.id.music_list);

    int music_index = 0;
    for (int i=0; i<globals.music_track_list.length; i+=1) {
        if (globals.appState.get_music().equals(globals.music_track_list[i])) {
            music_index = i;
        }
    }
    chosen_music = music_index;
    // ListView Item Click Listener
    ArrayAdapter<CharSequence> music_adapter = new ArrayAdapter<CharSequence>(this, R.layout.activity_listview, globals.music_names);
    choose_music.setAdapter(music_adapter);
    choose_music.setSelection(music_index);
    choose_music.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
        @Override
        public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
            globals.crash_log("I have chosen " + String.valueOf(i));
            if (chosen_music > 0 && musicBound && musicSrv != null) {
                musicSrv.halt();
            }
            chosen_music = i;
            if (musicBound) {
                if (i > 0) {
                    musicSrv.setSong(globals.music_locs[i]);
                    if (isPlaying()) {
                        musicSrv.playSong(isPlaying());
                    } else {
                        choice_while_paused = true;
                    }
                }
            }
        }

        @Override
        public void onNothingSelected(AdapterView<?> adapterView) {
        }
    });

    globals.crash_log("Layout dimensions: " + String.valueOf(my_view.getWidth() + ", "
            + String.valueOf(my_view.getHeight())));

    volume_control = findViewById(R.id.relative_volume);
    volume_control.setProgress(globals.appState.get_volume());
    volume_control.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
        @Override
        public void onProgressChanged(SeekBar seekBar, int volume, boolean from_user) {
            if (from_user) {
                int focus_vol = globals.roc_function(volume);
                try {
                    if (speechSrv != null) {
                        speechSrv.set_volume(focus_vol);
                    }
                    if (musicSrv != null) {
                        musicSrv.set_volume(100 - focus_vol);
                    }
                } catch (Exception err) {
                    globals.crash_log("Failed to set volume");
                    globals.crash_exception(err);
                }
            }
            globals.crash_log("SeekBar: new volume is " + String.valueOf(volume) + "   " +
                    String.valueOf(from_user));
        }

        @Override
        public void onStartTrackingTouch(SeekBar seekBar) {

        }

        @Override
        public void onStopTrackingTouch(SeekBar seekBar) {

        }
    });

    // Get any information passed down from the parent activity
    Intent intent = getIntent();
    track_name = intent.getStringExtra("name");
    track_location = intent.getStringExtra("location");
    res_location = intent.getIntExtra("res_location", 0);
    activity_name = intent.getStringExtra("activity_name");
    globals.crash_log("PlayActivity: onCreate: activity_name is " + activity_name);
    globals.crash_log("PlayActivity: onCreate: track_name is " + track_name);
    if ( ! (track_location == null) ) {
        globals.crash_log("PlayActivity: onCreate: track_loc is " + track_location);
    }
    globals.crash_log("PlayActivity: onCreate: res_location is "+String.valueOf(res_location));

    // Setup the action bar, and start the broadcast receiver.
    ActionBar action_bar = getSupportActionBar();
    action_bar.setTitle(track_name);
    action_bar.setDisplayHomeAsUpEnabled(true);

    IntentFilter filter = new IntentFilter("PLAY_ACTIVITY");
    this.registerReceiver(_refreshReceiver, filter);
}

2 Answers2

1

onCreate() is where you want to inflate views and/or initialize other objects. I would recommend doing this:

  1. Create a class variable for choose_music
  2. initialize the reference to the variable in onCreate() with findViewById()
  3. Move the remainder of the code into onStart() - this ensures that the views are all inflated, because I believe it's possible that the views are still rendering while in the onCreate() lifecycle and that's why some users are getting NPE. Also, using findViewById() is nullable, so I would still wrap all logic with some conditional checks to ensure it is safe to call.

Alternative solution, use DataBinding as that is null-safe.

zuko
  • 527
  • 4
  • 9
  • Thanks, that makes a lot of sense. It could be that some users phones are being laggy, and hence I get a null return from ```findViewById```. I'll try this out in my next update, and hopefully the problem will go. DataBinding looks really interesting too. – Neill Bowler Nov 16 '19 at 14:14
0

Crashing with NPE on that line does not make sense. If findViewById() was in a NPE-worthy state (like calling the method before the activity was attached to a Window), it would have NPE'd already many lines above it.

I suspect the line number reporting is slightly off (it happens) and it is in fact your globals.music_track_list that is null in some scenarios, and attempting to read its length is the NPE here.

I also encourage you to use a more versatile crash reporting service than Play console. For example, Crashlytics. One of the issues with Play console is that exception messages and nested exceptions are not reported.

laalto
  • 137,703
  • 64
  • 254
  • 280
  • Thanks for the suggestions. It would be pretty weird for ```globals.music_track_list```, since ```globals``` is a singleton class created when the app is started (in the main activity) and ```music_track_list``` is static: ```public static String[] music_track_list = new String[] {"none", "pulse", "dream", "the_river", "child_of_mine"};``` I use Crashlytics (which is pestering me to upgrade to Firebase). However, this crash doesn't appear in that system, only in the Google Play Console. – Neill Bowler Nov 16 '19 at 13:52
  • 1
    Any activity is an entry point to your app. The framework is allowed to kill your app and then resume it to any activity without going through your main activity. Similarly if you init crashlytics in the main activity it's not init if the app launches in this another activity. Move your app-wide init from activity to your application class. – laalto Nov 16 '19 at 13:55
  • You can probably reproduce this crash with the "don't keep activities" developer setting. – laalto Nov 16 '19 at 13:56
  • Thanks, that's really useful. – Neill Bowler Nov 17 '19 at 14:25