├── Bin
├── libeay32.dll
├── nghttp2.dll
└── ssleay32.dll
├── Example.Windows
├── DelphiRemotePushSender.dpr
├── DelphiRemotePushSender.dproj
├── DelphiRemotePushSender.res
├── FMain.dfm
└── FMain.pas
├── License.txt
├── Nghttp2.pas
├── README.md
└── ReceivingPushNotifications.md
/Bin/libeay32.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grijjy/DelphiRemotePushSender/f85c96b4efb8d902658d441d80fbde45d14c7721/Bin/libeay32.dll
--------------------------------------------------------------------------------
/Bin/nghttp2.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grijjy/DelphiRemotePushSender/f85c96b4efb8d902658d441d80fbde45d14c7721/Bin/nghttp2.dll
--------------------------------------------------------------------------------
/Bin/ssleay32.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grijjy/DelphiRemotePushSender/f85c96b4efb8d902658d441d80fbde45d14c7721/Bin/ssleay32.dll
--------------------------------------------------------------------------------
/Example.Windows/DelphiRemotePushSender.dpr:
--------------------------------------------------------------------------------
1 | program DelphiRemotePushSender;
2 |
3 | uses
4 | Vcl.Forms,
5 | FMain in 'FMain.pas' {FormMain},
6 | Grijjy.RemotePush.Sender in '..\..\GrijjyFoundation\Grijjy.RemotePush.Sender.pas',
7 | Grijjy.Bson in '..\..\GrijjyFoundation\Grijjy.Bson.pas',
8 | Grijjy.Http in '..\..\GrijjyFoundation\Grijjy.Http.pas',
9 | Grijjy.SocketPool.Win in '..\..\GrijjyFoundation\Grijjy.SocketPool.Win.pas',
10 | Grijjy.Uri in '..\..\GrijjyFoundation\Grijjy.Uri.pas',
11 | Grijjy.Winsock2 in '..\..\GrijjyFoundation\Grijjy.Winsock2.pas',
12 | Grijjy.OpenSSL in '..\..\GrijjyFoundation\Grijjy.OpenSSL.pas',
13 | Grijjy.OpenSSL.API in '..\..\GrijjyFoundation\Grijjy.OpenSSL.API.pas',
14 | Grijjy.MemoryPool in '..\..\GrijjyFoundation\Grijjy.MemoryPool.pas',
15 | Grijjy.Collections in '..\..\GrijjyFoundation\Grijjy.Collections.pas',
16 | Grijjy.BinaryCoding in '..\..\GrijjyFoundation\Grijjy.BinaryCoding.pas',
17 | Nghttp2 in '..\Nghttp2.pas',
18 | Grijjy.SysUtils in '..\..\GrijjyFoundation\Grijjy.SysUtils.pas',
19 | Grijjy.DateUtils in '..\..\GrijjyFoundation\Grijjy.DateUtils.pas',
20 | Grijjy.Bson.IO in '..\..\GrijjyFoundation\Grijjy.Bson.IO.pas';
21 |
22 | {$R *.res}
23 |
24 | begin
25 | Application.Initialize;
26 | Application.MainFormOnTaskbar := True;
27 | Application.CreateForm(TFormMain, FormMain);
28 | Application.Run;
29 | end.
30 |
--------------------------------------------------------------------------------
/Example.Windows/DelphiRemotePushSender.dproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | {DC769454-4F24-42BF-9554-DA0A49F5D879}
4 | 18.2
5 | VCL
6 | DelphiRemotePushSender.dpr
7 | True
8 | Debug
9 | Win32
10 | 1
11 | Application
12 |
13 |
14 | true
15 |
16 |
17 | true
18 | Base
19 | true
20 |
21 |
22 | true
23 | Base
24 | true
25 |
26 |
27 | true
28 | Base
29 | true
30 |
31 |
32 | true
33 | Cfg_1
34 | true
35 | true
36 |
37 |
38 | true
39 | Base
40 | true
41 |
42 |
43 | true
44 | Cfg_2
45 | true
46 | true
47 |
48 |
49 | DelphiRemotePushSender
50 | $(BDS)\bin\delphi_PROJECTICON.ico
51 | $(BDS)\bin\Artwork\Windows\UWP\delphi_UwpDefault_44.png
52 | System;Xml;Data;Datasnap;Web;Soap;Vcl;Vcl.Imaging;Vcl.Touch;Vcl.Samples;Vcl.Shell;$(DCC_Namespace)
53 | $(BDS)\bin\Artwork\Windows\UWP\delphi_UwpDefault_150.png
54 | CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments=
55 | 1033
56 | .\$(Platform)\$(Config)
57 | ..\Bin
58 | false
59 | false
60 | false
61 | false
62 | false
63 |
64 |
65 | Debug
66 | true
67 | DBXSqliteDriver;RESTComponents;fmxase;DBXDb2Driver;DBXInterBaseDriver;vclactnband;vclFireDAC;emsclientfiredac;tethering;svnui;DataSnapFireDAC;FireDACADSDriver;DBXMSSQLDriver;DatasnapConnectorsFreePascal;FireDACMSSQLDriver;vcltouch;vcldb;bindcompfmx;svn;DBXOracleDriver;inetdb;emsedge;FireDACIBDriver;fmx;fmxdae;FireDACDBXDriver;dbexpress;IndyCore;vclx;dsnap;emsclient;DataSnapCommon;FireDACCommon;RESTBackendComponents;DataSnapConnectors;VCLRESTComponents;soapserver;vclie;bindengine;DBXMySQLDriver;CloudService;FireDACOracleDriver;FireDACMySQLDriver;DBXFirebirdDriver;FireDACCommonODBC;FireDACCommonDriver;DataSnapClient;inet;IndyIPCommon;bindcompdbx;vcl;IndyIPServer;DBXSybaseASEDriver;IndySystem;FireDACDb2Driver;dsnapcon;FireDACMSAccDriver;fmxFireDAC;FireDACInfxDriver;vclimg;FireDAC;emshosting;FireDACSqliteDriver;FireDACPgDriver;FireDACASADriver;DBXOdbcDriver;FireDACTDataDriver;soaprtl;DbxCommonDriver;DataSnapServer;xmlrtl;soapmidas;DataSnapNativeClient;fmxobj;vclwinx;FireDACDSDriver;rtl;DbxClientDriver;DBXSybaseASADriver;CustomIPTransport;vcldsnap;bindcomp;appanalytics;DBXInformixDriver;IndyIPClient;bindcompvcl;dbxcds;VclSmp;adortl;FireDACODBCDriver;DataSnapIndy10ServerTransport;dsnapxml;DataSnapProviderClient;dbrtl;IndyProtocols;inetdbxpress;FireDACMongoDBDriver;DataSnapServerMidas;$(DCC_UsePackage)
68 | Winapi;System.Win;Data.Win;Datasnap.Win;Web.Win;Soap.Win;Xml.Win;Bde;$(DCC_Namespace)
69 | $(BDS)\bin\default_app.manifest
70 | CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments=
71 | 1033
72 |
73 |
74 | DBXSqliteDriver;RESTComponents;fmxase;DBXDb2Driver;DBXInterBaseDriver;vclactnband;vclFireDAC;emsclientfiredac;tethering;DataSnapFireDAC;FireDACADSDriver;DBXMSSQLDriver;DatasnapConnectorsFreePascal;FireDACMSSQLDriver;vcltouch;vcldb;bindcompfmx;DBXOracleDriver;inetdb;emsedge;FireDACIBDriver;fmx;fmxdae;FireDACDBXDriver;dbexpress;IndyCore;vclx;dsnap;emsclient;DataSnapCommon;FireDACCommon;RESTBackendComponents;DataSnapConnectors;VCLRESTComponents;soapserver;vclie;bindengine;DBXMySQLDriver;CloudService;FireDACOracleDriver;FireDACMySQLDriver;DBXFirebirdDriver;FireDACCommonODBC;FireDACCommonDriver;DataSnapClient;inet;IndyIPCommon;bindcompdbx;vcl;IndyIPServer;DBXSybaseASEDriver;IndySystem;FireDACDb2Driver;dsnapcon;FireDACMSAccDriver;fmxFireDAC;FireDACInfxDriver;vclimg;FireDAC;emshosting;FireDACSqliteDriver;FireDACPgDriver;FireDACASADriver;DBXOdbcDriver;FireDACTDataDriver;soaprtl;DbxCommonDriver;DataSnapServer;xmlrtl;soapmidas;DataSnapNativeClient;fmxobj;vclwinx;FireDACDSDriver;rtl;DbxClientDriver;DBXSybaseASADriver;CustomIPTransport;vcldsnap;bindcomp;appanalytics;DBXInformixDriver;IndyIPClient;bindcompvcl;dbxcds;VclSmp;adortl;FireDACODBCDriver;DataSnapIndy10ServerTransport;dsnapxml;DataSnapProviderClient;dbrtl;IndyProtocols;inetdbxpress;FireDACMongoDBDriver;DataSnapServerMidas;$(DCC_UsePackage)
75 |
76 |
77 | DEBUG;$(DCC_Define)
78 | true
79 | false
80 | true
81 | true
82 | true
83 |
84 |
85 | true
86 | true
87 | 1033
88 | true
89 | false
90 |
91 |
92 | false
93 | RELEASE;$(DCC_Define)
94 | 0
95 | 0
96 |
97 |
98 | true
99 | true
100 |
101 |
102 |
103 | MainSource
104 |
105 |
106 |
107 | dfm
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 | Cfg_2
126 | Base
127 |
128 |
129 | Base
130 |
131 |
132 | Cfg_1
133 | Base
134 |
135 |
136 |
137 | Delphi.Personality.12
138 | Application
139 |
140 |
141 |
142 | DelphiRemotePushSender.dpr
143 |
144 |
145 | Microsoft Office 2000 Sample Automation Server Wrapper Components
146 | Microsoft Office XP Sample Automation Server Wrapper Components
147 |
148 |
149 |
150 |
151 |
152 | DelphiRemotePushSender.exe
153 | true
154 |
155 |
156 |
157 |
158 | 1
159 |
160 |
161 | Contents\MacOS
162 | 1
163 |
164 |
165 | Contents\MacOS
166 | 0
167 |
168 |
169 |
170 |
171 | classes
172 | 1
173 |
174 |
175 |
176 |
177 | library\lib\armeabi-v7a
178 | 1
179 |
180 |
181 |
182 |
183 | library\lib\armeabi
184 | 1
185 |
186 |
187 |
188 |
189 | library\lib\mips
190 | 1
191 |
192 |
193 |
194 |
195 | library\lib\armeabi-v7a
196 | 1
197 |
198 |
199 |
200 |
201 | res\drawable
202 | 1
203 |
204 |
205 |
206 |
207 | res\values
208 | 1
209 |
210 |
211 |
212 |
213 | res\drawable
214 | 1
215 |
216 |
217 |
218 |
219 | res\drawable-xxhdpi
220 | 1
221 |
222 |
223 |
224 |
225 | res\drawable-ldpi
226 | 1
227 |
228 |
229 |
230 |
231 | res\drawable-mdpi
232 | 1
233 |
234 |
235 |
236 |
237 | res\drawable-hdpi
238 | 1
239 |
240 |
241 |
242 |
243 | res\drawable-xhdpi
244 | 1
245 |
246 |
247 |
248 |
249 | res\drawable-small
250 | 1
251 |
252 |
253 |
254 |
255 | res\drawable-normal
256 | 1
257 |
258 |
259 |
260 |
261 | res\drawable-large
262 | 1
263 |
264 |
265 |
266 |
267 | res\drawable-xlarge
268 | 1
269 |
270 |
271 |
272 |
273 | 1
274 |
275 |
276 | Contents\MacOS
277 | 1
278 |
279 |
280 | 0
281 |
282 |
283 |
284 |
285 | Contents\MacOS
286 | 1
287 | .framework
288 |
289 |
290 | 0
291 |
292 |
293 |
294 |
295 | 1
296 | .dylib
297 |
298 |
299 | 1
300 | .dylib
301 |
302 |
303 | 1
304 | .dylib
305 |
306 |
307 | Contents\MacOS
308 | 1
309 | .dylib
310 |
311 |
312 | 0
313 | .dll;.bpl
314 |
315 |
316 |
317 |
318 | 1
319 | .dylib
320 |
321 |
322 | 1
323 | .dylib
324 |
325 |
326 | 1
327 | .dylib
328 |
329 |
330 | Contents\MacOS
331 | 1
332 | .dylib
333 |
334 |
335 | 0
336 | .bpl
337 |
338 |
339 |
340 |
341 | 0
342 |
343 |
344 | 0
345 |
346 |
347 | 0
348 |
349 |
350 | 0
351 |
352 |
353 | Contents\Resources\StartUp\
354 | 0
355 |
356 |
357 | 0
358 |
359 |
360 |
361 |
362 | 1
363 |
364 |
365 | 1
366 |
367 |
368 | 1
369 |
370 |
371 |
372 |
373 | 1
374 |
375 |
376 | 1
377 |
378 |
379 | 1
380 |
381 |
382 |
383 |
384 | 1
385 |
386 |
387 | 1
388 |
389 |
390 | 1
391 |
392 |
393 |
394 |
395 | 1
396 |
397 |
398 | 1
399 |
400 |
401 | 1
402 |
403 |
404 |
405 |
406 | 1
407 |
408 |
409 | 1
410 |
411 |
412 | 1
413 |
414 |
415 |
416 |
417 | 1
418 |
419 |
420 | 1
421 |
422 |
423 | 1
424 |
425 |
426 |
427 |
428 | 1
429 |
430 |
431 | 1
432 |
433 |
434 | 1
435 |
436 |
437 |
438 |
439 | 1
440 |
441 |
442 |
443 |
444 | ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF
445 | 1
446 |
447 |
448 | ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF
449 | 1
450 |
451 |
452 |
453 |
454 | 1
455 |
456 |
457 | 1
458 |
459 |
460 |
461 |
462 | ..\
463 | 1
464 |
465 |
466 | ..\
467 | 1
468 |
469 |
470 |
471 |
472 | 1
473 |
474 |
475 | 1
476 |
477 |
478 | 1
479 |
480 |
481 |
482 |
483 | 1
484 |
485 |
486 | 1
487 |
488 |
489 | 1
490 |
491 |
492 |
493 |
494 | ..\
495 | 1
496 |
497 |
498 |
499 |
500 | Contents
501 | 1
502 |
503 |
504 |
505 |
506 | Contents\Resources
507 | 1
508 |
509 |
510 |
511 |
512 | library\lib\armeabi-v7a
513 | 1
514 |
515 |
516 | 1
517 |
518 |
519 | 1
520 |
521 |
522 | 1
523 |
524 |
525 | 1
526 |
527 |
528 | Contents\MacOS
529 | 1
530 |
531 |
532 | 0
533 |
534 |
535 |
536 |
537 | 1
538 |
539 |
540 | 1
541 |
542 |
543 |
544 |
545 | Assets
546 | 1
547 |
548 |
549 | Assets
550 | 1
551 |
552 |
553 |
554 |
555 | Assets
556 | 1
557 |
558 |
559 | Assets
560 | 1
561 |
562 |
563 |
564 |
565 |
566 |
567 |
568 |
569 |
570 |
571 |
572 |
573 | True
574 | False
575 |
576 |
577 | 12
578 |
579 |
580 |
581 |
582 |
583 |
--------------------------------------------------------------------------------
/Example.Windows/DelphiRemotePushSender.res:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/grijjy/DelphiRemotePushSender/f85c96b4efb8d902658d441d80fbde45d14c7721/Example.Windows/DelphiRemotePushSender.res
--------------------------------------------------------------------------------
/Example.Windows/FMain.dfm:
--------------------------------------------------------------------------------
1 | object FormMain: TFormMain
2 | Left = 0
3 | Top = 0
4 | Caption = 'FormMain'
5 | ClientHeight = 339
6 | ClientWidth = 837
7 | Color = clBtnFace
8 | Font.Charset = DEFAULT_CHARSET
9 | Font.Color = clWindowText
10 | Font.Height = -11
11 | Font.Name = 'Tahoma'
12 | Font.Style = []
13 | OldCreateOrder = False
14 | OnShow = FormShow
15 | PixelsPerInch = 96
16 | TextHeight = 13
17 | object LabelDeviceToken: TLabel
18 | Left = 416
19 | Top = 19
20 | Width = 64
21 | Height = 13
22 | Caption = 'Device Token'
23 | end
24 | object LabelTitle: TLabel
25 | Left = 416
26 | Top = 75
27 | Width = 20
28 | Height = 13
29 | Caption = 'Title'
30 | end
31 | object LabelMessage: TLabel
32 | Left = 416
33 | Top = 131
34 | Width = 42
35 | Height = 13
36 | Caption = 'Message'
37 | end
38 | object ButtonSend: TButton
39 | Left = 750
40 | Top = 296
41 | Width = 75
42 | Height = 25
43 | Caption = 'Send'
44 | TabOrder = 4
45 | OnClick = ButtonSendClick
46 | end
47 | object GroupBoxPlatform: TGroupBox
48 | Left = 8
49 | Top = 8
50 | Width = 377
51 | Height = 305
52 | Caption = 'Platform'
53 | TabOrder = 0
54 | object LabelAndroidAPIKey: TLabel
55 | Left = 24
56 | Top = 224
57 | Width = 75
58 | Height = 13
59 | Caption = 'Android APIKey'
60 | end
61 | object LabelAPNSTopic: TLabel
62 | Left = 24
63 | Top = 48
64 | Width = 54
65 | Height = 13
66 | Caption = 'APNS Topic'
67 | end
68 | object LabelAPNSCertificate: TLabel
69 | Left = 24
70 | Top = 94
71 | Width = 79
72 | Height = 13
73 | Caption = 'APNS Certificate'
74 | end
75 | object LabelAPNSKey: TLabel
76 | Left = 24
77 | Top = 141
78 | Width = 47
79 | Height = 13
80 | Caption = 'APNS Key'
81 | end
82 | object RadioButtoniOS: TRadioButton
83 | Left = 8
84 | Top = 24
85 | Width = 113
86 | Height = 17
87 | Caption = 'iOS'
88 | Checked = True
89 | TabOrder = 0
90 | TabStop = True
91 | OnClick = RadioButtoniOSClick
92 | end
93 | object RadioButtonAndroid: TRadioButton
94 | Left = 8
95 | Top = 199
96 | Width = 113
97 | Height = 17
98 | Caption = 'Android'
99 | TabOrder = 7
100 | OnClick = RadioButtonAndroidClick
101 | end
102 | object EditAndroidAPIKey: TEdit
103 | Left = 24
104 | Top = 243
105 | Width = 322
106 | Height = 21
107 | TabOrder = 6
108 | end
109 | object EditAPNSTopic: TEdit
110 | Left = 24
111 | Top = 67
112 | Width = 322
113 | Height = 21
114 | TabOrder = 1
115 | Text = 'com.mycool.app'
116 | end
117 | object EditAPNSCertificate: TEdit
118 | Left = 24
119 | Top = 112
120 | Width = 233
121 | Height = 21
122 | TabOrder = 2
123 | end
124 | object EditAPNSKey: TEdit
125 | Left = 24
126 | Top = 160
127 | Width = 233
128 | Height = 21
129 | TabOrder = 4
130 | end
131 | object ButtonBrowseCertificate: TButton
132 | Left = 271
133 | Top = 110
134 | Width = 75
135 | Height = 25
136 | Caption = 'Browse Cert'
137 | TabOrder = 3
138 | OnClick = ButtonBrowseCertificateClick
139 | end
140 | object ButtonBrowseKey: TButton
141 | Left = 271
142 | Top = 158
143 | Width = 75
144 | Height = 25
145 | Caption = 'Browse Key'
146 | TabOrder = 5
147 | OnClick = ButtonBrowseKeyClick
148 | end
149 | end
150 | object EditDeviceToken: TEdit
151 | Left = 416
152 | Top = 38
153 | Width = 409
154 | Height = 21
155 | TabOrder = 1
156 | end
157 | object EditTitle: TEdit
158 | Left = 416
159 | Top = 94
160 | Width = 409
161 | Height = 21
162 | TabOrder = 2
163 | Text = 'My Title'
164 | end
165 | object MemoMessage: TMemo
166 | Left = 416
167 | Top = 150
168 | Width = 409
169 | Height = 130
170 | Lines.Strings = (
171 | 'My Message')
172 | TabOrder = 3
173 | end
174 | object OpenDialog1: TOpenDialog
175 | Filter = '*.pem'
176 | Left = 336
177 | Top = 8
178 | end
179 | end
180 |
--------------------------------------------------------------------------------
/Example.Windows/FMain.pas:
--------------------------------------------------------------------------------
1 | unit FMain;
2 |
3 | interface
4 |
5 | uses
6 | Winapi.Windows,
7 | Winapi.Messages,
8 | System.SysUtils,
9 | System.Variants,
10 | System.Classes,
11 | System.IOUtils,
12 | Vcl.Graphics,
13 | Vcl.Controls,
14 | Vcl.Forms,
15 | Vcl.Dialogs,
16 | Vcl.StdCtrls,
17 | Grijjy.RemotePush.Sender;
18 |
19 | type
20 | TFormMain = class(TForm)
21 | ButtonSend: TButton;
22 | GroupBoxPlatform: TGroupBox;
23 | RadioButtoniOS: TRadioButton;
24 | RadioButtonAndroid: TRadioButton;
25 | EditDeviceToken: TEdit;
26 | LabelDeviceToken: TLabel;
27 | EditTitle: TEdit;
28 | LabelTitle: TLabel;
29 | LabelMessage: TLabel;
30 | MemoMessage: TMemo;
31 | EditAndroidAPIKey: TEdit;
32 | LabelAndroidAPIKey: TLabel;
33 | EditAPNSTopic: TEdit;
34 | LabelAPNSTopic: TLabel;
35 | EditAPNSCertificate: TEdit;
36 | LabelAPNSCertificate: TLabel;
37 | EditAPNSKey: TEdit;
38 | LabelAPNSKey: TLabel;
39 | ButtonBrowseCertificate: TButton;
40 | ButtonBrowseKey: TButton;
41 | OpenDialog1: TOpenDialog;
42 | procedure ButtonSendClick(Sender: TObject);
43 | procedure ButtonBrowseCertificateClick(Sender: TObject);
44 | procedure ButtonBrowseKeyClick(Sender: TObject);
45 | procedure RadioButtoniOSClick(Sender: TObject);
46 | procedure RadioButtonAndroidClick(Sender: TObject);
47 | procedure FormShow(Sender: TObject);
48 | private
49 | procedure UpdateControls;
50 | { Private declarations }
51 | public
52 | { Public declarations }
53 | end;
54 |
55 | var
56 | FormMain: TFormMain;
57 |
58 | implementation
59 |
60 | {$R *.dfm}
61 |
62 | procedure TFormMain.UpdateControls;
63 | begin
64 | EditAPNSTopic.Enabled := RadioButtoniOS.Checked;
65 | EditAPNSCertificate.Enabled := RadioButtoniOS.Checked;
66 | EditAPNSKey.Enabled := RadioButtoniOS.Checked;
67 | ButtonBrowseCertificate.Enabled := RadioButtoniOS.Checked;
68 | ButtonBrowseKey.Enabled := RadioButtoniOS.Checked;
69 | EditAndroidAPIKey.Enabled := RadioButtonAndroid.Checked;
70 | end;
71 |
72 | procedure TFormMain.FormShow(Sender: TObject);
73 | begin
74 | UpdateControls;
75 | end;
76 |
77 | procedure TFormMain.RadioButtonAndroidClick(Sender: TObject);
78 | begin
79 | UpdateControls;
80 | end;
81 |
82 | procedure TFormMain.RadioButtoniOSClick(Sender: TObject);
83 | begin
84 | UpdateControls;
85 | end;
86 |
87 | procedure TFormMain.ButtonBrowseCertificateClick(Sender: TObject);
88 | begin
89 | if OpenDialog1.Execute then
90 | EditAPNSCertificate.Text := OpenDialog1.FileName;
91 | end;
92 |
93 | procedure TFormMain.ButtonBrowseKeyClick(Sender: TObject);
94 | begin
95 | if OpenDialog1.Execute then
96 | EditAPNSKey.Text := OpenDialog1.FileName;
97 | end;
98 |
99 | procedure TFormMain.ButtonSendClick(Sender: TObject);
100 | var
101 | RemotePushSender: TgoRemotePushSender;
102 | APlatform: TOSVersion.TPlatform;
103 | begin
104 | RemotePushSender := TgoRemotePushSender.Create;
105 | try
106 | if RadioButtoniOS.Checked then
107 | begin
108 | APlatform := TOSVersion.TPlatform.pfiOS;
109 | RemotePushSender.APNSTopic := EditAPNSTopic.Text;
110 | RemotePushSender.APNSCertificate := TFile.ReadAllBytes(EditAPNSCertificate.Text);
111 | RemotePushSender.APNSKey := TFile.ReadAllBytes(EditAPNSKey.Text);
112 | end
113 | else
114 | begin
115 | APlatform := TOSVersion.TPlatform.pfAndroid;
116 | RemotePushSender.AndroidAPIKey := EditAndroidAPIKey.Text;
117 | end;
118 | RemotePushSender.Send(
119 | APlatform,
120 | EditDeviceToken.Text,
121 | EditTitle.Text,
122 | MemoMessage.Text);
123 | finally
124 | RemotePushSender.Free;
125 | end;
126 | end;
127 |
128 | end.
129 |
--------------------------------------------------------------------------------
/License.txt:
--------------------------------------------------------------------------------
1 | TgoRemotePushSender and DelphiRemotePushSender are licensed under the Simplified BSD License.
2 |
3 | -------------------------------------------------------------------------------
4 |
5 | Copyright (c) 2017 by Grijjy, Inc.
6 | All rights reserved.
7 |
8 | Redistribution and use in source and binary forms, with or without
9 | modification, are permitted provided that the following conditions are met:
10 |
11 | 1. Redistributions of source code must retain the above copyright notice, this
12 | list of conditions and the following disclaimer.
13 | 2. Redistributions in binary form must reproduce the above copyright notice,
14 | this list of conditions and the following disclaimer in the documentation
15 | and/or other materials provided with the distribution.
16 |
17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
18 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
21 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
24 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
26 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
/Nghttp2.pas:
--------------------------------------------------------------------------------
1 | unit Nghttp2;
2 |
3 | { Header translation of ngHttp library for HTTP/2 protocol support, see https://nghttp2.org }
4 |
5 | interface
6 |
7 | const
8 | {$IF Defined(MSWINDOWS)}
9 | NGHTTP2_LIB = 'nghttp2.dll';
10 | {$ELSEIF Defined(LINUX)}
11 | NGHTTP2_LIB = 'libnghttp2.so';
12 | {$ENDIF}
13 |
14 | const
15 | NGHTTP2_NO_ERROR = 0;
16 |
17 | NGHTTP2_ERR_CALLBACK_FAILURE = -902;
18 |
19 | NGHTTP2_DATA = 0;
20 | _NGHTTP2_HEADERS = 1;
21 |
22 | NGHTTP2_HCAT_REQUEST = 0;
23 | NGHTTP2_HCAT_RESPONSE = 1;
24 |
25 | NGHTTP2_NV_FLAG_NONE = 0;
26 | NGHTTP2_NV_FLAG_NO_INDEX = 1;
27 | NGHTTP2_NV_FLAG_NO_COPY_NAME = 2;
28 | NGHTTP2_NV_FLAG_NO_COPY_VALUE = 4;
29 |
30 | NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS = 3;
31 |
32 | NGHTTP2_FLAG_NONE = 0;
33 |
34 | NGHTTP2_DATA_FLAG_NONE = 0;
35 | NGHTTP2_DATA_FLAG_EOF = 1;
36 |
37 | type
38 | size_t = NativeUInt;
39 | ssize_t = NativeInt;
40 | puint8 = ^uint8;
41 | puint32 = ^uint32;
42 |
43 | type
44 | { The primary structure to hold the resources needed for a HTTP/2
45 | session. The details of this structure are intentionally hidden
46 | from the public API. }
47 | pnghttp2_session = Pointer;
48 |
49 |
{ Callback functions for :type:`nghttp2_session`. The details of
50 |
this structure are intentionally hidden from the public API. }
51 | pnghttp2_session_callbacks = Pointer;
52 |
53 | { The name/value pair, which mainly used to represent header fields. }
54 | nghttp2_nv = record
55 | name : MarshaledAString;
56 | value: MarshaledAString;
57 | namelen: size_t;
58 | valuelen: size_t;
59 | flags: uint8;
60 | end;
61 | pnghttp2_nv = ^nghttp2_nv;
62 |
63 | { The stream ID of the stream to depend on. Specifying 0 makes stream
64 | not depend any other stream. }
65 | nghttp2_priority_spec = record
66 | stream_id: int32;
67 | weight: int32;
68 | exclusive: uint8;
69 | end;
70 | pnghttp2_priority_spec = ^nghttp2_priority_spec;
71 |
72 | { The SETTINGS ID/Value pair }
73 | nghttp2_settings_entry = record
74 | settings_id: int32;
75 | value: uint32;
76 | end;
77 |
pnghttp2_settings_entry = ^nghttp2_settings_entry;
78 |
79 |
{ The frame header }
80 |
nghttp2_frame_hd = record
81 |
length: size_t;
82 | stream_id: int32;
83 | &type: uint8;
84 | flags: uint8;
85 | reserved: uint8;
86 | end;
87 |
88 | { The HEADERS frame }
89 | nghttp2_headers = record
90 | hd: nghttp2_frame_hd;
91 | padlen: size_t;
92 | pri_spec: nghttp2_priority_spec;
93 | nva: pnghttp2_nv;
94 | nvlen: size_t;
95 | cat: Integer; { enum }
96 | end;
97 |
98 | { This union includes all frames to pass them to various function
99 |
calls as nghttp2_frame type. }
100 |
nghttp2_frame = record
101 |
case Integer of
102 | 0:(hd: nghttp2_frame_hd);
103 | 1:(headers: nghttp2_headers);
104 | // nghttp2_frame_hd hd;
105 | // nghttp2_data data;
106 | // nghttp2_headers headers;
107 | // nghttp2_priority priority;
108 | // nghttp2_rst_stream rst_stream;
109 | // nghttp2_settings settings;
110 | // nghttp2_push_promise push_promise;
111 | // nghttp2_ping ping;
112 | // nghttp2_goaway goaway;
113 | // nghttp2_window_update window_update;
114 | // nghttp2_extension ext;
115 | end;
116 | pnghttp2_frame = ^nghttp2_frame;
117 |
118 |
type
119 | { nghttp2_data_source_read_callback }
120 |
121 | { This union represents the some kind of data source passed to
122 | :type:`nghttp2_data_source_read_callback` }
123 | nghttp2_data_source = record
124 | case Integer of
125 | 0:(fd: Integer);
126 | 1:(ptr: Pointer);
127 | end;
128 | pnghttp2_data_source = ^nghttp2_data_source;
129 |
130 | { Callback function invoked when the library wants to read data from
131 | the |source|. The read data is sent in the stream |stream_id|.
132 | The implementation of this function must read at most |length|
133 | bytes of data from |source| (or possibly other places) and store
134 | them in |buf| and return number of data stored in |buf|. If EOF is
135 | reached, set :enum:`NGHTTP2_DATA_FLAG_EOF` flag in |*data_flags|. }
136 | nghttp2_data_source_read_callback = function(session: pnghttp2_session;
137 | stream_id: int32; buf: puint8; length: size_t;
138 | data_flags: puint32; source: pnghttp2_data_source; user_data: Pointer): ssize_t; cdecl;
139 |
140 | { This struct represents the data source and the way to read a chunk
141 | of data from it. }
142 | nghttp2_data_provider = record
143 | source: nghttp2_data_source;
144 | read_callback: nghttp2_data_source_read_callback;
145 | end;
146 | pnghttp2_data_provider = ^nghttp2_data_provider;
147 |
148 | type
149 | { nghttp2_on_header_callback }
150 |
151 | { Callback function invoked when a header name/value pair is received
152 | for the |frame|. The |name| of length |namelen| is header name.
153 | The |value| of length |valuelen| is header value. The |flags| is
154 | bitwise OR of one or more of :type:`nghttp2_nv_flag`. }
155 | nghttp2_on_header_callback = function(session: pnghttp2_session; const frame: pnghttp2_frame;
156 | const name: puint8; namelen: size_t; const value: puint8; valuelen: size_t;
157 | flags: uint8; user_data: Pointer): Integer; cdecl;
158 |
159 | type
160 | { nghttp2_on_frame_recv_callback }
161 |
162 | { Callback function invoked by `nghttp2_session_recv()` and
163 | `nghttp2_session_mem_recv()` when a frame is received. The
164 | |user_data| pointer is the third argument passed in to the call to
165 | `nghttp2_session_client_new()` or `nghttp2_session_server_new()`. }
166 | nghttp2_on_frame_recv_callback = function(session: pnghttp2_session;
167 | const frame: pnghttp2_frame; user_data: Pointer): Integer; cdecl;
168 |
169 | type
170 | { nghttp2_on_data_chunk_recv_callback }
171 |
172 | { Callback function invoked when a chunk of data in DATA frame is
173 | received. The |stream_id| is the stream ID this DATA frame belongs to. }
174 | nghttp2_on_data_chunk_recv_callback = function(session: pnghttp2_session;
175 | flags: uint8; stream_id: int32; const data: puint8; len: size_t;
176 | user_data: Pointer): Integer; cdecl;
177 |
178 | type
179 | { nghttp2_on_stream_close_callback }
180 |
181 | { Callback function invoked when the stream |stream_id| is closed.
182 | The reason of closure is indicated by the |error_code| }
183 | nghttp2_on_stream_close_callback = function(session: pnghttp2_session;
184 | stream_id: int32; error_code: uint32; user_data: Pointer): Integer; cdecl;
185 |
186 | var
187 | { Stores local settings and submits SETTINGS frame. The |iv| is the
188 | pointer to the array of :type:`nghttp2_settings_entry`. The |niv|
189 | indicates the number of :type:`nghttp2_settings_entry`. }
190 | nghttp2_submit_settings: function(session: pnghttp2_session; flags: uint8;
191 | const iv: pnghttp2_settings_entry; niv: size_t): Integer; cdecl = nil;
192 |
193 | { Initializes |*session_ptr| for client use. The all members of |callbacks|
194 | are copied to |*session_ptr|. Therefore |*session_ptr| does not store
195 | |callbacks|. The |user_data| is an arbitrary user supplied data, which
196 | will be passed to the callback functions. }
197 | nghttp2_session_client_new: function(var session_ptr: pnghttp2_session;
198 | const callbacks: pnghttp2_session_callbacks; user_data: Pointer): Integer; cdecl = nil;
199 |
200 | { Initializes |*callbacks_ptr| with NULL values. }
201 | nghttp2_session_callbacks_new: function(out callbacks_ptr: pnghttp2_session_callbacks): Integer; cdecl = nil;
202 |
203 | { Frees any resources allocated for |callbacks|. If |callbacks| is ``NULL``, this
204 | function does nothing. }
205 | nghttp2_session_callbacks_del: procedure(callbacks: pnghttp2_session_callbacks); cdecl = nil;
206 |
207 | { Sets callback function invoked when a header name/value pair is received. }
208 | nghttp2_session_callbacks_set_on_header_callback: procedure(callbacks: pnghttp2_session_callbacks;
209 | on_header_callback: nghttp2_on_header_callback); cdecl = nil;
210 |
211 | { Sets callback function invoked by `nghttp2_session_recv()` and `nghttp2_session_mem_recv()`
212 | when a frame is received. }
213 | nghttp2_session_callbacks_set_on_frame_recv_callback: procedure(callbacks: pnghttp2_session_callbacks;
214 | on_frame_recv_callback: nghttp2_on_frame_recv_callback); cdecl = nil;
215 |
216 | { Sets callback function invoked when a chunk of data in DATA frame is received. }
217 | nghttp2_session_callbacks_set_on_data_chunk_recv_callback: procedure(callbacks: pnghttp2_session_callbacks;
218 | on_data_chunk_recv_callback: nghttp2_on_data_chunk_recv_callback); cdecl = nil;
219 |
220 | { Sets callback function invoked when the stream is closed. }
221 | nghttp2_session_callbacks_set_on_stream_close_callback: procedure(callbacks: pnghttp2_session_callbacks;
222 | on_stream_close_callback: nghttp2_on_stream_close_callback); cdecl = nil;
223 |
224 | { Signals the session so that the connection should be terminated. }
225 | nghttp2_session_terminate_session: function(session: pnghttp2_session; error_code: uint32): Integer; cdecl = nil;
226 |
227 | { Submits HEADERS frame and optionally one or more DATA frames. }
228 | nghttp2_submit_request: function(session: pnghttp2_session; const pri_spec: pnghttp2_priority_spec;
229 | const nva: pnghttp2_nv; nvlen: size_t; const data_prd: pnghttp2_data_provider; stream_user_data: Pointer): int32; cdecl = nil;
230 |
231 | { Returns stream_user_data for the stream |stream_id|. }
232 | nghttp2_session_get_stream_user_data: function(session: pnghttp2_session;
233 | stream_id: int32): Pointer; cdecl = nil;
234 |
235 | { Processes data |in| as an input from the remote endpoint. The
236 | |inlen| indicates the number of bytes in the |in|. }
237 | nghttp2_session_mem_recv: function(session: pnghttp2_session; const &in: Pointer; const inlen: size_t): Integer; cdecl = nil;
238 |
239 | { Returns the serialized data to send. }
240 | nghttp2_session_mem_send: function(session: pnghttp2_session; out data_ptr: Pointer): Integer; cdecl = nil;
241 |
242 | { Returns nonzero value if |session| wants to receive data from the
243 | remote peer. If both `nghttp2_session_want_read()` and `nghttp2_session_want_write()`
244 | return 0, the application should drop the connection. }
245 | nghttp2_session_want_read: function(session: pnghttp2_session): Integer; cdecl = nil;
246 |
247 | { Returns nonzero value if |session| wants to send data to the remote
248 | peer. If both `nghttp2_session_want_read()` and * `nghttp2_session_want_write()`
249 | return 0, the application should drop the connection. }
250 | nghttp2_session_want_write: function(session: pnghttp2_session): Integer; cdecl = nil;
251 |
252 | //function MAKE_NV2(name, value: MarshaledAString): nghttp2_nv;
253 | //function MAKE_NV(name, value: MarshaledAString; valuelen: uint8): nghttp2_nv;
254 |
255 | implementation
256 |
257 | uses
258 | Classes,
259 | {$IFDEF MSWINDOWS}
260 | Windows,
261 | {$ENDIF}
262 | SyncObjs,
263 | SysUtils;
264 |
265 | var
266 | NGHTTP2Handle: HMODULE;
267 |
268 | { Helpers }
269 |
270 | //function MAKE_NV2(name, value: MarshaledAString): nghttp2_nv;
271 | //begin
272 | // Result.name := name;
273 | // Result.value := value;
274 | // Result.namelen := StrLen(name);
275 | // Result.valuelen := StrLen(value);
276 | // Result.flags := NGHTTP2_NV_FLAG_NONE;
277 | //end;
278 | //
279 | //function MAKE_NV(name, value: MarshaledAString; valuelen: uint8): nghttp2_nv;
280 | //begin
281 | // Result.name := name;
282 | // Result.value := value;
283 | // Result.namelen := StrLen(name);
284 | // Result.valuelen := valuelen;
285 | // Result.flags := NGHTTP2_NV_FLAG_NONE;
286 | //end;
287 |
288 | { Library }
289 |
290 | function LoadLib(const ALibFile: String): HMODULE;
291 | begin
292 | Result := LoadLibrary(PChar(ALibFile));
293 | if (Result = 0) then
294 | raise Exception.CreateFmt('load %s failed', [ALibFile]);
295 | end;
296 |
297 | function FreeLib(ALibModule: HMODULE): Boolean;
298 | begin
299 | Result := FreeLibrary(ALibModule);
300 | end;
301 |
302 | function GetProc(AModule: HMODULE; const AProcName: String): Pointer;
303 | begin
304 | Result := GetProcAddress(AModule, PChar(AProcName));
305 | if (Result = nil) then
306 | raise Exception.CreateFmt('%s is not found', [AProcName]);
307 | end;
308 |
309 | procedure LoadNGHTTP2;
310 | begin
311 | if (NGHTTP2Handle <> 0) then Exit;
312 | NGHTTP2Handle := LoadLib(NGHTTP2_LIB);
313 | if (NGHTTP2Handle = 0) then
314 | begin
315 | raise Exception.CreateFmt('Load %s failed', [NGHTTP2_LIB]);
316 | Exit;
317 | end;
318 |
319 | nghttp2_submit_settings := GetProc(NGHTTP2Handle, 'nghttp2_submit_settings');
320 | nghttp2_session_client_new := GetProc(NGHTTP2Handle, 'nghttp2_session_client_new');
321 | nghttp2_session_callbacks_new := GetProc(NGHTTP2Handle, 'nghttp2_session_callbacks_new');
322 | nghttp2_session_callbacks_del := GetProc(NGHTTP2Handle, 'nghttp2_session_callbacks_del');
323 | nghttp2_session_callbacks_set_on_header_callback := GetProc(NGHTTP2Handle, 'nghttp2_session_callbacks_set_on_header_callback');
324 | nghttp2_session_callbacks_set_on_frame_recv_callback := GetProc(NGHTTP2Handle, 'nghttp2_session_callbacks_set_on_frame_recv_callback');
325 | nghttp2_session_callbacks_set_on_data_chunk_recv_callback := GetProc(NGHTTP2Handle, 'nghttp2_session_callbacks_set_on_data_chunk_recv_callback');
326 | nghttp2_session_callbacks_set_on_stream_close_callback := GetProc(NGHTTP2Handle, 'nghttp2_session_callbacks_set_on_stream_close_callback');
327 | nghttp2_session_terminate_session := GetProc(NGHTTP2Handle, 'nghttp2_session_terminate_session');
328 |
329 | nghttp2_submit_request := GetProc(NGHTTP2Handle, 'nghttp2_submit_request');
330 |
nghttp2_session_get_stream_user_data := GetProc(NGHTTP2Handle, 'nghttp2_session_get_stream_user_data');
331 |
332 |
nghttp2_session_mem_recv := GetProc(NGHTTP2Handle, 'nghttp2_session_mem_recv');
333 | nghttp2_session_mem_send := GetProc(NGHTTP2Handle, 'nghttp2_session_mem_send');
334 | nghttp2_session_want_read := GetProc(NGHTTP2Handle, 'nghttp2_session_want_read');
335 | nghttp2_session_want_write := GetProc(NGHTTP2Handle, 'nghttp2_session_want_write');
336 | end;
337 |
338 | procedure UnloadNGHTTP2;
339 | begin
340 | if (NGHTTP2Handle = 0) then Exit;
341 | FreeLib(NGHTTP2Handle);
342 | NGHTTP2Handle := 0;
343 | end;
344 |
345 | initialization
346 | LoadNGHTTP2;
347 |
348 | finalization
349 | UnloadNGHTTP2;
350 |
351 | end.
352 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Sending iOS (and Android) remote push notifications from your Delphi service with the HTTP/2 protocol
2 |
3 | In this article we are going to discuss how to make a base unifying class for sending remote push notifications from your service to both the iOS (Apple Push Notification Service) and Android (Firebase Cloud Messaging) as well as covering how to use the newer Apple HTTP/2 protocol interface for sending push notifications from Delphi.
4 |
5 | # Introduction
6 |
7 | If you are authoring a Delphi service for the cloud, you may have the need to send push notifications from your service to your client application. There are various ways to accomplish this including using third-party BAAS providers or rolling your own interface with some of the established libraries.
8 |
9 | In the past Apple used a custom TCP binary protocol to send messages to the APNS. This was cumbersome and inflexible and had numerous pitfalls in determining whether your message was actually accepted by the interface. Apple recently released a newer interface that uses HTTP/2. The nice thing about this interface is, just like FCM it utilizes JSON for the payload of the protocol. However, the protocol uses the newer HTTP/2 protocol and ALPN (Application-Layer Protocol Negotiation Extension, defined in RFC 7301) which is required in your SSL handshake.
10 |
11 | One other advantage, Apple also increased the maximium size of the payload from 2K to 4K with the new HTTP/2 interface as of iOS 9 devices.
12 |
13 | We will show how to combine the [Grijjy scalable client socket library](https://blog.grijjy.com/2017/01/09/scalable-https-and-tcp-client-sockets-for-the-cloud) with the excellent [nghttp2](http://nghttp2.org) to solve both of these issues and transparently send push notifications to either Apple's APNS or Google's FCM from your Delphi service.
14 |
15 | For more information about us, our support and services visit the [Grijjy homepage](http://www.grijjy.com) or the [Grijjy developers blog](http://blog.grijjy.com).
16 |
17 | The example contained here depends upon part of our [Grijjy Foundation library](https://github.com/grijjy/GrijjyFoundation).
18 |
19 | The source code and related example repository is hosted on GitHub at [https://github.com/grijjy/DelphiRemotePushSender](https://github.com/grijjy/DelphiRemotePushSender).
20 |
21 | # nghttp2
22 |
23 | [Nghttp2 is a fairly established library](http://nghttp2.org) for implementing the HTTP/2 protocol. It handles all the compression and bitstreaming for implementing HTTP/2.
24 |
25 | The [HTTP/2 protocol](https://http2.github.io/) and its header compression are rather complex and evolving so it is probably best not to try and implement this ourselves.
26 |
27 | One of the great things about nghttp2 is you can utilize it entirely with memory buffers so it is a good match for our own [scalable client socket classes](https://github.com/grijjy/DelphiScalableClientSockets) for Windows and Linux. This way we get the benefit of HTTP/2 but we don't have to rely on another implementation of sockets for scaling up our service.
28 |
29 | ## Building nghttp2
30 |
31 | If you want to build the nghttp2 library for Windows to use in your Delphi application you will need to download the [latest source](https://github.com/nghttp2/nghttp2). To build you can use Visual Studio or just [download the Visual Studio Build Tools](http://landinghub.visualstudio.com/visual-cpp-build-tools). You will also need to download and install [CMAKE](https://cmake.org/).
32 |
33 | 1. Download the latest nghttp2 source from https://github.com/nghttp2/nghttp2
34 | 2. Install CMAKE and the Build Tools for Visual Studio.
35 | 3. Run CMAKE followed by a period
36 | `cmake .`
37 | 4. Run CMAKE to build the release version
38 | `cmake --build . --config RELEASE`
39 |
40 | ## Delphi header translation
41 | Once completed you will have a nghttp2.dll. We will need our [conversion for the header file for Delphi](https://github.com/grijjy/DelphiRemotePushSender/blob/master/Nghttp2.pas) so we can use the nghttp2 methods directly.
42 |
43 | ## Apple APNS & iOS Prerequisites
44 | To use the `TgoRemotePushSender` class with Apple's Push Notification Service, we need to create a certificate and convert that certificate into 2 PEM files, one for the certificate and one the private key.
45 |
46 | There are quite a few primers on the Internet about how to create APNS certificates, so we will be brief here as it applies to our `TgoRemotePushSender` class.
47 |
48 | 1. On iOS you must create a production certificate that is universal by selecting the option "Apple Push Notification service SSL (Sandbox & Production)" under Certificates in the developer portal. This certificate must be matched with the proper AppId.
49 |
50 | 2. You need to download and install the certificate into the Mac OSX Keychain.
51 |
52 | 3. You need to export the Apple Push Notification service certificate and private key as 2 separate files (cert.p12 and key.p12) from the Keychain application on OSX. These can only be exported as .p12 files from the Keychain application.
53 |
54 | To create PEM files we recommend using the OpenSSL binaries to convert as follows:
55 | ```shell
56 | openssl pkcs12 -clcerts -nokeys -out APNS-cert.pem -in cert.p12
57 | ```
58 | ```shell
59 | openssl pkcs12 -nocerts -out APNS-key-password.pem -in key.p12
60 | ```
61 | To remove the password from the APNS-key-password.pem file:
62 | ```shell
63 | openssl rsa -in APNS-key-password.pem -out APNS-key.pem
64 | ```
65 | To test the certificate against the APNS interface to see if it works:
66 | ```shell
67 | openssl s_client -connect gateway.push.apple.com:2195 -cert APNS-cert.pem -key APNS-key.pem
68 | ```
69 | # TgoRemotePushSender
70 |
71 | To use the `TgoRemotePushSender` class we need to know the Device Token for the target user's device. You pass this value to the `TgoRemotePushSender.Send` method when sending your remote push notification. This is true of both iOS and Android targets.
72 |
73 | For Android you also need to specify the `TgoRemotePushSender.AndroidAPIKey`. You can obtain an API Key by setting up cloud messaging on [Google's developer site](https://developers.google.com/cloud-messaging/). This is also called the Server API Key under Firebase Cloud Messaging.
74 |
75 | For iOS you need to specify the `TgoRemotePushSender.APNSKey` which is essentially your Bundle Identifier (ex: com.mycool.app). You also need to apply the APNS certificate and private key .PEM files you previously created.
76 | ```Delphi
77 | TgoRemotePushSender.APNSKey := 'com.mycool.app';
78 | TgoRemotePushSender.APNSCertificate := TFile.ReadAllBytes(PathToCertificate.PEM);
79 | TgoRemotePushSender.APNSKey := TFile.ReadAllBytes(PathToKey.PEM);
80 | ```
81 |
82 | The structure of the class is fairly straightforward. To send a remote push notification, call the `Send` method and provide the `DeviceToken` and also the `Title` and `Message` body for the payload.
83 |
84 | ```Delphi
85 | TgoRemotePushSender = class(TObject)
86 | public
87 | constructor Create;
88 | destructor Destroy; override;
89 | public
90 | { Send push notification }
91 | function Send(const APlatform: TOSVersion.TPlatform; const ADeviceToken: String;
92 | const ATitle, AMessage: String): Boolean;
93 | public
94 | { Android API Key for your app }
95 | property AndroidAPIKey: String read FAndroidAPIKey write FAndroidAPIKey;
96 |
97 | { iOS Certificate }
98 | property APNSCertificate: TBytes read FAPNSCertificate write FAPNSCertificate;
99 |
100 | { iOS Key }
101 | property APNSKey: TBytes read FAPNSKey write FAPNSKey;
102 |
103 | { iOS Topic }
104 | property APNSTopic: String read FAPNSTopic write FAPNSTopic;
105 | end;
106 | ```
107 |
108 | > Note: You should create and reuse an instance of this class to avoid creating multiple connections to the push notification host. One model would be to perform notifications in batches based upon time. Apple warns about creating too many connections as a reason for blocking your service.
109 |
110 | [An example application](https://github.com/grijjy/DelphiRemotePushSender) demonstrating how to use this class is available within our repository on GitHub.
111 |
112 | # Sending custom user data
113 |
114 | Now that both Apple and Google use JSON as their payload format, you can easily add your own custom JSON fields to the payload that are consistent with both platforms. While we don't expose this directly in our class you can add additional content by simply expanding the JSON with your extra custom data.
115 |
116 | # License
117 |
118 | TgoRemotePushSender and DelphiRemotePushSender are licensed under the Simplified BSD License. See License.txt for details.
--------------------------------------------------------------------------------
/ReceivingPushNotifications.md:
--------------------------------------------------------------------------------
1 | # Receiving push notifications from Apple's Push Notification and Google's Firebase Cloud Messaging services
2 |
3 | In the [previous article](https://blog.grijjy.com/2017/01/18/sending-ios-and-android-remote-push-notifications-from-your-delphi-service-with-the-http2-protocol/) we demonstrated how to roll your own Google Firebase Cloud Messaging and Apple APNS push notifications from Delphi apps in a unified manner. This article continues the conversation about how to easily receive and consume them in your Delphi mobile app.
4 |
5 | # Introduction
6 | If you are authoring a Delphi service for the cloud, you may have the need to send and receive push notifications from your service to your mobile client application. You could use an existing BAAS provider to accomplish this, but why pay for this when you can roll your own so easily with Delphi?
7 |
8 | Delphi provides a base set of classes in the units `FMX.PushNotification.Android` and `FMX.PushNotification.iOS` that do most of the heavy lifting and allow you to easily consume remote push notifications. In this conversation we will be demonstrating a helper class that unifies the approach for both platforms, uses TMessage callbacks for notifications and device token changes and adds some new missing features.
9 |
10 | For more information about us, our support and services visit the [Grijjy homepage](http://www.grijjy.com) or the [Grijjy developers blog](http://blog.grijjy.com).
11 |
12 | The example contained here depends upon part of our [Grijjy Foundation library](https://github.com/grijjy/GrijjyFoundation).
13 |
14 | # Getting started on iOS
15 | To receive push notifications on iOS you must:
16 |
17 | 1. Already have created an [Apple Push Services certificate in the Apple Developer website](https://developer.apple.com/account/ios/certificate/) for use in sending remote push notifications.
18 |
19 | 2. Create an App ID on the Apple Developer website for your project.
20 |
21 | 3. Enable Push Notifications for your App ID on the Apple Developer website for your project.
22 |
23 | 4. A Provisioning Profile must exist on the Apple website that matches the identifier.
24 |
25 | Then in your Delphi Project Options you must set your CFBundleIdentifier for iOS build configurations to the matching name of the ID in Apple Developer website. (ex: com.some.app) This is the only thing your app will need to identify itself for Apple Push Notifications.
26 |
27 | # Getting started on Android
28 | To receive push notifications on Android you must:
29 |
30 | 1. Obtain a Firebase Cloud Messaging API Key by setting up cloud messaging on [Google's developer site](https://developers.google.com/cloud-messaging/). This is also called the Server API Key under Firebase Cloud Messaging.
31 |
32 | 2. Modify your AndroidManifest.xml to register an Intent and add the following receiver:
33 |
34 | ```xml
35 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | <%receivers%>
45 | ```
46 | For Android, we only need the Firebase Cloud Messaging API Key for our Delphi app.
47 |
48 | # TgoRemotePushReceiver class
49 | The [TgoRemotePushReceiver class](https://github.com/grijjy/GrijjyFoundation/blob/master/Grijjy.RemotePush.Receiver.pas) unifies the receiving of remote push notifications from both APNS and Firebase Cloud Messaging into a common class model.
50 | ```Delphi
51 | TgoRemotePushReceiver = class(TObject)
52 | public
53 | constructor Create(const AGCMAppId: UnicodeString);
54 | destructor Destroy; override;
55 | public
56 | property DeviceToken: String read FDeviceToken;
57 | property Number: Integer read GetNumber write SetNumber;
58 | end;
59 | ```
60 |
61 | When you receive a remote push notification message from Android or iOS, the following TMessage is fired:
62 | ```Delphi
63 | TgoRemoteNotificationMessage = class(TMessage)
64 | public
65 | constructor Create(const ADataKey, AJson: String; const AActivated: Boolean);
66 | property DataKey: String read FDataKey;
67 | property Json: String read FJson;
68 |
69 | { Whether message is activated by the user (tapping in it) }
70 | property Activated: Boolean read FActivated;
71 | end;
72 | ```
73 | Since we are receiving a JSON payload for both Android and iOS we can examine the content here. This is useful is we want to include our own custom content in the payload that is app specific. See our [article on sending remote push notifications](https://blog.grijjy.com/2017/01/18/sending-ios-and-android-remote-push-notifications-from-your-delphi-service-with-the-http2-protocol/) for a discussion about customizing the payload.
74 |
75 | If your device token changes on Android or iOS, the following TMessage is fired:
76 | ```Delphi
77 | { Device token change message }
78 | TgoDeviceTokenChangeMessage = class(TMessage)
79 | private
80 | FDeviceToken: String;
81 | public
82 | constructor Create(const ADeviceToken: String);
83 | property DeviceToken: String read FDeviceToken;
84 | end;
85 | ```
86 | The above message is fired initially when you device token is assigned after app startup. Due to the variances in the way device tokens are handled on iOS and Android this event could be immediate or delayed by the operating system.
87 |
88 | As a developer you should also be prepared that the device token could change if your application is reinstalled, the operating system is updated as well as dynamically while your app is active.
89 |
90 | # Apple APNS quirks
91 | On iOS Remote Push notification events are received only when your application is in the foreground or background. If your application is "force quit" also known as "terminated" then you cannot receive remote push notification events into your app. Instead when the push notification arrives on screen and is pressed your app is launched but without receiving a notification event.
92 |
93 | > Note: You only receive a single remote notification event at a time, meaning that if there are several events queued, the only one you receive is the one that the user clicked on in the iOS user interface.
94 |
95 | iOS 8.x introduces a new approach called the PushKit which has the ability to silently launch your app into the background state. We will explore this in a future article.
96 |
97 | # Hello World for push notifications
98 | To get started using the helper class, you will need your Android API Key for Firebase Cloud Messaging.
99 |
100 | ```Delphi
101 | RemotePushReceiver := TgrRemotePushReceiver.Create(MyAndroidApiKey);
102 | ```
103 | To subscribe to remote push notifications:
104 | ```Delphi
105 | TMessageManager.DefaultManager.SubscribeToMessage(TgoRemoteNotificationMessage, RemoteNotificationHandler);
106 | ```
107 |
108 | ```Delphi
109 | procedure TMyClass.RemoteNotificationHandler(const Sender: TObject;
110 | const Msg: TMessage);
111 | var
112 | RemoteNotificationMessage: TgrRemoteNotificationMessage;
113 | Doc: TgoBsonDocument;
114 | begin
115 | Assert(Assigned(Msg));
116 | Assert(Msg is TgoRemoteNotificationMessage);
117 | RemoteNotificationMessage := Msg as TgoRemoteNotificationMessage;
118 | { The user tapped this push notification, which resulted in the bringing the app to the
119 | foreground? }
120 | if (RemoteNotificationMessage.Activated) then
121 | Doc := TgrBsonDocument.Parse(RemoteNotificationMessage.Json);
122 | end;
123 | ```
124 | You will receive an Activated set to True if the notification is the likely result of a end-user actually pressing the notification.
125 |
126 | To subscribe to device token changes:
127 | ```Delphi
128 | TMessageManager.DefaultManager.SubscribeToMessage(TgoDeviceTokenChangeMessage, DeviceTokenChangeListener);
129 | ```
130 |
131 | ```Delphi
132 | procedure TMyClass.DeviceTokenChangeListener(const Sender: TObject;
133 | const M: TMessage);
134 | var
135 | DeviceTokenChangeMessage: TgoDeviceTokenChangeMessage;
136 | begin
137 | DeviceTokenChangeMessage := M as TgoDeviceTokenChangeMessage;
138 | Writeln('Device Token is ' + DeviceTokenChangeMessage.DeviceToken);
139 | end;
140 | ```
141 |
142 | > Make sure you unsubscribe all the various TMessage listeners when you are completed.
143 |
144 | # Conclusion
145 | Receiving remote push notifications in Delphi apps for Android and iOS is relatively straightforward with this helper class. In the end, all you need to know is the device token in order to send a message to a device.
146 |
147 | # License
148 |
149 | TgoRemotePushReceiver is licensed under the Simplified BSD License. See License.txt for details.
--------------------------------------------------------------------------------