pebble/src/fw/shell/normal/shutdown_charging.c
2025-01-27 11:38:16 -08:00

172 lines
5.6 KiB
C

/*
* 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 <inttypes.h>
#include <stdio.h>
#include <string.h>
#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;
}