/* * Copyright 2024 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include #include #include #include "applib/app.h" #include "applib/app_timer.h" #include "applib/battery_state_service.h" #include "applib/ui/dialogs/simple_dialog.h" #include "applib/ui/window.h" #include "kernel/pbl_malloc.h" #include "kernel/util/standby.h" #include "process_management/pebble_process_md.h" #include "process_management/worker_manager.h" #include "process_state/app_state/app_state.h" #include "resource/resource.h" #include "resource/resource_ids.auto.h" #include "services/runlevel.h" #include "services/common/status_led.h" #include "system/logging.h" #include "system/passert.h" #include "system/reboot_reason.h" #include "system/reset.h" static const uint32_t CHARGER_DISCONNECT_TIMEOUT_MS = 3000; typedef enum DialogState { DialogState_Uninitialized = 0, DialogState_Charging, DialogState_FullyCharged, } DialogState; struct AppData { SimpleDialog *dialog; AppTimer *poweroff_timer; DialogState last_dialog_state; bool was_plugged; }; static void prv_reboot_on_click(ClickRecognizerRef recognizer, void *data) { // Don't try to return to normal functioning; just reboot the watch. The user // thinks the watch is already off anyway. RebootReason reboot_reason = { RebootReasonCode_ShutdownMenuItem }; reboot_reason_set(&reboot_reason); system_reset(); } static void prv_config_provider(void *context) { for (int i = 0; i < NUM_BUTTONS; ++i) { window_long_click_subscribe(i, 0, prv_reboot_on_click, NULL); } } static void prv_power_off_timer_expired(void *data) { enter_standby(RebootReasonCode_ShutdownMenuItem); } static void prv_battery_state_handler(BatteryChargeState charge) { struct AppData *data = app_state_get_user_data(); if (charge.is_plugged && !data->was_plugged) { app_timer_cancel(data->poweroff_timer); } else if (!charge.is_plugged && data->was_plugged) { data->poweroff_timer = app_timer_register( CHARGER_DISCONNECT_TIMEOUT_MS, prv_power_off_timer_expired, NULL); } DialogState next_dialog_state = DialogState_Uninitialized; if (charge.is_charging) { next_dialog_state = DialogState_Charging; } else if (charge.is_plugged) { next_dialog_state = DialogState_FullyCharged; } else { // Unplugged. We'll be shutting down in a couple seconds if the user doesn't // plug the charger back in, so don't change the dialog. next_dialog_state = data->last_dialog_state; } Dialog *dialog = simple_dialog_get_dialog(data->dialog); if (next_dialog_state != data->last_dialog_state) { // Setting the dialog icon to itself restarts the animation, which looks // bad, so we want to avoid that if we can help it. switch (next_dialog_state) { case DialogState_FullyCharged: dialog_set_text(dialog, i18n_get("Fully Charged", data)); dialog_set_icon(dialog, RESOURCE_ID_BATTERY_ICON_FULL_LARGE_INVERTED); break; case DialogState_Charging: default: dialog_set_text(dialog, i18n_get("Charging", data)); dialog_set_icon(dialog, RESOURCE_ID_BATTERY_ICON_CHARGING_LARGE_INVERTED); break; } } if (charge.is_plugged) { if (charge.is_charging) { status_led_set(StatusLedState_Charging); } else { status_led_set(StatusLedState_FullyCharged); } } else { status_led_set(StatusLedState_Off); } data->was_plugged = charge.is_plugged; data->last_dialog_state = next_dialog_state; } static void prv_handle_init(void) { struct AppData *data = app_malloc_check(sizeof(struct AppData)); *data = (struct AppData){}; app_state_set_user_data(data); data->dialog = simple_dialog_create(WINDOW_NAME("Shutdown Charging")); Dialog *dialog = simple_dialog_get_dialog(data->dialog); dialog_set_background_color(dialog, GColorBlack); dialog_set_text_color(dialog, GColorWhite); window_set_click_config_provider(&dialog->window, prv_config_provider); // The assumption is that this app is launched when the charger is connected // and the shutdown menu item is selected. data->was_plugged = true; data->last_dialog_state = DialogState_Uninitialized; battery_state_service_subscribe(prv_battery_state_handler); // Handle the edge-case where the charger is disconnected between the user // selecting shut down and this app subscribing to battery state events. // Also set the initial battery charge level. prv_battery_state_handler(battery_state_service_peek()); app_simple_dialog_push(data->dialog); // TODO: have the runlevel machinery disable bluetooth and worker. services_set_runlevel(RunLevel_BareMinimum); worker_manager_disable(); } static void s_main(void) { prv_handle_init(); app_event_loop(); } const PebbleProcessMd* shutdown_charging_get_app_info(void) { static const PebbleProcessMdSystem s_app_md = { .common = { .main_func = s_main, .visibility = ProcessVisibilityHidden, // UUID: 48fa66c4-4e6f-4b32-bf75-a16e12d630c3 .uuid = {0x48, 0xfa, 0x66, 0xc4, 0x4e, 0x6f, 0x4b, 0x32, 0xbf, 0x75, 0xa1, 0x6e, 0x12, 0xd6, 0x30, 0xc3}, }, .name = "Shutdown Charging", }; return (const PebbleProcessMd*) &s_app_md; }