Updating more Frequently in Ambient Mode not working - Android Wear / WearOS

588 views Asked by At

I'm currently developing an app with support for ambient mjode. That's nothing difficult. Now, my app has two timers that need to Update every second. So I went for the Google Documentation here. I've tried it multiple times, I've checked every line of code the documentation suggests, but I couldn't get it to work properly.

When the emulator or watch enters ambient Mode, the timer goes on for a few seconds, then stops for a few seconds. Then updates like 1-3 times, and then stops again. The weird thing is, this happens without any sort of pattern.

I haven't got a single clue on how to deal with this problem.

Edit: When running in the emulator, it seems to more or less be an interval of 5seconds. However, when I output the triggerTimeMs - CurrenttimeMs to the log, it always shows 1, which would technically mean that the alarm has been scheduled for the next second.

Here's my code:

import android.app.AlarmManager;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.*;
import android.support.constraint.ConstraintLayout;
import android.support.v4.app.FragmentActivity;
import android.support.v4.content.ContextCompat;
import android.support.wear.ambient.AmbientModeSupport;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.Chronometer;
import android.widget.TextClock;
import android.widget.TextView;
import android.widget.Toast;
import android.support.v4.app.NotificationCompat;

import java.time.LocalDateTime;
import java.util.List;
import java.util.concurrent.TimeUnit;

public class TimerActivity extends FragmentActivity implements AmbientModeSupport.AmbientCallbackProvider {

    @Override
    public AmbientModeSupport.AmbientCallback getAmbientCallback() {
        return new MyAmbientCallback();
    }

    private static final String AMBIENT_UPDATE_ACTION = "de.jannisgoeing.sportstimerforwearos.action.AMBIENT_UPDATE";

    private static final int BURN_IN_OFFSET_PX = 10;
    boolean mIsLowBitAmbient;
    boolean mDoBurnInProtection;

    private AlarmManager mAmbientUpdateAlarmManager;
    private PendingIntent mAmbientUpdatePendingIntent;
    private BroadcastReceiver mAmbientUpdateBroadcastReceiver;

    private AmbientModeSupport.AmbientController mAmbientController;

    private ConstraintLayout timer_layout;
    private TextClock clock;
    private TextView timerName;
    private TextView sectionName;
    private long pauseOffset;
    private Intent intent;
    private Timer timer;
    private Button timerButton;
    private int timerStatus = 0;
    private TimerSection currentTimerSection;
    private int prev_timer_status;
    private TextView label_current;
    private TextView label_actual;

    private Chronometer current;
    private Chronometer actual;
    private Boolean running;

    private NotificationManager notificationManager;

    @SuppressWarnings( "deprecation" )
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_timer);

        // Enables Always-on
        mAmbientController = AmbientModeSupport.attach(this);

        mAmbientUpdateAlarmManager =
                (AlarmManager) getSystemService(Context.ALARM_SERVICE);

        Intent ambientUpdateIntent = new Intent(AMBIENT_UPDATE_ACTION);

        mAmbientUpdatePendingIntent = PendingIntent.getBroadcast(
                this, 0, ambientUpdateIntent, PendingIntent.FLAG_UPDATE_CURRENT);

        mAmbientUpdateBroadcastReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                refreshDisplayAndSetNextUpdate();
            }
        };

        final Vibrator vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
        notificationManager = getSystemService(NotificationManager.class);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            notificationManager.createNotificationChannel(
                    new NotificationChannel("timer", "Timer", NotificationManager.IMPORTANCE_HIGH)
            );
        }

        intent = this.getIntent();
        if (intent.getExtras() == null) {
            Toast.makeText(this, R.string.error_noProfilePassed, Toast.LENGTH_LONG).show();
            intent = new Intent(this, MainActivity.class);
            this.startActivity(intent);
        } else {
            timer = (Timer) intent.getSerializableExtra("TIMER");
        }

        if (timer == null) {
            Toast.makeText(this, R.string.error_noValidTimerPassed, Toast.LENGTH_LONG).show();
            intent = new Intent(this, MainActivity.class);
            this.startActivity(intent);
        }

        timer_layout = findViewById(R.id.timer_layout);

        clock = findViewById(R.id.textClock);
        clock.setFormat24Hour(clock.getFormat24Hour());

        timerName = findViewById(R.id.timerName);
        timerName.setText(timer.getName());

        current = findViewById(R.id.chronometerCurrent);
        actual = findViewById(R.id.chronometerActual);

        label_current = findViewById(R.id.labelCurrentTime);
        label_actual = findViewById(R.id.labelActualTime);

        currentTimerSection = getFirstTimerSection(timer);

        sectionName = findViewById(R.id.sectionName);
        sectionName.setText(currentTimerSection.getName());

        running = false;

        Intent stopIntent = new Intent("de.jannisgoeing.sportstimerforwearos.STOP");
        PendingIntent stopPendingIntent = PendingIntent.getBroadcast(this, 0, stopIntent, 0);
        Intent startIntent = new Intent("de.jannisgoeing.sportstimerforwearos.STARTPAUSE");
        PendingIntent startPendingIntent = PendingIntent.getBroadcast(this, 0, startIntent, 0);

        final NotificationCompat.Builder builder = new NotificationCompat.Builder(this)
                .setWhen(System.currentTimeMillis() - SystemClock.elapsedRealtime() + current.getBase())
                .setUsesChronometer(false)
                .setContentText("Waiting for Next Section to start.")
                .setSmallIcon(timer.getIcon())
                .setAutoCancel(false)
                .setOngoing(true)
                .setOnlyAlertOnce(true)
                .setContentIntent(
                        PendingIntent.getActivity(this, 10,
                                new Intent(this, TimerActivity.class)
                        .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP),
                                0)
                )
                .addAction(running ? R.drawable.ic_pause_white
                        : R.drawable.ic_play_white,
                        running ? this.getString(R.string.button_pause)
                        : this.getString(R.string.button_resume),
                        startPendingIntent)
                .addAction(R.drawable.ic_stop_white, this.getString(R.string.button_end), stopPendingIntent);
        notificationManager.notify(1, builder.build());

        timerButton = findViewById(R.id.timerButton);
        timerButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                if(timerStatus == 0) { //0=Status: Not started
                    timerStatus = 1;
                    timerButton.setText(R.string.button_pause);
                    checkHeadStart();
                    builder.setWhen(System.currentTimeMillis() - SystemClock.elapsedRealtime() + current.getBase()).setUsesChronometer(true).setContentText("").build();
                    notificationManager.notify(1, builder.build());
                    current.start();
                    actual.start();
                    running = true;
                } else if (timerStatus == 1) { //1=Status: Running
                    timerStatus = 2;
                    timerButton.setText(R.string.button_resume);
                    pauseOffset = SystemClock.elapsedRealtime() - current.getBase();
                    builder.setUsesChronometer(false).setContentText("Paused").build();
                    notificationManager.notify(1, builder.build());
                    current.stop();
                    running = false;
                    if(currentTimerSection.getType() == 2) {
                        actual.stop();
                    }
                } else if (timerStatus == 2) { //2=Status: Paused
                    timerStatus = 1;
                    timerButton.setText(R.string.button_pause);

                    current.setBase(SystemClock.elapsedRealtime() - pauseOffset);
                    builder.setWhen(System.currentTimeMillis() - SystemClock.elapsedRealtime() + current.getBase()).setUsesChronometer(true).setContentText("").build();
                    notificationManager.notify(1, builder.build());
                    current.start();
                    running = true;

                    if(currentTimerSection.getType() == 2) {
                        actual.setBase(SystemClock.elapsedRealtime() - pauseOffset);
                        actual.start();
                    }
                } else if (timerStatus == 3) { //3=Status: Finished
                    vibrator.cancel();
                    currentTimerSection = getNextTimerSection(timer, currentTimerSection.getId());
                    current.stop();
                    actual.stop();
                    running = false;
                    actual.setFormat("%s");
                    actual.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.white));
                    if (currentTimerSection == null) {
                        notificationManager.cancel(1);
                        Toast.makeText(getApplicationContext(), R.string.match_finished, Toast.LENGTH_LONG).show();
                        intent = new Intent(getApplicationContext(), MainActivity.class);
                        getApplicationContext().startActivity(intent);
                    } else {
                        sectionName.setText(currentTimerSection.getName());
                        timerButton.setText(R.string.button_start);
                        timerStatus = 0;

                        checkHeadStart();
                        builder.setWhen(System.currentTimeMillis() - SystemClock.elapsedRealtime() + current.getBase()).setUsesChronometer(false).setContentText("Waiting for Next Section to start.").build();
                        notificationManager.notify(1, builder.build());
                    }
                }
            }
        });

        timerButton.setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View v) {
                if(timerStatus != 3) {
                    prev_timer_status = timerStatus;
                    timerStatus = 3;
                    timerButton.setText(R.string.button_end);
                    return true;
                } else {
                    timerStatus = prev_timer_status;
                    if(timerStatus == 1) {
                        timerButton.setText(R.string.button_pause);
                    } else if (timerStatus == 2) {
                        timerButton.setText(R.string.button_resume);
                    } else if (timerStatus == 0) {
                        timerButton.setText(R.string.button_start);
                    }
                    return true;
                }
            }
        });

        actual.setOnChronometerTickListener(new Chronometer.OnChronometerTickListener() {
            @Override
            public void onChronometerTick(Chronometer chronometer) {
                if(SystemClock.elapsedRealtime() - chronometer.getBase() >= currentTimerSection.getMinutes()*60000 + currentTimerSection.getSeconds()*1000 + currentTimerSection.getMinutesHeadStart()*60000 + currentTimerSection.getSecondsHeadStart()*1000 && chronometer.getCurrentTextColor() != ContextCompat.getColor(getApplicationContext(), R.color.red)) {
                    chronometer.setFormat("+%s");
                    chronometer.setBase(SystemClock.elapsedRealtime());
                    chronometer.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.red));
                    long[] timings = {0, 500, 500, 500, 500};
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                        vibrator.vibrate(VibrationEffect.createWaveform(timings, 0));
                    } else {
                        vibrator.vibrate(timings, 0);
                    }
                }
            }
        });
        current.setOnChronometerTickListener(new Chronometer.OnChronometerTickListener() {
            @Override
            public void onChronometerTick(Chronometer chronometer) {
                if(SystemClock.elapsedRealtime() - chronometer.getBase() >= currentTimerSection.getMinutes()*60000 + currentTimerSection.getSeconds()*1000 + currentTimerSection.getMinutesHeadStart()*60000 + currentTimerSection.getSecondsHeadStart()*1000 && timerStatus != 3) {
                    timerStatus = 3;
                    timerButton.setText(R.string.button_end);
                    current.stop();
                }
            }
        });
    }

    private void checkHeadStart() {
        if(currentTimerSection.isTimerHeadStart()) {
            current.setBase(SystemClock.elapsedRealtime() - currentTimerSection.getMinutesHeadStart()*60000 - currentTimerSection.getSecondsHeadStart()*1000);
            actual.setBase(SystemClock.elapsedRealtime() - currentTimerSection.getMinutesHeadStart()*60000 - currentTimerSection.getSecondsHeadStart()*1000);
        } else {
            current.setBase(SystemClock.elapsedRealtime());
            actual.setBase(SystemClock.elapsedRealtime());
        }
    }

    private static final long AMBIENT_INTERVAL_MS = TimeUnit.SECONDS.toMillis(1);
    //private static final long ACTIVE_INTERVAL_MS = TimeUnit.SECONDS.toMillis(1);
    //private static final int MSG_UPDATE_SCREEN = 0;
    private void refreshDisplayAndSetNextUpdate() {
        if (mAmbientController.isAmbient()) {
            // Implement data retrieval and update the screen for ambient mode
        } else {
            // Implement data retrieval and update the screen for interactive mode
        }
        long timeMs = System.currentTimeMillis();
        // Schedule a new alarm
        if (mAmbientController.isAmbient()) {
            // Calculate the next trigger time
            long delayMs = AMBIENT_INTERVAL_MS - (timeMs % AMBIENT_INTERVAL_MS);
            long triggerTimeMs = timeMs + delayMs;
            mAmbientUpdateAlarmManager.setExact(
                    AlarmManager.RTC_WAKEUP,
                    triggerTimeMs,
                    mAmbientUpdatePendingIntent);
        } else {
            //long delayMs = ACTIVE_INTERVAL_MS - (timeMs % ACTIVE_INTERVAL_MS);

            //mActiveModeUpdateHandler.removeMessages(MSG_UPDATE_SCREEN);
            //mActiveModeUpdateHandler.sendEmptyMessageDelayed(MSG_UPDATE_SCREEN, delayMs);
        }
    }

    @Override
    public void onResume() {
        super.onResume();
        IntentFilter filter = new IntentFilter(AMBIENT_UPDATE_ACTION);
        registerReceiver(mAmbientUpdateBroadcastReceiver, filter);

        refreshDisplayAndSetNextUpdate();
    }

    @Override
    public void onPause() {
        super.onPause();
        unregisterReceiver(mAmbientUpdateBroadcastReceiver);
        mAmbientUpdateAlarmManager.cancel(mAmbientUpdatePendingIntent);
    }

    @Override
    public void onDestroy() {
        mAmbientUpdateAlarmManager.cancel(mAmbientUpdatePendingIntent);
        notificationManager.cancel(1);
        Vibrator vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
        vibrator.cancel();
        super.onDestroy();
    }

    public TimerSection getFirstTimerSection(Timer timer) {
        List<TimerSection> timerSectionList = timer.getTimerSections();
        for (TimerSection t : timerSectionList) {
            if (t.id == 1) {
                return t;
            }
        }

        return null;
    }

    public TimerSection getNextTimerSection(Timer timer, int id) {
        List<TimerSection> timerSectionList = timer.getTimerSections();
        for(TimerSection t : timerSectionList) {
            if (t.id == id + 1) {
                return t;
            }
        }

        return null;
    }

    private class MyAmbientCallback extends AmbientModeSupport.AmbientCallback {
        /** Prepares the UI for ambient mode. */
        @Override
        public void onEnterAmbient(Bundle ambientDetails) {
            super.onEnterAmbient(ambientDetails);

            timerButton.setVisibility(View.INVISIBLE);
            Vibrator vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
            vibrator.cancel();

            mIsLowBitAmbient =
                    ambientDetails.getBoolean(AmbientModeSupport.EXTRA_LOWBIT_AMBIENT, false);
            mDoBurnInProtection =
                    ambientDetails.getBoolean(AmbientModeSupport.EXTRA_BURN_IN_PROTECTION, false);

            /* Clears Handler queue (only needed for updates in active mode). */
            //mActiveModeUpdateHandler.removeMessages(MSG_UPDATE_SCREEN);

            /*
             * Following best practices outlined in WatchFaces API (keeping most pixels black,
             * avoiding large blocks of white pixels, using only black and white, and disabling
             * anti-aliasing, etc.)
             */

            if (mIsLowBitAmbient) {
                clock.getPaint().setAntiAlias(false);
                current.getPaint().setAntiAlias(false);
                actual.getPaint().setAntiAlias(false);
                timerButton.getPaint().setAntiAlias(false);
                label_actual.getPaint().setAntiAlias(false);
                label_current.getPaint().setAntiAlias(false);
                timerName.getPaint().setAntiAlias(false);
                sectionName.getPaint().setAntiAlias(false);
            }

            refreshDisplayAndSetNextUpdate();
        }

        @Override
        public void onUpdateAmbient() {
            super.onUpdateAmbient();

            if (mDoBurnInProtection) {
                int x = (int) (Math.random() * 2 * BURN_IN_OFFSET_PX - BURN_IN_OFFSET_PX);
                int y = (int) (Math.random() * 2 * BURN_IN_OFFSET_PX - BURN_IN_OFFSET_PX);
                timer_layout.setPadding(x, y, 0, 0);
            }
        }

        /** Restores the UI to active (non-ambient) mode. */
        @Override
        public void onExitAmbient() {
            super.onExitAmbient();

            timerButton.setVisibility(View.VISIBLE);
            Vibrator vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
            vibrator.cancel();

            /* Clears out Alarms since they are only used in ambient mode. */
            mAmbientUpdateAlarmManager.cancel(mAmbientUpdatePendingIntent);


            if (mIsLowBitAmbient) {
                clock.getPaint().setAntiAlias(true);
                current.getPaint().setAntiAlias(true);
                actual.getPaint().setAntiAlias(true);
                timerButton.getPaint().setAntiAlias(true);
                label_actual.getPaint().setAntiAlias(true);
                label_current.getPaint().setAntiAlias(true);
                timerName.getPaint().setAntiAlias(true);
                sectionName.getPaint().setAntiAlias(true);
            }

            /* Reset any random offset applied for burn-in protection. */
            if (mDoBurnInProtection) {
                timer_layout.setPadding(0, 0, 0, 0);
            }

            refreshDisplayAndSetNextUpdate();
        }
    }
}
1

There are 1 answers

0
promanowicz On

First you can try to use setExactAndAllowWhileIdle instead setExact method to run the alarm. Eventually you can try to use setAlarmClock but I think most reliable will be using a WakeLock and a Handler for the activity ambient mode.