سؤال

Edit: here's the source on PasteBin. I feel like I might need to just redesign the entire Service.. :(

I'm the developer of RingPack. The basic idea is that there is a Service launched in the background that takes care of switching the ringtone out for the user. I'm having issues with losing my reference to an ArrayList within the Service. I think I may be misunderstanding how the lifecycle works. My intent was for it to be started whenever the user selects a pack from the Activity.

Intent i = new Intent(RingActivity.this, RingService.class); i.putExtra(RingService.ACTION, RingService.PACK_SET); i.putExtra(RingService.PASSED_PACK, currentPackId); RingActivity.this.startService(i);

I tell the Service to set the Default Notification Tone to the first tone of the pack corresponding to "currentPackId".

When the user wants to turn off RingPack, the disabling is done like so:

Intent i = new Intent(RingActivity.this, RingService.class); RingActivity.this.stopService(i);

Toast.makeText(RingActivity.this.getBaseContext(), RingActivity.this.getString(R.string.ringPackDisabled), Toast.LENGTH_SHORT).show();

So the Service's onCreate looks like so:

public void onCreate() {
db = new DbManager(this);
db.open();

vib = (Vibrator) getSystemService(VIBRATOR_SERVICE);

IntentFilter intentFilter = new IntentFilter(Intent.ACTION_TIME_TICK);
registerReceiver(tReceiver, intentFilter);
timeTick = 0;

//figure out the widget's status
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getBaseContext());
widgetEnabled = prefs.getBoolean(WIDGET_ALIVE, false);

//save the ringtone for playing for the widget
Uri u = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
r = RingtoneManager.getRingtone(this.getBaseContext(), u);
playAfterSet = false;

super.onCreate();}

Then it passes it off to onStartCommand, which returns START_NOT_STICKY (since I will be creating and destroying the Service manually), who passes it off to handleStart().

@Override private void handleStart(Intent intent) {     
final Intent i = intent;
if (isSdOk()) {
    int action = i.getIntExtra(ACTION, -1);

    if (action != -1) {
        if (action == PACK_SET) {
            playAfterSet = true;

            Thread packSetThread = new Thread() {
                @Override
                public void run() {
                    int passedPackId = i.getIntExtra(PASSED_PACK, -1);
                    //we were passed the id
                    if (passedPackId != -1) {
                        checkPrefs();

                        if (!enabled)
                            initControl(passedPackId);
                        else
                            setPack(passedPackId);

                        packSetHandler.sendEmptyMessage(0);
                    }
                }
            };
            packSetThread.start();
        }
        else if (action == NEXT_TONE) {
            checkPrefs();
            swapTone();
        }
        else if (action == PLAY_TONE) {
            playCurrentTone();
        }
        else if (action == WIDGET_STATUS) {
            widgetEnabled = intent.getBooleanExtra(WIDGET_ALIVE, false);
            if (toneName != null)
                RingWidget.update(getBaseContext(), toneName);
        }
    }
}}

The isSdOk() method just checks if the SD card is mounted, since the ringtones are stored on it. initControl() just saves the user's default ringtone, so that we can give it back when they disable us. The setPack() method looks like this:

private void setPack(int packId) {
//save the current pack id in the prefs for restarting
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(RingService.this.getBaseContext());
SharedPreferences.Editor editor = prefs.edit();
editor.putInt(SAVED_PACKID, packId);
editor.commit();

//get the info we need to work with this pack
//it's path on the SD
//build the tones ArrayList to work from
grabPath(packId);
if (tones == null)
    tones = new ArrayList<Integer>();
else
    tones.clear();
mapTones(packId);
currIndex = 0;
setNotificationTone(tones.get(currIndex));}

The tones ArrayList is what I've been losing. This is where it is initialized. It holds the ids of all the enabled ringtones within a pack. The NullPointerException I've been seeing is in swapTone():

private void swapTone() {
//locked
if (lockPref)
    return;
//shuffle on
else if (shufflePref) {
    int randIndex = currIndex;

    while (randIndex == currIndex)
        randIndex = (int) Math.floor(Math.random() * tones.size());
    currIndex = randIndex;
}
//shuffle off
else {
    if (currIndex != (tones.size() - 1))
        currIndex++;
    else
        currIndex = 0;
}

setNotificationTone(tones.get(currIndex));}

They way I intended it work is for swapTone() to never be called if setPack() hasn't already. Again, my users keep getting this error, but I can't reproduce it myself. Any help would be greatly appreciated. I apologize for the code wall, but I am very confused. Perhaps I'm not using the concept of a Service correctly?

هل كانت مفيدة؟

المحلول

Well, despite the "code wall", your listings are incomplete (e.g., you say your problem is with tones but never show where it is defined). I am going to take a guess that it is a data member of your Service.

In that case, it will be null if the service was stopped between PACK_SET and NEXT_TONE operations. This can easily occur, if Android stops the service because it has been running too long. Even with START_NOT_STICKY, if the next startService() call after Android stops the service is NEXT_TONE, not PACK_SET, you will have this problem.

IOW, your data model (the chosen ring pack) is not stored in a persistent location, but rather is held in RAM (tones). You have two choices:

  1. Try to figure out a way that you do not need to be an always-running service, and load the tones out of a persistent store (e.g., database) as needed. This would be ideal, as you are chewing up a bunch of RAM while not delivering value for that RAM every microsecond. For example, you could use AlarmManager with an IntentService.

  2. Use startForeground() to make it less likely that Android will stop your service. The trade-off is that you will need to place a Notification on the screen, so the user knows you are constantly running. You might make that Notification lead the user to the activity where they can configure or shut down the service.

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top