Skip to content

Commit f79b3a3

Browse files
committed
Add feedback menu item.
Signed-off-by: Katharine Berry <[email protected]>
1 parent 2194e58 commit f79b3a3

File tree

16 files changed

+525
-5
lines changed

16 files changed

+525
-5
lines changed

app/CMakeLists.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,4 +68,5 @@ add_executable(tiny_assistant_app
6868
src/c/version/version.c
6969
src/c/util/time.c
7070
src/c/converse/segments/widgets/timer.c
71-
src/c/converse/segments/widgets/number.c)
71+
src/c/converse/segments/widgets/number.c
72+
src/c/menus/feedback_window.c)

app/package.json

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,12 @@
8383
"HIGHLIGHT_WIDGET",
8484
"HIGHLIGHT_WIDGET_PRIMARY",
8585
"HIGHLIGHT_WIDGET_SECONDARY",
86-
"QUOTA_HAS_SUBSCRIPTION"
86+
"QUOTA_HAS_SUBSCRIPTION",
87+
"FEEDBACK_TEXT",
88+
"FEEDBACK_APP_MAJOR",
89+
"FEEDBACK_APP_MINOR",
90+
"FEEDBACK_ALARM_COUNT",
91+
"FEEDBACK_SEND_RESULT"
8792
],
8893
"resources": {
8994
"media": [
@@ -193,6 +198,11 @@
193198
"name": "LOCATION_CONSENT_TEXT",
194199
"type": "raw"
195200
},
201+
{
202+
"file": "text/feedback_blurb.txt",
203+
"name": "FEEDBACK_BLURB",
204+
"type": "raw"
205+
},
196206
{
197207
"file": "weather/medium/generic.pdc",
198208
"name": "WEATHER_MEDIUM_GENERIC",
@@ -272,6 +282,11 @@
272282
"file": "weather/small/sunny.pdc",
273283
"name": "WEATHER_SMALL_SUNNY",
274284
"type": "raw"
285+
},
286+
{
287+
"file": "images/sent.pdc",
288+
"name": "SENT_IMAGE",
289+
"type": "raw"
275290
}
276291
]
277292
}

app/resources/images/sent.pdc

74 Bytes
Binary file not shown.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
You can give us your feedback on Bobby by dictating a message here. Feedback is not anonymous, and is linked to your Rebble account.
2+
3+
You can also give us feedback on the Rebble discord at rebble.io/discord.

app/src/c/menus/feedback_window.c

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#include "feedback_window.h"
18+
#include "../util/vector_sequence_layer.h"
19+
#include "../util/result_window.h"
20+
#include "../util/style.h"
21+
#include "../alarms/manager.h"
22+
#include "../version/version.h"
23+
#include <pebble.h>
24+
#include <pebble-events/pebble-events.h>
25+
26+
typedef struct {
27+
DictationSession *dict_session;
28+
ScrollLayer *scroll_layer;
29+
TextLayer *text_layer;
30+
GBitmap *select_indicator;
31+
BitmapLayer *select_indicator_layer;
32+
char *blurb;
33+
EventHandle event_handle;
34+
GDrawCommandSequence *loading_sequence;
35+
VectorSequenceLayer *loading_layer;
36+
Layer *scroll_indicator_down;
37+
StatusBarLayer *status_bar_layer;
38+
} FeedbackWindowData;
39+
40+
static void prv_window_load(Window *window);
41+
static void prv_window_unload(Window *window);
42+
static void prv_dictation_status_callback(DictationSession *session, DictationSessionStatus status, char *transcription, void *context);
43+
static void prv_click_config_provider();
44+
static void prv_select_clicked(ClickRecognizerRef recognizer, void *context);
45+
static void prv_app_message_received(DictionaryIterator *iterator, void *context);
46+
47+
void feedback_window_push() {
48+
Window *window = window_create();
49+
FeedbackWindowData *data = malloc(sizeof(FeedbackWindowData));
50+
window_set_user_data(window, data);
51+
window_set_window_handlers(window, (WindowHandlers) {
52+
.load = prv_window_load,
53+
.unload = prv_window_unload,
54+
});
55+
window_stack_push(window, true);
56+
}
57+
58+
static void prv_window_load(Window *window) {
59+
FeedbackWindowData *data = window_get_user_data(window);
60+
GRect bounds = layer_get_bounds(window_get_root_layer(window));
61+
Layer *layer = window_get_root_layer(window);
62+
63+
data->status_bar_layer = status_bar_layer_create();
64+
layer_add_child(layer, status_bar_layer_get_layer(data->status_bar_layer));
65+
bobby_status_bar_config(data->status_bar_layer);
66+
67+
data->scroll_layer = scroll_layer_create(GRect(0, STATUS_BAR_LAYER_HEIGHT, bounds.size.w, bounds.size.h - STATUS_BAR_LAYER_HEIGHT));
68+
scroll_layer_set_callbacks(data->scroll_layer, (ScrollLayerCallbacks) {
69+
.click_config_provider = prv_click_config_provider,
70+
});
71+
scroll_layer_set_shadow_hidden(data->scroll_layer, true);
72+
scroll_layer_set_context(data->scroll_layer, window);
73+
scroll_layer_set_click_config_onto_window(data->scroll_layer, window);
74+
layer_add_child(layer, scroll_layer_get_layer(data->scroll_layer));
75+
data->scroll_indicator_down = layer_create(GRect(0, bounds.size.h - STATUS_BAR_LAYER_HEIGHT, bounds.size.w, STATUS_BAR_LAYER_HEIGHT));
76+
layer_add_child(layer, data->scroll_indicator_down);
77+
ContentIndicator* indicator = scroll_layer_get_content_indicator(data->scroll_layer);
78+
const ContentIndicatorConfig up_config = (ContentIndicatorConfig) {
79+
.layer = status_bar_layer_get_layer(data->status_bar_layer),
80+
.times_out = true,
81+
.alignment = GAlignCenter,
82+
.colors = {
83+
.foreground = GColorBlack,
84+
.background = GColorWhite,
85+
}
86+
};
87+
content_indicator_configure_direction(indicator, ContentIndicatorDirectionUp, &up_config);
88+
const ContentIndicatorConfig down_config = (ContentIndicatorConfig) {
89+
.layer = data->scroll_indicator_down,
90+
.times_out = true,
91+
.alignment = GAlignCenter,
92+
.colors = {
93+
.foreground = GColorBlack,
94+
.background = GColorWhite,
95+
},
96+
};
97+
content_indicator_configure_direction(indicator, ContentIndicatorDirectionDown, &down_config);
98+
99+
ResHandle blurb_handle = resource_get_handle(RESOURCE_ID_FEEDBACK_BLURB);
100+
size_t blurb_length = resource_size(blurb_handle);
101+
data->blurb = malloc(blurb_length + 1);
102+
resource_load(blurb_handle, (uint8_t *)data->blurb, blurb_length);
103+
data->blurb[blurb_length] = '\0';
104+
105+
data->text_layer = text_layer_create(GRect(5, 5, bounds.size.w - 10, 2000));
106+
text_layer_set_text(data->text_layer, data->blurb);
107+
text_layer_set_font(data->text_layer, fonts_get_system_font(FONT_KEY_GOTHIC_24_BOLD));
108+
text_layer_set_overflow_mode(data->text_layer, GTextOverflowModeWordWrap);
109+
GSize text_size = text_layer_get_content_size(data->text_layer);
110+
layer_set_frame(text_layer_get_layer(data->text_layer), GRect(5, 5, bounds.size.w - 10, text_size.h));
111+
scroll_layer_add_child(data->scroll_layer, text_layer_get_layer(data->text_layer));
112+
scroll_layer_set_content_size(data->scroll_layer, GSize(bounds.size.w, text_size.h + 10));
113+
114+
data->select_indicator = gbitmap_create_with_resource(RESOURCE_ID_BUTTON_INDICATOR);
115+
data->select_indicator_layer = bitmap_layer_create(GRect(bounds.size.w - 5, bounds.size.h / 2 - 10, 5, 20));
116+
layer_add_child(layer, bitmap_layer_get_layer(data->select_indicator_layer));
117+
bitmap_layer_set_bitmap(data->select_indicator_layer, data->select_indicator);
118+
bitmap_layer_set_compositing_mode(data->select_indicator_layer, GCompOpSet);
119+
120+
data->loading_sequence = gdraw_command_sequence_create_with_resource(RESOURCE_ID_RUNNING_PONY);
121+
GSize pony_size = gdraw_command_sequence_get_bounds_size(data->loading_sequence);
122+
data->loading_layer = vector_sequence_layer_create(GRect(bounds.size.w / 2 - pony_size.w / 2, bounds.size.h / 2 - pony_size.h / 2, pony_size.w, pony_size.h));
123+
vector_sequence_layer_set_sequence(data->loading_layer, data->loading_sequence);
124+
125+
data->dict_session = dictation_session_create(0, prv_dictation_status_callback, window);
126+
dictation_session_enable_error_dialogs(data->dict_session, true);
127+
dictation_session_enable_confirmation(data->dict_session, true);
128+
129+
data->event_handle = events_app_message_register_inbox_received(prv_app_message_received, window);
130+
}
131+
132+
static void prv_window_unload(Window *window) {
133+
APP_LOG(APP_LOG_LEVEL_DEBUG, "Window unloading");
134+
FeedbackWindowData *data = window_get_user_data(window);
135+
dictation_session_destroy(data->dict_session);
136+
text_layer_destroy(data->text_layer);
137+
scroll_layer_destroy(data->scroll_layer);
138+
gbitmap_destroy(data->select_indicator);
139+
bitmap_layer_destroy(data->select_indicator_layer);
140+
gdraw_command_sequence_destroy(data->loading_sequence);
141+
vector_sequence_layer_destroy(data->loading_layer);
142+
layer_destroy(data->scroll_indicator_down);
143+
status_bar_layer_destroy(data->status_bar_layer);
144+
events_app_message_unsubscribe(data->event_handle);
145+
free(data->blurb);
146+
free(data);
147+
window_destroy(window);
148+
APP_LOG(APP_LOG_LEVEL_DEBUG, "Window unloaded");
149+
}
150+
151+
static void prv_click_config_provider() {
152+
APP_LOG(APP_LOG_LEVEL_INFO, "Click menu configuration");
153+
window_single_click_subscribe(BUTTON_ID_SELECT, prv_select_clicked);
154+
}
155+
156+
static void prv_select_clicked(ClickRecognizerRef recognizer, void *context) {
157+
APP_LOG(APP_LOG_LEVEL_INFO, "Click menu selection");
158+
Window *window = context;
159+
FeedbackWindowData *data = window_get_user_data(window);
160+
dictation_session_start(data->dict_session);
161+
}
162+
163+
static void prv_dictation_status_callback(DictationSession *session, DictationSessionStatus status, char *transcription, void *context) {
164+
Window *window = context;
165+
FeedbackWindowData *data = window_get_user_data(window);
166+
if (status != DictationSessionStatusSuccess) {
167+
return;
168+
}
169+
layer_remove_from_parent(scroll_layer_get_layer(data->scroll_layer));
170+
layer_add_child(window_get_root_layer(window), vector_sequence_layer_get_layer(data->loading_layer));
171+
vector_sequence_layer_play(data->loading_layer);
172+
DictionaryIterator *iter;
173+
app_message_outbox_begin(&iter);
174+
dict_write_cstring(iter, MESSAGE_KEY_FEEDBACK_TEXT, transcription);
175+
VersionInfo version = version_get_current();
176+
dict_write_int8(iter, MESSAGE_KEY_FEEDBACK_APP_MAJOR, version.major);
177+
dict_write_int8(iter, MESSAGE_KEY_FEEDBACK_APP_MINOR, version.minor);
178+
dict_write_int8(iter, MESSAGE_KEY_FEEDBACK_ALARM_COUNT, alarm_manager_get_alarm_count());
179+
app_message_outbox_send();
180+
}
181+
182+
static void prv_app_message_received(DictionaryIterator *iter, void *context) {
183+
Window *window = context;
184+
Tuple *tuple = dict_find(iter, MESSAGE_KEY_FEEDBACK_SEND_RESULT);
185+
if (!tuple) {
186+
return;
187+
}
188+
int result = tuple->value->int32;
189+
if (result == 0) {
190+
GDrawCommandImage *image = gdraw_command_image_create_with_resource(RESOURCE_ID_SENT_IMAGE);
191+
result_window_push("Sent", "Thank you!", image, BRANDED_BACKGROUND_COLOUR);
192+
} else {
193+
GDrawCommandImage *image = gdraw_command_image_create_with_resource(RESOURCE_ID_FAILED_PONY);
194+
result_window_push("Error", "There was a problem 🙁", image, COLOR_FALLBACK(GColorSunsetOrange, GColorWhite));
195+
}
196+
window_stack_remove(window, false);
197+
}

app/src/c/menus/feedback_window.h

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#pragma once
18+
19+
void feedback_window_push();

app/src/c/menus/root_menu.c

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include "alarm_menu.h"
2020
#include "legal_window.h"
2121
#include "reminders_menu.h"
22+
#include "feedback_window.h"
2223
#include "../util/style.h"
2324
#include <pebble.h>
2425

@@ -30,6 +31,7 @@ static void prv_push_alarm_screen(int index, void* context);
3031
static void prv_push_timer_screen(int index, void* context);
3132
static void prv_push_legal_screen(int index, void* context);
3233
static void prv_push_reminders_screen(int index, void* context);
34+
static void prv_push_feedback_screen(int index, void* context);
3335

3436
typedef struct {
3537
SimpleMenuLayer *menu_layer;
@@ -53,7 +55,7 @@ static void prv_window_load(Window* window) {
5355
static SimpleMenuSection section = {
5456
.num_items = 0,
5557
};
56-
static SimpleMenuItem items[5];
58+
static SimpleMenuItem items[6];
5759
// This setup has to be done separately because otherwise the initializer isn't constant.
5860
if (section.num_items == 0) {
5961
items[0] = (SimpleMenuItem) {
@@ -73,10 +75,14 @@ static void prv_window_load(Window* window) {
7375
.callback = prv_push_quota_screen,
7476
};
7577
items[4] = (SimpleMenuItem) {
78+
.title = "Feedback",
79+
.callback = prv_push_feedback_screen,
80+
};
81+
items[5] = (SimpleMenuItem) {
7682
.title = "Legal",
7783
.callback = prv_push_legal_screen,
7884
};
79-
section.num_items = 5;
85+
section.num_items = 6;
8086
section.items = items;
8187
}
8288

@@ -119,3 +125,7 @@ static void prv_push_legal_screen(int index, void* context) {
119125
static void prv_push_reminders_screen(int index, void* context) {
120126
reminders_menu_push();
121127
}
128+
129+
static void prv_push_feedback_screen(int index, void* context) {
130+
feedback_window_push();
131+
}

app/src/pkjs/feedback.js

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/**
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
var config = require('./config');
18+
var location = require('./location');
19+
var reminders = require('./lib/reminders');
20+
var package_json = require('package.json');
21+
var urls = require('./urls');
22+
var session = require('./session');
23+
24+
exports.handleFeedbackRequest = function(request) {
25+
var text = request['FEEDBACK_TEXT'];
26+
var appVersion = '' + request['FEEDBACK_APP_MAJOR'] + '.' + request['FEEDBACK_APP_MINOR'];
27+
var alarmCount = request['FEEDBACK_ALARM_COUNT'];
28+
var locationEnabled = config.isLocationEnabled();
29+
var locationReady = location.isReady();
30+
var settings = config.getSettings();
31+
var unitPreference = settings['UNIT_PREFERENCE'] || '';
32+
var languagePreference = settings['LANGUAGE_CODE'] || '';
33+
var reminderCount = reminders.getAllReminders().length;
34+
var jsVersion = package_json['version'];
35+
var timezone = (-(new Date()).getTimezoneOffset());
36+
var platform = 'unknown';
37+
if (window.cobble) {
38+
platform = 'Cobble';
39+
} else if (window.navigator) {
40+
var userAgent = navigator.userAgent;
41+
var androidVersionRegex = /Android (\d+(?:\.\d+)?)/;
42+
var androidVersion = androidVersionRegex.exec(userAgent);
43+
if (androidVersion) {
44+
platform = 'Android ' + androidVersion[1];
45+
} else {
46+
platform = 'iOS';
47+
}
48+
} else {
49+
platform = 'iOS';
50+
}
51+
var feedback = {
52+
'text': text,
53+
'appVersion': appVersion,
54+
'alarmCount': alarmCount,
55+
'locationEnabled': locationEnabled,
56+
'locationReady': locationReady,
57+
'unitPreference': unitPreference,
58+
'languagePreference': languagePreference,
59+
'reminderCount': reminderCount,
60+
'jsVersion': jsVersion,
61+
'timezone': timezone,
62+
'platform': platform,
63+
'timelineToken': session.userToken
64+
}
65+
console.log("Feedback request: " + JSON.stringify(feedback));
66+
67+
var req = new XMLHttpRequest();
68+
req.open('POST', urls.FEEDBACK_URL, true);
69+
req.setRequestHeader('Content-Type', 'application/json');
70+
req.onload = function(e) {
71+
if (req.readyState === 4) {
72+
if (req.status === 200) {
73+
console.log("Feedback sent successfully");
74+
Pebble.sendAppMessage({FEEDBACK_SEND_RESULT: 0});
75+
} else {
76+
console.log("Feedback request returned error code " + req.status.toString());
77+
Pebble.sendAppMessage({FEEDBACK_SEND_RESULT: 1});
78+
}
79+
}
80+
}
81+
req.send(JSON.stringify(feedback));
82+
}

0 commit comments

Comments
 (0)