/* * 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 "battery_ui.h" #include #include #include #include "applib/ui/dialogs/dialog_private.h" #include "applib/ui/dialogs/simple_dialog.h" #include "applib/ui/window_stack.h" #include "applib/ui/ui.h" #include "kernel/event_loop.h" #include "kernel/pbl_malloc.h" #include "kernel/ui/kernel_ui.h" #include "kernel/ui/modals/modal_manager.h" #include "process_management/app_manager.h" #include "resource/resource_ids.auto.h" #include "services/common/battery/battery_curve.h" #include "services/common/clock.h" #include "services/common/i18n/i18n.h" #include "services/common/light.h" #include "system/logging.h" #include "util/time/time.h" typedef void (*DialogUpdateFn)(Dialog *, void *); static Dialog *s_dialog = NULL; typedef struct { uint32_t percent; GColor background_color; ResourceId warning_icon; } BatteryWarningDisplayData; // UI Callbacks /////////////////////// static const GColor s_warning_color[] = { { .argb = GColorLightGrayARGB8 }, { .argb = GColorRedARGB8 }, }; static const ResourceId s_warning_icon[] = { RESOURCE_ID_BATTERY_ICON_LOW_LARGE, RESOURCE_ID_BATTERY_ICON_VERY_LOW_LARGE }; static void prv_update_ui_fully_charged(Dialog *dialog, void *ignored) { dialog_set_text(dialog, i18n_get("Fully Charged", dialog)); dialog_set_background_color(dialog, GColorKellyGreen); dialog_set_icon(dialog, RESOURCE_ID_BATTERY_ICON_FULL_LARGE); } static void prv_update_ui_charging(Dialog *dialog, void *ignored) { dialog_set_text(dialog, i18n_get("Charging", dialog)); dialog_set_background_color(dialog, GColorLightGray); dialog_set_icon(dialog, RESOURCE_ID_BATTERY_ICON_CHARGING_LARGE); } static void prv_update_ui_warning(Dialog *dialog, void *context) { const BatteryWarningDisplayData *data = context; const uint32_t percent = data->percent; dialog_set_background_color(dialog, data->background_color); const size_t warning_length = 64; char buffer[warning_length]; const uint32_t battery_hours_left = battery_curve_get_hours_remaining(percent); const char *message = clock_get_relative_daypart_string(rtc_get_time(), battery_hours_left); if (message) { snprintf(buffer, warning_length, i18n_get("Powered 'til %s", dialog), i18n_get(message, dialog)); dialog_set_text(dialog, buffer); } dialog_set_icon(dialog, data->warning_icon); } static void prv_dialog_on_unload(void *context) { Dialog *dialog = context; i18n_free_all(dialog); if (dialog == s_dialog) { s_dialog = NULL; } } static void prv_display_modal(WindowStack *stack, DialogUpdateFn update_fn, void *data) { if (s_dialog) { update_fn(s_dialog, data); return; } SimpleDialog *new_simple_dialog = simple_dialog_create( WINDOW_NAME("Battery Status")); Dialog *new_dialog = simple_dialog_get_dialog(new_simple_dialog); dialog_set_callbacks(new_dialog, &(DialogCallbacks) { .unload = prv_dialog_on_unload, }, NULL); update_fn(new_dialog, data); Dialog *old_dialog = s_dialog; s_dialog = new_dialog; simple_dialog_push(new_simple_dialog, stack); #if PBL_ROUND // For circular display, to fit some battery_ui messages requires 3 lines // Simple dialog only allows up to 2 lines, so adjust here // This has to occur after the dialog push has been called TextLayer *text_layer = &new_simple_dialog->dialog.text_layer; GContext *ctx = graphics_context_get_current_context(); const int font_height = fonts_get_font_height(text_layer->font); const int text_cap_height = fonts_get_font_cap_offset(text_layer->font); const int max_text_height = 2 * font_height + text_cap_height; const int32_t text_height = text_layer_get_content_size(ctx, text_layer).h; if (text_height > max_text_height) { // Values used below were to improve visual aesthetics and were reviewed by design const int num_lines = 3; const int line_spacing_delta = -4; const int text_shift_y = -2; const int text_box_height = (font_height + text_cap_height) * num_lines + line_spacing_delta * (num_lines - 1); const int text_flow_inset = 6; // Modify to allow longer central lines text_layer_enable_screen_text_flow_and_paging(text_layer, text_flow_inset); text_layer_set_size(text_layer, GSize(DISP_COLS, text_box_height)); text_layer->layer.frame.origin.y += text_shift_y; text_layer_set_line_spacing_delta(text_layer, line_spacing_delta); } #endif if (old_dialog) { dialog_pop(old_dialog); } } // Public API //////////////////// void battery_ui_display_plugged(void) { // If we're plugged in for charging, we want to alert the user of this, // but we don't want to overlay ourselves over anything they may have // on the screen at the moment. WindowStack *stack = modal_manager_get_window_stack(ModalPriorityGeneric); prv_display_modal(stack, prv_update_ui_charging, NULL); } void battery_ui_display_fully_charged(void) { // If we're plugged in (charged), we want to alert the user of this, // but we don't want to overlay ourselves over anything they may have // on the screen at the moment. WindowStack *stack = modal_manager_get_window_stack(ModalPriorityGeneric); prv_display_modal(stack, prv_update_ui_fully_charged, NULL); } void battery_ui_display_warning(uint32_t percent, BatteryUIWarningLevel warning_level) { // If we're not plugged in, that means we hit a critical power notification, // so we want to alert the user, subverting any non-critical windows they // have on the screen. WindowStack *stack = modal_manager_get_window_stack(ModalPriorityAlert); BatteryWarningDisplayData display_data = { .percent = percent, .background_color = s_warning_color[warning_level], .warning_icon = s_warning_icon[warning_level], }; prv_display_modal(stack, prv_update_ui_warning, &display_data); } void battery_ui_dismiss_modal(void) { if (s_dialog) { dialog_pop(s_dialog); s_dialog = NULL; } }