91aff1ab310fc6c9bb70dc6fe7a6190ffaea7d5f
[platform/framework/native/appfw.git] / src / server / app / FApp_AulServer.cpp
1 //
2 // Copyright (c) 2012 Samsung Electronics Co., Ltd.
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 /**
18  * @file         FApp_AulServer.cpp
19  * @brief       This is the implementation for the _AulServer.cpp class.
20  */
21 #include <cstdio>
22 #include <cstdlib>
23 #include <new>
24 #include <unistd.h>
25 #include <sys/prctl.h>
26 #include <signal.h>
27 #include <unique_ptr.h>
28
29 #include <aul.h>
30 #include <bundle.h>
31 #include <appsvc/appsvc.h>
32 #include <heynoti.h>
33
34 #include <FBaseObject.h>
35 #include <FBaseString.h>
36 #include <FBaseUtil.h>
37 #include <FBaseSysLog.h>
38 #include <FBaseColHashMapT.h>
39 #include <FAppPkgPackageInfo.h>
40
41 #include <FBaseRt_Process.h>
42 #include <FBase_StringConverter.h>
43 #include "FAppPkg_PackageManagerImpl.h"
44 #include "FApp_Types.h"
45 #include "FApp_AulServer.h"
46 #include "FApp_TemplateUtil.h"
47
48 #ifdef __cplusplus
49 extern "C" {
50 #endif
51 extern int aul_listen_app_dead_signal(int (* func)(int, void*), void* data);
52 #ifdef __cplusplus
53 }
54 #endif
55
56 using namespace Tizen::App::Package;
57 using namespace Tizen::Base;
58 using namespace Tizen::Base::Collection;
59 using namespace Tizen::Base::Runtime;
60 using namespace Tizen::Base::Utility;
61
62 namespace
63 {
64
65 const char _DESKTOP_FILE_PATH[] = "/opt/share/applications";
66 const char _DESKTOP_FILE_PATH_FORMAT[] = "%s/%s.desktop";
67
68 const char _X_TIZEN_SVC[] = "x-tizen-svc"; //X-TIZEN-SVC=[operation1] | [URI1] | [MIME1] ; [operation2] | [URI2] | [MIME2]
69 const int _MAX_TIZEN_SVC_DESC_LEN = 1024;
70
71 const int _MAX_CATEGORY = 12;
72 const int _MAX_PACKAGE_ID_LENGTH = 10;
73
74 const char _APP_PATH_FORMAT[] = "/opt/usr/apps/0000000000/bin/%ls";
75 const char _APP_PATH_FORMAT2[] = "/opt/apps/0000000000/bin/%ls";
76 const char PATH_ROOT[] = "/opt/usr/apps/";
77 const char PATH_ROOT2[] = "/opt/apps/";
78
79 }
80
81 namespace Tizen { namespace App
82 {
83
84 struct _CategoryList
85 {
86         const char category[_MAX_CATEGORY];
87         _AppType type;
88 };
89
90 static const _CategoryList _CATEGORY_LIST[] =
91 {
92         {"home-screen", _APP_TYPE_HOME_APP},
93         {"lock-screen", _APP_TYPE_LOCK_APP},
94         {"ime", _APP_TYPE_IME_APP},
95 };
96
97 static const int _NUM_CATEGORY = sizeof(_CATEGORY_LIST) / sizeof(_CategoryList);
98
99
100 result
101 _AulServer::SendResult(bundle* b, appsvc_result_val res)
102 {
103         result r = E_SUCCESS;
104
105         int ret = appsvc_send_result(b, res);
106
107         switch (ret)
108         {
109         case AUL_R_EINVAL:
110                 r = E_INVALID_ARG;
111                 SysLogException(NID_APP, r, "Invalid result bundle.");
112                 break;
113
114         case AUL_R_ECOMM:
115                 r = E_SYSTEM;
116                 SysLogException(NID_APP, r, "Internal IPC error.");
117                 break;
118
119         case AUL_R_ERROR:
120                 r = E_SYSTEM;
121                 SysLogException(NID_APP, r, "General error.");
122                 break;
123
124         default:
125                 break;
126         }
127
128         SysLog(NID_APP, "SendResult() ok");
129
130         return r;
131 }
132
133
134 bool
135 _AulServer::IsRunning(const AppId& appId, const String& exeName)
136 {
137         char slpPackageName[MAX_SLP_PACKAGE_ID] = {0, };
138
139         _PackageManagerImpl* pPackageManagerImpl = _PackageManagerImpl::GetInstance();
140         SysTryReturn(NID_APP, pPackageManagerImpl != null, false, E_INVALID_STATE, "[E_INVALID_STATE] Invalid package instance.");
141
142         pPackageManagerImpl->GetPackageName(appId, &exeName, slpPackageName, MAX_SLP_PACKAGE_ID);
143
144         const bool isRunning = (aul_app_is_running(slpPackageName) > 0);
145
146         SysLog(NID_APP, "'%s' %s running now.", slpPackageName, (isRunning) ? "is" : "is NOT");
147
148         return isRunning;
149 }
150
151 bool
152 _AulServer::IsRunning(const String& packageName)
153 {
154         std::unique_ptr<char[]> pPackageId(_StringConverter::CopyToCharArrayN(packageName));
155
156         const bool isRunning = (aul_app_is_running(pPackageId.get()) > 0);
157
158         SysLog(NID_APP, "'%ls' %s running now.", packageName.GetPointer(), (isRunning) ? "is" : "is NOT");
159         return isRunning;
160 }
161
162
163 void
164 _AulServer::SetOnAppTerminatedCb(int (* pf_app_dead_handler)(int pid, void* pData), void* pData)
165 {
166         aul_listen_app_dead_signal(pf_app_dead_handler, pData);
167         SysLog(NID_APP, "'app_dead_handler is set.");
168 }
169
170
171 result
172 _AulServer::TerminateApplicationByPid(int pid)
173 {
174         int ret_aul = aul_subapp_terminate_request_pid(pid);
175         result r = E_SUCCESS;
176
177         switch (ret_aul)
178         {
179         case AUL_R_EINVAL:
180                 r = E_INVALID_ARG;
181                 SysLogException(NID_APP, r, "invaild pid.\n");
182                 break;
183
184         case AUL_R_ECOMM:
185                 r = E_SYSTEM;
186                 SysLogException(NID_APP, r, "internal AUL IPC error.\n");
187                 break;
188
189         case AUL_R_ERROR:
190                 r = E_SYSTEM;
191                 SysLogException(NID_APP, r, "general error.\n");
192                 break;
193
194         default:
195                 SysLog(NID_APP, "'%d' is terminated.", pid);
196                 break;
197         }
198
199         return r;
200 }
201
202 result
203 _AulServer::SetOomAdj(int pid, int adj)
204 {
205         // set oom_adj to -17 for system service
206         result r = E_SUCCESS;
207         char buf[FILENAME_MAX];
208         FILE *fP = NULL;
209
210         snprintf(buf, FILENAME_MAX, "/proc/%d/oom_adj", pid);
211         fP = fopen(buf, "w");
212         SysTryReturnResult(NID_APP, fP != NULL, E_SYSTEM, "oom_adj change failed with %s.", strerror(errno));
213
214         fprintf(fP, "%d", adj);
215         fclose(fP);
216
217         return r;
218 }
219
220 result
221 _AulServer::SetPowerOffNotiListener( void (*powerOffCb)(void *pData), void *pData)
222 {
223         int heyFd = heynoti_init();
224         SysTryReturnResult(NID_APP, heyFd >= 0, E_SYSTEM, "heynoti_init failed.");
225
226         int ret = heynoti_subscribe(heyFd, "power_off_start", powerOffCb, pData);
227         SysTryReturnResult(NID_APP, ret >= 0, E_SYSTEM, "heynoti_subscribe failed.");
228
229         ret = heynoti_attach_handler(heyFd);
230         SysTryReturnResult(NID_APP, ret >= 0, E_SYSTEM, "heynoti_attach_handler failed.");
231
232         return E_SUCCESS;
233 }
234
235 int
236 _AulServer::GetAppType(const String& category)
237 {
238         int ret = 0;
239
240         HashMapT<String, int> map;
241         map.Construct();
242
243         StringTokenizer strTok(category, L';');
244
245         String token;
246         while (strTok.HasMoreTokens())
247         {
248                 result r = strTok.GetNextToken(token);
249                 if (r == E_SUCCESS)
250                 {
251                         map.Add(token, 0);
252                 }
253         }
254
255         SysLog(NID_APP, "%d category items .", map.GetCount());
256
257         String key;
258
259         for (int i = 0; i < _NUM_CATEGORY; i++)
260         {
261                 bool b = false;
262                 key = _CATEGORY_LIST[i].category;
263                 result r = map.ContainsKey(key, b);
264                 if (r == E_SUCCESS && b)
265                 {
266                         ret |= _CATEGORY_LIST[i].type;
267                 }
268         }
269
270         return ret;
271 }
272
273 bool _AulServer::IsInstalled(const AppId& appId)
274 {
275         String packageId;
276         packageId = _PackageManagerImpl::GetPackageIdByAppId(appId);
277
278         return _PackageManagerImpl::GetInstance()->IsPackageInstalled(packageId);
279 }
280
281 int _AulServer::CreateProcess(const AppId& appId)
282 {
283         SysTryReturn(NID_APP, appId.GetLength() > 10 && appId[10] == L'.', -1, E_INVALID_ARG, "[E_INVALID_ARG] Wrong appId %ls.", appId.GetPointer());
284
285         int pid = fork();
286
287         if (pid == -1)
288         {
289                 return -1;
290         }
291         else if (pid == 0)
292         {
293                 char path[FILENAME_MAX];
294                 memset(path, '\0', FILENAME_MAX);
295
296 #if 0
297                 snprintf(path, FILENAME_MAX, _APP_PATH_FORMAT2, appId.GetPointer() + 11);
298
299                 int ret = wcstombs(path + strlen(PATH_ROOT2), appId.GetPointer(), 10);
300                 if (ret == -1)
301                 {
302                         SysLogException(NID_APP, E_SYSTEM, "Launching service (%ls)(%s) failed with [%s].", appId.GetPointer(), path, strerror(errno));
303                         _Process::Exit(1);
304                 }
305 #else
306                 snprintf(path, FILENAME_MAX, _APP_PATH_FORMAT2, appId.GetPointer() + 11);
307
308                 int ret = wcstombs(path + strlen(PATH_ROOT2), appId.GetPointer(), 10);
309                 if (ret == -1)
310                 {
311                         SysLogException(NID_APP, E_SYSTEM, "Launching service (%ls)(%s) failed with [%s].", appId.GetPointer(), path, strerror(errno));
312                         _Process::Exit(1);
313                 }
314
315                 if (euidaccess(path, R_OK) != 0)
316                 {
317                         snprintf(path, FILENAME_MAX, _APP_PATH_FORMAT, appId.GetPointer() + 11);
318
319                         ret = wcstombs(path + strlen(PATH_ROOT), appId.GetPointer(), 10);
320                         if (ret == -1)
321                         {
322                                 SysLogException(NID_APP, E_SYSTEM, "Launching service (%ls)(%s) failed with [%s].", appId.GetPointer(), path, strerror(errno));
323                                 _Process::Exit(1);
324                         }
325                 }
326 #endif
327
328                 SysLog(NID_APP, "Launching %s.", path);
329
330                 int currentPid = getpid();
331
332                 //SetOomAdj(currentPid, -17); // set oom_adj to -17 for system service
333
334                 prctl(PR_SET_PDEATHSIG, SIGTERM);
335
336                 setpgid(currentPid, currentPid);
337
338                 ret = execl(path, path, static_cast<char*>(NULL));
339                 if (ret < 0)
340                 {
341                         SysLogException(NID_APP, E_SYSTEM, "Launching service (%ls)(%s) failed with [%s].", appId.GetPointer(), path, strerror(errno));
342                         _Process::Exit(1);
343                 }
344         }
345
346         return pid;
347
348 }
349
350 bool
351 _AulServer::IsUserPreferredAppForAppControlResolution(const AppId& appId)
352 {
353         std::unique_ptr<char[]> pAppId(_StringConverter::CopyToCharArrayN(appId));
354
355         int ret = appsvc_is_defapp(pAppId.get());
356         SysTryReturn(NID_APP, ret == 1, false, E_SUCCESS,"%ls is not an UserPreferredAppForAppControlResolution. ret(%d)", appId.GetPointer(), ret);
357         
358         SysLog(NID_APP, "%ls is an UserPreferredAppForAppControlResolution.", appId.GetPointer());      
359         return true;
360 }
361
362 result
363 _AulServer::ClearUserPreferenceForAppControlResolution(const AppId& appId)
364 {
365         std::unique_ptr<char[]> pAppId(_StringConverter::CopyToCharArrayN(appId));
366
367         int ret_aul = appsvc_unset_defapp(pAppId.get());
368         SysTryReturnResult(NID_APP, ret_aul == APPSVC_RET_OK, E_SYSTEM, "Fail to clear UserPreferredAppForAppControlResolution of %ls. ret_aul(%d)", appId.GetPointer(), ret_aul);
369
370         SysLog(NID_APP, "Succeed to clear UserPreferredAppForAppControlResolution of %ls.", appId.GetPointer());
371         return E_SUCCESS;
372 }
373
374 result
375 _AulServer::_DesktopFile::MakePath(const AppId& appId, const String* pExeName, char* path, int size)
376 {
377         SysTryReturnResult(NID_APP, path != null, E_INVALID_ARG, "");
378
379         char packageName[MAX_SLP_PACKAGE_ID] = {0, };
380         result r = E_SUCCESS;
381         _PackageManagerImpl* pPackageManagerImpl = _PackageManagerImpl::GetInstance();
382         SysTryReturnResult(NID_APP, pPackageManagerImpl, E_INVALID_STATE, "Invalid package manager instance.");
383
384         r = pPackageManagerImpl->GetPackageName(appId, pExeName, packageName, MAX_SLP_PACKAGE_ID);
385         SysTryReturn(NID_APP, !IsFailed(r), r, r, "%s", GetErrorMessage(r));
386
387         snprintf(path, size, _DESKTOP_FILE_PATH_FORMAT, _DESKTOP_FILE_PATH, packageName);
388
389         return r;
390 }
391
392 result
393 _AulServer::_DesktopFile::UpdateService(const AppId& appId, const char* value)
394 {
395         char path[FILENAME_MAX] = {0, };
396         MakePath(appId, null, path, FILENAME_MAX);
397
398         return UpdateField(path, _X_TIZEN_SVC, value);
399 }
400
401
402 result
403 _AulServer::_DesktopFile::RemoveService(const AppId& appId, const char* operationOnlyValue)
404 {
405         char path[FILENAME_MAX] = {0, };
406         MakePath(appId, null, path, FILENAME_MAX);
407
408         return UpdateField(path, _X_TIZEN_SVC, operationOnlyValue, true);
409 }
410
411
412 //
413 // Update value of specified field.
414 // currently only "x-slp-svc" field is supported.
415 //
416 #define BUFFER_SIZE 1024
417 result
418 _AulServer::_DesktopFile::UpdateField(const char* path, const char* fieldName, const char* value, bool isRemove)
419 {
420         SysTryReturnResult(NID_APP, path != null, E_INVALID_ARG, "path should not be null.");
421         SysTryReturnResult(NID_APP, fieldName != null, E_INVALID_ARG, "fieldName should not be null.");
422         SysTryReturnResult(NID_APP, value != null, E_INVALID_ARG, "value should not be null.");
423
424         FILE* fp = fopen(path, "r+");
425         SysTryReturnResult(NID_APP, fp != null, E_SYSTEM, "falied to open '%s' due to %s", path, strerror(errno));
426
427         char buffer[BUFFER_SIZE] = {0, };
428         bool found = false;
429         int len = 0;
430         int pos = 0;
431         int foundpos = 0;
432         result r = E_SUCCESS;
433         int remains = 0;
434
435         ArrayListT<char*> buffers;
436         buffers.Construct();
437
438         char* pCurrent = null;
439
440         while (fgets(buffer, BUFFER_SIZE, fp) != NULL)
441         {
442                 len = strlen(buffer);
443                 SysTryCatch(NID_APP, len < BUFFER_SIZE, , r = E_INVALID_ARG, "strlen returns invalid value. (%d)", len );
444
445                 if (found)
446                 {
447                         pCurrent = new (std::nothrow) char[len + 1];
448                         SysTryCatch(NID_APP, pCurrent != null, , r = E_OUT_OF_MEMORY, "failed to allocate mem pCurrent");
449
450                         strncpy(pCurrent, buffer, len);
451                         buffers.Add(pCurrent);
452                 }
453                 else
454                 {
455                         if (strncmp(buffer, fieldName, len) == 0)
456                         {
457                                 int fieldNameLen = strlen(fieldName);
458                                 SysTryCatch(NID_APP, len > fieldNameLen, , E_INVALID_ARG, "[E_INVALID_ARG] fieldName(%s)", fieldName);
459
460                                 pCurrent = UpdateServiceValueN(buffer + fieldNameLen, value, isRemove);
461                                 SysTryCatch(NID_APP, pCurrent != null, , r = GetLastResult(), "[%s] UpdateServiceValue failed", GetErrorMessage(GetLastResult()));
462
463                                 buffers.Add(pCurrent);
464
465                                 foundpos = pos;
466                                 found = true;
467                         }
468                 }
469
470                 pos += len;
471         }
472
473         if (found)
474         {
475                 fsetpos(fp, (fpos_t*) &foundpos);
476
477                 remains = buffers.GetCount();   // prevent infinite loop
478                 while (buffers.GetCount() > 0 && remains-- > 0)
479                 {
480                         pCurrent = null;
481                         buffers.GetAt(0, pCurrent);
482                         buffers.RemoveAt(0);
483                         SysTryCatch(NID_APP, pCurrent != null, , r = E_INVALID_STATE, "");
484
485                         fputs(pCurrent, fp);
486                         len = strlen(pCurrent);
487                         pos += len;
488                         delete[] pCurrent;
489                 }
490
491                 int ret = truncate(path, pos);
492                 SysTryLog(NID_APP, ret == 0, "Truncate failure (%s).", strerror(errno));
493         }
494         else
495         {
496                 char svctext[_MAX_TIZEN_SVC_DESC_LEN] = {0, };
497                 snprintf(svctext, _MAX_TIZEN_SVC_DESC_LEN, "%s=%s\n", fieldName, value);
498                 fputs(svctext, fp);
499         }
500         fclose(fp);
501
502         return E_SUCCESS;
503
504 CATCH:
505
506         remains = buffers.GetCount();   // prevent infinite loop
507         while (buffers.GetCount() > 0 && remains-- > 0)
508         {
509                 pCurrent = null;
510                 buffers.GetAt(0, pCurrent);
511                 buffers.RemoveAt(0);
512                 if (pCurrent != null)
513                 {
514                         delete[] pCurrent;
515                 }
516         }
517
518         fclose(fp);
519
520         return r;
521 }
522
523 //
524 //      Tizen service string example
525 //      X-TIZEN-SVC= http://tizen.org/appcontrol/operation/pick|NULL|image/jpge; http://tizen.org/appcontrol/operation/pick|NULL|video/mp4; http://tizen.org/appcontrol/operation/pick|NULL|NULL; http://tizen.org/appcontrol/operation/pview|NULL|NULL
526 //
527 char*
528 _AulServer::_DesktopFile::UpdateServiceValueN(char* buffer, const char* newValue, bool isRemove)
529 {
530         SysTryReturn(NID_APP, buffer != null, null, E_INVALID_ARG, "");
531         SysTryReturn(NID_APP, newValue != null, null, E_INVALID_ARG, "");
532
533         SysLog(NID_APP, "current(%s), new(%s), isRemove(%s)", buffer, newValue, (isRemove) ? "true" : "false");
534
535         String buf(buffer);
536         bool found = false;
537
538         const String& servicePattern(L"([A-Za-z&=:/\\.\\-]*);?");
539
540         ArrayList services;
541         String resultString;
542
543         Utility::RegularExpression regex;
544         result r = regex.Construct(servicePattern);
545         SysTryReturn(NID_APP, !IsFailed(r), null, r, "");
546
547         String newOperation;
548         String newUrl;
549         String newMimeType;
550         String newService(newValue);
551
552         if (isRemove == false)
553         {
554                 ParseService(newService, newOperation, newUrl, newMimeType);
555         }
556         else
557         {
558                 newOperation = newValue;
559         }
560
561         services.Construct();
562
563         while (regex.Consume(buf, &services) == true)
564         {
565                 String* pCurrentService = (String*) services.GetAt(1);
566                 services.RemoveAll(false);
567
568                 String operation;
569                 String url;
570                 String mimeType;
571
572                 ParseService(*pCurrentService, operation, url, mimeType);
573
574                 if (operation == newOperation)
575                 {
576                         if (isRemove == true)
577                         {
578                                 SysLog(NID_APP, "opreration '%ls' will be removed", operation.GetPointer());
579                         }
580                         else
581                         {
582                                 SysAssertf(found == false, "It's assumed that service doesn't have duplicated operation in tizen desktop file. But it isn't, so now we have to check this case.");
583                                 // replace operation.
584                                 if (found == false) // ( if duplicated operation is already exist, It will be keeped.
585                                 {
586                                         // update value
587                                         AppendServiceValueToString(resultString, newService);
588                                         SysLog(NID_APP, "opreration '%ls;%ls;%ls' will be updated to ;%ls;%ls", operation.GetPointer(), url.GetPointer(), mimeType.GetPointer(), newUrl.GetPointer(), mimeType.GetPointer());
589                                 }
590                         }
591                         found = true;
592                 }
593                 else
594                 {
595                         // add not specified service.
596                         AppendServiceValueToString(resultString, *pCurrentService);
597                 }
598
599                 delete pCurrentService;
600         }
601
602         if (found == false && isRemove == false)
603         {
604                 AppendServiceValueToString(resultString, newService);
605                 SysLog(NID_APP, "opreration '%ls;%ls;%ls' will be added", newOperation.GetPointer(), newUrl.GetPointer(), newMimeType.GetPointer());
606         }
607
608         SysLog(NID_APP, "updated string is '%ls'", resultString.GetPointer());
609         return _StringConverter::CopyToCharArrayN(resultString);
610 }
611
612
613 void
614 _AulServer::_DesktopFile::AppendServiceValueToString(String& serviceString, const String& newVaue)
615 {
616         if (serviceString.GetLength() > 0)
617         {
618                 serviceString += ";";
619         }
620
621         serviceString += newVaue;
622 }
623
624
625 result
626 _AulServer::_DesktopFile::ParseService(const String& service, String& operation, String& url, String& mimeType)
627 {
628         SysLog(NID_APP, "service(%ls)", service.GetPointer());
629
630         const String& serviceDetailPattern(L"([A-Za-z&=/\\.\\-]*):(.*://[A-Za-z&=/\\.\\-]*|[A-Za-z&=/\\.\\-]*):([A-Za-z&=/\\.\\-]*)");
631
632         Utility::RegularExpression regexDetail;
633         result r = regexDetail.Construct(serviceDetailPattern);
634         SysTryReturn(NID_APP, !IsFailed(r), null, r, "[%s] RegularExpression::Construct(L\"%ls\") failed.", GetErrorMessage(r), serviceDetailPattern.GetPointer());
635
636         ArrayList matchedItems;
637         matchedItems.Construct();
638         regexDetail.Match(service, true, &matchedItems);
639
640         int matchedCount = matchedItems.GetCount();
641         SysTryLog(NID_APP, matchedCount == 4, "It's assumed that x-slp-svc value always have operation:url:mime in tizen desktop file. But it isn't or our parser is invalid. so now we have to check this case. %d", matchedItems.GetCount());
642
643         if (matchedCount > 1)
644         {
645                 operation = *(String*) matchedItems.GetAt(1);
646         }
647
648         if (matchedCount > 2)
649         {
650                 url = *(String*) matchedItems.GetAt(2);
651         }
652
653         if (matchedCount > 3)
654         {
655                 mimeType = *(String*) matchedItems.GetAt(3);
656         }
657
658         SysLog(NID_APP, "matched(%d) : (%ls;%ls;%ls)", matchedItems.GetCount(), operation.GetPointer(), url.GetPointer(), mimeType.GetPointer());
659         matchedItems.RemoveAll(true);
660
661         return E_SUCCESS;
662 }
663
664 } } // Tizen::App