0

I have an Android watch app built with jetpack compose. One of the screens in the app is a timer. When the user taps a button the timer starts. That all works fine. I added a Tile for the app that should launch the app, switch to the timer page, and automatically start the timer. Everything happens as it should, the app is launched to the correct page, the events are all happening, but the timer isn't starting and I'm not sure why. I could really use a hand...

The Tile code... simplified...

@OptIn(ExperimentalHorologistApi::class)
class TimerRileRenderer(context: Context) : SingleTileLayoutRendrer<TimerTileState, Boolean(context) {

  override fun renderTile(state: TimerTileState, deviceParameters: DeviceParameters): LayoutElement {
    return timerTileLayout(
      context,
      deviceParameters,
      state,
      launchActivityClickable("start_button", startTimer())
    )
  }
}

private fun timerTileLayout(
  context: Context,
  deviceParameters: DeviceParameters,
  state: TimerTileState,
  buttonClickable: Clickable
) = PrimaryLayout.Builder(deviceParameters)
  .setContent(
    Column.Builder()
      .setHorizontalAlignment(HORIZONTAL_ALIGNMENT_CENTER)
      .addContent(
        Chip.Builder(context, buttonClickable, deviceParameters)
          .setIconContent(TimerTileRenderer.START_ICON)
          .setPrimaryLabelContent("START")
          .build()
      )
      .build()
    )
    .build()

Then my clickable code...

fun launchActivityClickable(
  clickableID: String, 
  androidActivity: ActionBuilders.AndroidActivity
) = ModifiersBuilders.Clickable.Builder()
  .setId(clickableID)
  .setOnClick(
    ActionBuilder.LaunchAction.Builder()
      .setAndroidActivity(androidActivity)
      .build()
  )
  .build()

fun startTimer() = ActionBuilders.AndroidActivity.Builder()
  .setActivity()
  .addKeyToExtraMapping(
      NotificationUtil.ACTION_START,
      ActionBuilders.booleanExtra(true)
  )
  .addKeyToExtraMapping(
      NotificationUtil.ACTION_OPEN_PAGE,
      ActionBuilders.stringExtra(NotificationUtil.PAGE_EXTRA_TIMER)
  )
  .build()

internal fun ActionBuilders.AndroidActivity.Builder.setActivity(): ActionBuilders.AndroidActivity.Builder {
  return setPackageName("mypackagenamehere")
    .setClassName("mypackagenamehere.MainActivity")
}

Then in my MainActivity...

var openedPage: Int = 0
 
class MainActivity: ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
      super.onCreate(savedInstanceState)
      //... lots of other code...
      val page = intent.extras?.getString(NotificationUtil.ACTION_OPEN_PAGE)
      if(page != null) {
        openedPage = when(page) {
          NotificationUtil.PAGE_TIMER_EXTRA -> 1
          NotificationUtil.PAGE_STATS_EXTRA -> 2
          else -> 0
        }
      }
      val startTimer = intent.extras?.getBoolean(NotificationUtil.ACTION_START) ?: false
      if(startTimer) {
        openedPage = 1
        val intent = Intent(NotificationUtil.ACTION_START)
        LocalBroadcastManager.getInstance(this).sendBroadcast(intent)
      }

      setContent {
        WearApp(timeViewModel) //that was defined earlier, but ommitted
      }
    }
}

@Composable
fun WearApp(model: TimeViewModel) {
    //lots of code for things here...
    WearAppTheme {
      Scaffold(...) {
        val maxPages = 5
        val currentPage = remember {
          derivedStateOf {
            openedPage
          }
        }
        val pagerState = rememberPagerState(initialPage = currentPage.value)
        //code for other display things...
        HorizontalPager(state = pagerState, count = maxPages) { page ->
          openedPage = pagerState.currentPage
          when(page) {
            0 -> ProgressView(model)
            1 -> TimerView(model)
            //... all the other pages.
          }
        }
      }
    }
}

And finally my TimerView...

@Composable
fun TimerView(model: TimeViewModel) {
  var currentTime by rememberSaveable {
    mutableStateOf(0L)
  }
  var timerIsRunning by rememberSaveable {
    mutableStateOf(false)
  }
  var timerStartedAt by rememberSaveable {
    mutableStateOf(0L)
  }
  LaunchedEffect(key1 = currentTime, key2 = timerIsRunning) {
    if(timerIsRunning) {
      delay(1000L)
      currentTime = SystemClock.elapsedRealtime() - timerStartedAt
    }
  }
  val startReceiver = object: BroadcastReceiver() {
    override fun onReceive(context: Context?, intent: Intent?) {
      Log.d(TAG, "Receive for start timer")
      context?.let {
        Log.d(TAG, "Context is set")
        if(!timerIsRunning) {
          Log.d(TAG, "Timer is NOT running")
          timerStartedAt = SystemClock.elapsedRealtime()
          timerIsRunning = true
        }
      }
    }
  }

  LocalBroadcastManager.getInstance(LocalContext.current).registerReceiver(
    startReceiver, IntentFilter(NotificationUtil.ACTION_START)
  )

  Column(...) {
    Text(
      getAttributedString((currentTime / 1000L).toInt())
    )
    //... lots of other code.
  }
}

Now, when I click on the Chip on the Tile it launches my app to the timer page, and I can see in the logs...

Receive for start timer

Context is set

Timer is NOT running

Which means the broadcast receiver is getting the broadcast and setting the variable to true. Later in the display code I have a button that does the exact same thing and when I click the button the timer starts properly. I can tell because that text element shows the amount of time the timer is running. I'm not sure why the timer isn't actually starting when started from the broadcast receiver. I'm thinking it's something to do with the LaunchedEffect, but I'm not sure, nor am I sure how to fix it.

Help would be greatly appreciated, and I do apologize for the long post.

Neglected Sanity
  • 1,770
  • 6
  • 23
  • 46

0 Answers0