html2apk 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,6 +10,7 @@ const i18n = {
10
10
  navSettings: "Configuracoes",
11
11
  navAppearance: "Aparencia",
12
12
  navBuild: "Build",
13
+ navCodes: "Codigos",
13
14
  navLogs: "Logs",
14
15
  navHelp: "Ajuda",
15
16
  theme: "Tema",
@@ -33,7 +34,17 @@ const i18n = {
33
34
  appVersion: "Versao do app",
34
35
  mode: "Modo",
35
36
  chooseMode: "Escolha o modo",
37
+ modeFullscreen: "Tela cheia",
38
+ modeStandalone: "Normal",
39
+ modeFloating: "Flutuante",
40
+ orientation: "Orientacao",
41
+ orientationDefault: "Automatico",
42
+ orientationPortrait: "Vertical",
43
+ orientationLandscape: "Horizontal",
44
+ minSdkVersion: "Android minimo",
36
45
  appIcon: "Icone do app",
46
+ appThemeColor: "Cor do tema do app",
47
+ androidPermissions: "Permissoes Android",
37
48
  chooseIcon: "Escolher icone PNG",
38
49
  reviewBuild: "Revisar build",
39
50
  debugBuild: "Debug tecnico",
@@ -58,6 +69,9 @@ const i18n = {
58
69
  showApk: "Mostrar APK",
59
70
  logsEyebrow: "Ao vivo",
60
71
  logsTitle: "Logs do processo",
72
+ codesEyebrow: "Bridge nativa",
73
+ codesTitle: "Codigos interpretados",
74
+ codesIntro: "Estas funcoes chamadas no JavaScript do app sao interpretadas pelo plugin Cordova e executadas no Java Android.",
61
75
  clearLogs: "Limpar logs",
62
76
  helpEyebrow: "Sem misterio",
63
77
  helpTitle: "Doctor, build e dependencias",
@@ -92,6 +106,8 @@ const i18n = {
92
106
  missingMode: "Escolha o modo do app.",
93
107
  missingIcon: "Escolha o icone do app.",
94
108
  invalidIconType: "Use um icone PNG para evitar falhas no Android.",
109
+ invalidThemeColor: "Use uma cor hexadecimal valida, exemplo: #126fff.",
110
+ invalidMinSdkVersion: "Escolha uma versao minima do Android entre API 24 e API 36.",
95
111
  iconSelected: "Icone selecionado",
96
112
  progressLabel: "Progresso",
97
113
  progressIdle: "Aguardando pasta",
@@ -123,6 +139,7 @@ const i18n = {
123
139
  navSettings: "Settings",
124
140
  navAppearance: "Appearance",
125
141
  navBuild: "Build",
142
+ navCodes: "Code",
126
143
  navLogs: "Logs",
127
144
  navHelp: "Help",
128
145
  theme: "Theme",
@@ -146,7 +163,17 @@ const i18n = {
146
163
  appVersion: "App version",
147
164
  mode: "Mode",
148
165
  chooseMode: "Choose mode",
166
+ modeFullscreen: "Fullscreen",
167
+ modeStandalone: "Normal",
168
+ modeFloating: "Floating",
169
+ orientation: "Orientation",
170
+ orientationDefault: "Auto",
171
+ orientationPortrait: "Portrait",
172
+ orientationLandscape: "Landscape",
173
+ minSdkVersion: "Minimum Android",
149
174
  appIcon: "App icon",
175
+ appThemeColor: "App theme color",
176
+ androidPermissions: "Android permissions",
150
177
  chooseIcon: "Choose PNG icon",
151
178
  reviewBuild: "Review build",
152
179
  debugBuild: "Technical debug",
@@ -171,6 +198,9 @@ const i18n = {
171
198
  showApk: "Show APK",
172
199
  logsEyebrow: "Live",
173
200
  logsTitle: "Process logs",
201
+ codesEyebrow: "Native bridge",
202
+ codesTitle: "Interpreted code",
203
+ codesIntro: "These functions called in your app JavaScript are interpreted by the Cordova plugin and executed in Android Java.",
174
204
  clearLogs: "Clear logs",
175
205
  helpEyebrow: "Plain and simple",
176
206
  helpTitle: "Doctor, build and dependencies",
@@ -205,6 +235,8 @@ const i18n = {
205
235
  missingMode: "Choose the app mode.",
206
236
  missingIcon: "Choose the app icon.",
207
237
  invalidIconType: "Use a PNG icon to avoid Android build failures.",
238
+ invalidThemeColor: "Use a valid hex color, example: #126fff.",
239
+ invalidMinSdkVersion: "Choose a minimum Android version between API 24 and API 36.",
208
240
  iconSelected: "Icon selected",
209
241
  progressLabel: "Progress",
210
242
  progressIdle: "Waiting for folder",
@@ -241,6 +273,69 @@ const animatedBuildLines = [
241
273
  "Procurando APK final / Finding final APK"
242
274
  ];
243
275
 
276
+ const DEFAULT_PERMISSIONS = ["INTERNET", "POST_NOTIFICATIONS", "VIBRATE"];
277
+ const DEFAULT_MIN_SDK_VERSION = 24;
278
+ const MIN_SDK_OPTIONS = [24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36];
279
+
280
+ const permissionOptions = [
281
+ {
282
+ value: "INTERNET",
283
+ label: { pt: "Internet", en: "Internet" },
284
+ detail: { pt: "Acesso a rede", en: "Network access" }
285
+ },
286
+ {
287
+ value: "POST_NOTIFICATIONS",
288
+ label: { pt: "Notificacoes", en: "Notifications" },
289
+ detail: { pt: "Android 13+", en: "Android 13+" }
290
+ },
291
+ {
292
+ value: "VIBRATE",
293
+ label: { pt: "Vibracao", en: "Vibration" },
294
+ detail: { pt: "Permite vibrar", en: "Allows vibration" }
295
+ },
296
+ {
297
+ value: "CAMERA",
298
+ label: { pt: "Camera", en: "Camera" },
299
+ detail: { pt: "Captura de imagem", en: "Image capture" }
300
+ },
301
+ {
302
+ value: "RECORD_AUDIO",
303
+ label: { pt: "Microfone", en: "Microphone" },
304
+ detail: { pt: "Captura de audio", en: "Audio capture" }
305
+ },
306
+ {
307
+ value: "ACCESS_FINE_LOCATION",
308
+ label: { pt: "Localizacao precisa", en: "Precise location" },
309
+ detail: { pt: "GPS e rede", en: "GPS and network" }
310
+ },
311
+ {
312
+ value: "ACCESS_COARSE_LOCATION",
313
+ label: { pt: "Localizacao aproximada", en: "Approximate location" },
314
+ detail: { pt: "Rede", en: "Network" }
315
+ },
316
+ {
317
+ value: "SYSTEM_ALERT_WINDOW",
318
+ label: { pt: "Sobrepor apps", en: "Draw over apps" },
319
+ detail: { pt: "Necessaria no modo flutuante", en: "Required for floating mode" }
320
+ }
321
+ ];
322
+
323
+ const nativeCodeEntries = [
324
+ { js: "toast('Mensagem')", java: "toast", description: { pt: "Mostra uma mensagem rapida nativa.", en: "Shows a native short message." } },
325
+ { js: "vibrar(250)", java: "vibrate", description: { pt: "Aciona a vibracao do aparelho.", en: "Triggers device vibration." } },
326
+ { js: "notificar({ titulo, texto })", java: "notify", description: { pt: "Cria uma notificacao Android imediata.", en: "Creates an immediate Android notification." } },
327
+ { js: "agendarNotificacao({ titulo, texto, quando })", java: "scheduleNotification", description: { pt: "Agenda notificacao mesmo com o app fechado.", en: "Schedules a notification even when the app is closed." } },
328
+ { js: "fullscreen(true)", java: "fullscreen", description: { pt: "Liga ou desliga o modo imersivo.", en: "Toggles immersive mode." } },
329
+ { js: "solicitarPermissaoSobreposicao()", java: "requestOverlayPermission", description: { pt: "Abre a permissao para sobrepor outros apps.", en: "Opens draw-over-apps permission." } },
330
+ { js: "iniciarIconeFlutuante()", java: "startFloatingIcon", description: { pt: "Mostra o icone flutuante nativo.", en: "Shows the native floating icon." } },
331
+ { js: "pararIconeFlutuante()", java: "stopFloatingIcon", description: { pt: "Remove o icone flutuante.", en: "Removes the floating icon." } },
332
+ { js: "manterTelaAcordada(true)", java: "keepScreenAwake", description: { pt: "Impede a tela de apagar enquanto o app esta aberto.", en: "Keeps the screen awake while the app is open." } },
333
+ { js: "brilhoTela(0.8)", java: "setScreenBrightness", description: { pt: "Ajusta o brilho da janela do app.", en: "Adjusts this app window brightness." } },
334
+ { js: "copiarTexto('texto')", java: "copyText", description: { pt: "Copia texto para a area de transferencia.", en: "Copies text to the clipboard." } },
335
+ { js: "compartilharTexto('texto')", java: "shareText", description: { pt: "Abre o compartilhamento nativo do Android.", en: "Opens the native Android share sheet." } },
336
+ { js: "abrirUrl('https://...')", java: "openUrl", description: { pt: "Abre um link em outro app Android.", en: "Opens a link in another Android app." } }
337
+ ];
338
+
244
339
  const state = {
245
340
  language: localStorage.getItem("html2apk.language") || null,
246
341
  theme: localStorage.getItem("html2apk.theme") || "light",
@@ -292,7 +387,12 @@ function collectElements() {
292
387
  "packageIdInput",
293
388
  "versionInput",
294
389
  "modeInput",
390
+ "orientationInput",
391
+ "minSdkVersionInput",
295
392
  "androidPlatformInput",
393
+ "themeColorInput",
394
+ "themeColorTextInput",
395
+ "permissionGrid",
296
396
  "iconPathInput",
297
397
  "iconPreview",
298
398
  "selectIconButton",
@@ -308,6 +408,7 @@ function collectElements() {
308
408
  "progressBar",
309
409
  "progressPercent",
310
410
  "reviewGrid",
411
+ "nativeCodeGrid",
311
412
  "resultPanel",
312
413
  "apkPath",
313
414
  "openDistButton",
@@ -349,6 +450,8 @@ function applyLanguage() {
349
450
  });
350
451
  applyTheme();
351
452
  applyLogBarVisibility();
453
+ renderPermissionOptions(selectedPermissions());
454
+ renderNativeCodeGrid();
352
455
  if (state.project) {
353
456
  validateSettings();
354
457
  renderReview();
@@ -492,6 +595,16 @@ function packageSegment(value) {
492
595
  .replace(/^[^a-z]+/, "") || "app";
493
596
  }
494
597
 
598
+ function normalizeHexColor(value, fallback = "#126fff") {
599
+ const textValue = String(value || "").trim();
600
+ const normalized = textValue.startsWith("#") ? textValue : `#${textValue}`;
601
+ return /^#[0-9a-fA-F]{6}$/.test(normalized) ? normalized.toLowerCase() : fallback;
602
+ }
603
+
604
+ function currentLanguage() {
605
+ return state.language || "pt";
606
+ }
607
+
495
608
  function toFileUrl(filePath) {
496
609
  if (!filePath) {
497
610
  return "../../../html2apk.png";
@@ -521,13 +634,106 @@ function escapeHtml(value) {
521
634
  .replace(/"/g, """);
522
635
  }
523
636
 
637
+ function selectedOptionText(select) {
638
+ if (!select || !select.selectedOptions || !select.selectedOptions.length) {
639
+ return select ? select.value : "";
640
+ }
641
+ return select.selectedOptions[0].textContent || select.value;
642
+ }
643
+
644
+ function selectedPermissions() {
645
+ if (!elements.permissionGrid) {
646
+ return DEFAULT_PERMISSIONS.slice();
647
+ }
648
+
649
+ const inputs = Array.from(elements.permissionGrid.querySelectorAll("input[data-permission-option]"));
650
+ if (!inputs.length) {
651
+ return DEFAULT_PERMISSIONS.slice();
652
+ }
653
+
654
+ const selected = inputs
655
+ .filter((input) => input.checked)
656
+ .map((input) => input.value);
657
+
658
+ if (elements.modeInput && elements.modeInput.value === "floating" && !selected.includes("SYSTEM_ALERT_WINDOW")) {
659
+ selected.push("SYSTEM_ALERT_WINDOW");
660
+ }
661
+
662
+ return Array.from(new Set(selected));
663
+ }
664
+
665
+ function renderPermissionOptions(selected = DEFAULT_PERMISSIONS) {
666
+ if (!elements.permissionGrid) {
667
+ return;
668
+ }
669
+
670
+ const selectedSet = new Set(selected);
671
+ if (elements.modeInput && elements.modeInput.value === "floating") {
672
+ selectedSet.add("SYSTEM_ALERT_WINDOW");
673
+ }
674
+ const language = currentLanguage();
675
+
676
+ elements.permissionGrid.innerHTML = permissionOptions.map((permission) => {
677
+ const checked = selectedSet.has(permission.value) ? " checked" : "";
678
+ const disabled = elements.modeInput && elements.modeInput.value === "floating" && permission.value === "SYSTEM_ALERT_WINDOW"
679
+ ? " disabled"
680
+ : "";
681
+
682
+ return `
683
+ <label class="permission-option">
684
+ <input type="checkbox" data-permission-option value="${escapeHtml(permission.value)}"${checked}${disabled}>
685
+ <span>
686
+ <strong>${escapeHtml(permission.label[language] || permission.label.pt)}</strong>
687
+ <small>${escapeHtml(permission.detail[language] || permission.detail.pt)}</small>
688
+ </span>
689
+ </label>
690
+ `;
691
+ }).join("");
692
+ }
693
+
694
+ function normalizeOrientationInputValue(value) {
695
+ if (value === "vertical") {
696
+ return "portrait";
697
+ }
698
+ if (value === "horizontal") {
699
+ return "landscape";
700
+ }
701
+ return ["portrait", "landscape"].includes(value) ? value : "default";
702
+ }
703
+
704
+ function normalizeMinSdkVersion(value) {
705
+ const parsed = Number.parseInt(value, 10);
706
+ return MIN_SDK_OPTIONS.includes(parsed) ? parsed : DEFAULT_MIN_SDK_VERSION;
707
+ }
708
+
709
+ function renderNativeCodeGrid() {
710
+ if (!elements.nativeCodeGrid) {
711
+ return;
712
+ }
713
+
714
+ const language = currentLanguage();
715
+ elements.nativeCodeGrid.innerHTML = nativeCodeEntries.map((entry) => `
716
+ <article class="code-card">
717
+ <code>${escapeHtml(entry.js)}</code>
718
+ <span>Java: ${escapeHtml(entry.java)}</span>
719
+ <p>${escapeHtml(entry.description[language] || entry.description.pt)}</p>
720
+ </article>
721
+ `).join("");
722
+ }
723
+
524
724
  function populateSettings(config = {}, project = state.project) {
525
725
  const projectName = project ? project.name : "MeuApp";
526
726
  elements.appNameInput.value = config.appName || projectName || "";
527
727
  elements.packageIdInput.value = config.packageId || `com.html2apk.${packageSegment(projectName)}`;
528
728
  elements.versionInput.value = config.version || "1.0.0";
529
729
  elements.modeInput.value = config.mode || "fullscreen";
730
+ elements.orientationInput.value = normalizeOrientationInputValue(config.orientation);
731
+ elements.minSdkVersionInput.value = String(normalizeMinSdkVersion(config.minSdkVersion || config.androidMinSdkVersion));
530
732
  elements.androidPlatformInput.value = config.androidPlatform || "android@15.0.0";
733
+ const themeColor = normalizeHexColor(config.themeColor || config.splashBackgroundColor || config.backgroundColor);
734
+ elements.themeColorInput.value = themeColor;
735
+ elements.themeColorTextInput.value = themeColor;
736
+ renderPermissionOptions(Array.isArray(config.permissions) && config.permissions.length ? config.permissions : DEFAULT_PERMISSIONS);
531
737
  elements.iconPathInput.value = config.icon || "";
532
738
  elements.iconPreview.src = iconPreviewPath(config.icon || "");
533
739
  elements.debugInput.checked = Boolean(config.debug);
@@ -554,6 +760,12 @@ function validateSettings() {
554
760
  if (!elements.modeInput.value) {
555
761
  errors.push(text("missingMode"));
556
762
  }
763
+ if (!MIN_SDK_OPTIONS.includes(Number.parseInt(elements.minSdkVersionInput.value, 10))) {
764
+ errors.push(text("invalidMinSdkVersion"));
765
+ }
766
+ if (!/^#[0-9a-fA-F]{6}$/.test(elements.themeColorTextInput.value.trim())) {
767
+ errors.push(text("invalidThemeColor"));
768
+ }
557
769
  if (!elements.iconPathInput.value.trim()) {
558
770
  errors.push(text("missingIcon"));
559
771
  } else if (!/\.png$/i.test(elements.iconPathInput.value.trim())) {
@@ -596,7 +808,11 @@ function renderReview() {
596
808
  [text("appName"), elements.appNameInput.value.trim()],
597
809
  [text("packageId"), elements.packageIdInput.value.trim()],
598
810
  [text("appVersion"), elements.versionInput.value.trim()],
599
- [text("mode"), elements.modeInput.value],
811
+ [text("mode"), selectedOptionText(elements.modeInput)],
812
+ [text("orientation"), selectedOptionText(elements.orientationInput)],
813
+ [text("minSdkVersion"), selectedOptionText(elements.minSdkVersionInput)],
814
+ [text("appThemeColor"), elements.themeColorTextInput.value.trim()],
815
+ [text("androidPermissions"), selectedPermissions().join(", ")],
600
816
  [text("appIcon"), elements.iconPathInput.value.trim()]
601
817
  ];
602
818
 
@@ -830,6 +1046,10 @@ function buildOptions() {
830
1046
  packageId: elements.packageIdInput.value.trim(),
831
1047
  version: elements.versionInput.value.trim(),
832
1048
  mode: elements.modeInput.value,
1049
+ orientation: elements.orientationInput.value,
1050
+ minSdkVersion: normalizeMinSdkVersion(elements.minSdkVersionInput.value),
1051
+ themeColor: normalizeHexColor(elements.themeColorTextInput.value),
1052
+ permissions: selectedPermissions(),
833
1053
  icon: elements.iconPathInput.value.trim(),
834
1054
  androidPlatform: elements.androidPlatformInput.value.trim(),
835
1055
  debug: elements.debugInput.checked,
@@ -980,11 +1200,32 @@ function bindEvents() {
980
1200
  appendLog(`${text("iconSelected")}: ${iconPath}`, "system");
981
1201
  validateSettings();
982
1202
  });
1203
+ elements.themeColorInput.addEventListener("input", () => {
1204
+ elements.themeColorTextInput.value = elements.themeColorInput.value;
1205
+ validateSettings();
1206
+ });
1207
+ elements.themeColorTextInput.addEventListener("input", () => {
1208
+ if (/^#[0-9a-fA-F]{6}$/.test(elements.themeColorTextInput.value.trim())) {
1209
+ elements.themeColorInput.value = elements.themeColorTextInput.value.trim();
1210
+ }
1211
+ validateSettings();
1212
+ });
1213
+ elements.modeInput.addEventListener("change", () => {
1214
+ const overlayInput = elements.permissionGrid.querySelector("input[value='SYSTEM_ALERT_WINDOW']");
1215
+ const selected = selectedPermissions();
1216
+ const nextPermissions = elements.modeInput.value === "floating" || !overlayInput || !overlayInput.disabled
1217
+ ? selected
1218
+ : selected.filter((permission) => permission !== "SYSTEM_ALERT_WINDOW");
1219
+ renderPermissionOptions(nextPermissions);
1220
+ validateSettings();
1221
+ });
1222
+ elements.permissionGrid.addEventListener("change", validateSettings);
983
1223
  [
984
1224
  elements.appNameInput,
985
1225
  elements.packageIdInput,
986
1226
  elements.versionInput,
987
- elements.modeInput,
1227
+ elements.orientationInput,
1228
+ elements.minSdkVersionInput,
988
1229
  elements.androidPlatformInput,
989
1230
  elements.debugInput,
990
1231
  elements.releaseInput
@@ -381,7 +381,9 @@ h2 {
381
381
  .toggle-grid,
382
382
  .pipeline,
383
383
  .review-grid,
384
- .help-grid {
384
+ .help-grid,
385
+ .permission-grid,
386
+ .code-grid {
385
387
  display: grid;
386
388
  gap: 16px;
387
389
  }
@@ -489,10 +491,56 @@ h2 {
489
491
  outline: none;
490
492
  }
491
493
 
492
- .icon-field {
494
+ .icon-field,
495
+ .permissions-field {
493
496
  grid-column: span 2;
494
497
  }
495
498
 
499
+ .permission-grid {
500
+ grid-template-columns: repeat(2, minmax(0, 1fr));
501
+ gap: 10px;
502
+ }
503
+
504
+ .permission-option {
505
+ min-height: 74px;
506
+ border: 1px solid var(--line);
507
+ border-radius: 8px;
508
+ padding: 12px;
509
+ display: flex;
510
+ gap: 10px;
511
+ align-items: center;
512
+ background: var(--bg);
513
+ }
514
+
515
+ .permission-option input {
516
+ width: 20px;
517
+ height: 20px;
518
+ accent-color: var(--blue);
519
+ flex: 0 0 auto;
520
+ }
521
+
522
+ .permission-option strong,
523
+ .permission-option small {
524
+ display: block;
525
+ }
526
+
527
+ .permission-option small {
528
+ margin-top: 4px;
529
+ color: var(--muted);
530
+ }
531
+
532
+ .color-picker {
533
+ display: grid;
534
+ grid-template-columns: 64px minmax(0, 1fr);
535
+ gap: 10px;
536
+ align-items: center;
537
+ }
538
+
539
+ .field .color-picker input[type="color"] {
540
+ width: 64px;
541
+ padding: 4px;
542
+ }
543
+
496
544
  .icon-picker {
497
545
  display: grid;
498
546
  grid-template-columns: 86px minmax(0, 1fr);
@@ -800,6 +848,44 @@ h2 {
800
848
  padding: 20px;
801
849
  }
802
850
 
851
+ .view-intro {
852
+ max-width: 760px;
853
+ color: var(--muted);
854
+ margin-bottom: 18px;
855
+ }
856
+
857
+ .code-grid {
858
+ grid-template-columns: repeat(2, minmax(0, 1fr));
859
+ }
860
+
861
+ .code-card {
862
+ border: 1px solid var(--line);
863
+ border-radius: 8px;
864
+ background: var(--panel);
865
+ box-shadow: var(--shadow);
866
+ padding: 16px;
867
+ display: grid;
868
+ gap: 10px;
869
+ }
870
+
871
+ .code-card code {
872
+ overflow-wrap: anywhere;
873
+ color: #d6e4f7;
874
+ background: #0c1117;
875
+ border-radius: 7px;
876
+ padding: 10px;
877
+ }
878
+
879
+ .code-card span {
880
+ color: var(--blue);
881
+ font-weight: 800;
882
+ }
883
+
884
+ .code-card p {
885
+ margin: 0;
886
+ color: var(--muted);
887
+ }
888
+
803
889
  .success-view {
804
890
  min-height: 100%;
805
891
  display: none;
@@ -1152,11 +1238,14 @@ body.logs-visible .bottom-log-bar {
1152
1238
  .toggle-grid,
1153
1239
  .pipeline,
1154
1240
  .review-grid,
1155
- .help-grid {
1241
+ .help-grid,
1242
+ .permission-grid,
1243
+ .code-grid {
1156
1244
  grid-template-columns: 1fr;
1157
1245
  }
1158
1246
 
1159
- .icon-field {
1247
+ .icon-field,
1248
+ .permissions-field {
1160
1249
  grid-column: span 1;
1161
1250
  }
1162
1251
 
@@ -21,6 +21,7 @@
21
21
  </config-file>
22
22
 
23
23
  <config-file target="AndroidManifest.xml" parent="/manifest/application">
24
+ <service android:name="dev.html2apk.bridge.FloatingIconService" android:exported="false" />
24
25
  <receiver android:name="dev.html2apk.bridge.NotificationReceiver" android:exported="false" />
25
26
  <receiver android:name="dev.html2apk.bridge.BootReceiver" android:enabled="true" android:exported="true">
26
27
  <intent-filter>
@@ -31,6 +32,7 @@
31
32
  </config-file>
32
33
 
33
34
  <source-file src="src/android/Html2ApkBridge.java" target-dir="app/src/main/java/dev/html2apk/bridge" />
35
+ <source-file src="src/android/FloatingIconService.java" target-dir="app/src/main/java/dev/html2apk/bridge" />
34
36
  <source-file src="src/android/NotificationReceiver.java" target-dir="app/src/main/java/dev/html2apk/bridge" />
35
37
  <source-file src="src/android/BootReceiver.java" target-dir="app/src/main/java/dev/html2apk/bridge" />
36
38
  <source-file src="src/android/NotificationStore.java" target-dir="app/src/main/java/dev/html2apk/bridge" />
@@ -0,0 +1,141 @@
1
+ package dev.html2apk.bridge;
2
+
3
+ import android.app.Service;
4
+ import android.content.Intent;
5
+ import android.graphics.PixelFormat;
6
+ import android.graphics.drawable.GradientDrawable;
7
+ import android.os.Build;
8
+ import android.os.IBinder;
9
+ import android.provider.Settings;
10
+ import android.view.Gravity;
11
+ import android.view.MotionEvent;
12
+ import android.view.View;
13
+ import android.view.WindowManager;
14
+ import android.widget.ImageView;
15
+
16
+ public class FloatingIconService extends Service {
17
+ private WindowManager windowManager;
18
+ private View floatingView;
19
+ private WindowManager.LayoutParams params;
20
+ private int startX;
21
+ private int startY;
22
+ private float touchStartX;
23
+ private float touchStartY;
24
+
25
+ @Override
26
+ public IBinder onBind(Intent intent) {
27
+ return null;
28
+ }
29
+
30
+ @Override
31
+ public int onStartCommand(Intent intent, int flags, int startId) {
32
+ showFloatingIcon();
33
+ return START_STICKY;
34
+ }
35
+
36
+ @Override
37
+ public void onDestroy() {
38
+ removeFloatingIcon();
39
+ super.onDestroy();
40
+ }
41
+
42
+ private void showFloatingIcon() {
43
+ if (floatingView != null) {
44
+ return;
45
+ }
46
+
47
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Settings.canDrawOverlays(this)) {
48
+ stopSelf();
49
+ return;
50
+ }
51
+
52
+ windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
53
+ if (windowManager == null) {
54
+ stopSelf();
55
+ return;
56
+ }
57
+
58
+ ImageView icon = new ImageView(this);
59
+ int size = dp(58);
60
+ int padding = dp(8);
61
+ icon.setImageDrawable(getApplicationInfo().loadIcon(getPackageManager()));
62
+ icon.setPadding(padding, padding, padding, padding);
63
+
64
+ GradientDrawable background = new GradientDrawable();
65
+ background.setShape(GradientDrawable.OVAL);
66
+ background.setColor(0xff126fff);
67
+ icon.setBackground(background);
68
+
69
+ int windowType = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
70
+ ? WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
71
+ : WindowManager.LayoutParams.TYPE_PHONE;
72
+
73
+ params = new WindowManager.LayoutParams(
74
+ size,
75
+ size,
76
+ windowType,
77
+ WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
78
+ PixelFormat.TRANSLUCENT
79
+ );
80
+ params.gravity = Gravity.TOP | Gravity.START;
81
+ params.x = dp(16);
82
+ params.y = dp(96);
83
+
84
+ icon.setOnTouchListener(new View.OnTouchListener() {
85
+ @Override
86
+ public boolean onTouch(View view, MotionEvent event) {
87
+ switch (event.getAction()) {
88
+ case MotionEvent.ACTION_DOWN:
89
+ startX = params.x;
90
+ startY = params.y;
91
+ touchStartX = event.getRawX();
92
+ touchStartY = event.getRawY();
93
+ return true;
94
+ case MotionEvent.ACTION_MOVE:
95
+ params.x = startX + Math.round(event.getRawX() - touchStartX);
96
+ params.y = startY + Math.round(event.getRawY() - touchStartY);
97
+ windowManager.updateViewLayout(floatingView, params);
98
+ return true;
99
+ case MotionEvent.ACTION_UP:
100
+ float deltaX = Math.abs(event.getRawX() - touchStartX);
101
+ float deltaY = Math.abs(event.getRawY() - touchStartY);
102
+ if (deltaX < dp(6) && deltaY < dp(6)) {
103
+ openApp();
104
+ }
105
+ return true;
106
+ default:
107
+ return false;
108
+ }
109
+ }
110
+ });
111
+
112
+ floatingView = icon;
113
+ try {
114
+ windowManager.addView(floatingView, params);
115
+ } catch (RuntimeException error) {
116
+ floatingView = null;
117
+ stopSelf();
118
+ }
119
+ }
120
+
121
+ private void removeFloatingIcon() {
122
+ if (windowManager != null && floatingView != null) {
123
+ windowManager.removeView(floatingView);
124
+ }
125
+ floatingView = null;
126
+ }
127
+
128
+ private void openApp() {
129
+ Intent launchIntent = getPackageManager().getLaunchIntentForPackage(getPackageName());
130
+ if (launchIntent == null) {
131
+ return;
132
+ }
133
+
134
+ launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
135
+ startActivity(launchIntent);
136
+ }
137
+
138
+ private int dp(int value) {
139
+ return Math.round(value * getResources().getDisplayMetrics().density);
140
+ }
141
+ }