diff --git a/Android.bp b/Android.bp index 0d9a68b7c6095f54a59e111811a16b6af3de7da7..90660543fc757f8e0772d291d52856be448fcdd4 100644 --- a/Android.bp +++ b/Android.bp @@ -28,6 +28,11 @@ android_app { "org.lineageos.platform.internal", "androidx.browser_browser", "elib", + "setupwizard2-jackson-core", + "setupwizard2-jackson-databind", + "setupwizard2-jackson-annotations", + "setupwizard2-zxing-android", + "setupwizard2-zxing-core", ], libs: ["telephony-common"], diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 63cc5fb55c8be13a4bacf65ea7d12a75a743d5ea..1c6f5bd2c309f2ddad70205af1697639bc687a2b 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -5,6 +5,7 @@ SPDX-License-Identifier: Apache-2.0 --> @@ -34,6 +35,12 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libs/Android.bp b/libs/Android.bp new file mode 100644 index 0000000000000000000000000000000000000000..062c44d3d7cf5ee6adbc6d3eefc8cfdc407a6490 --- /dev/null +++ b/libs/Android.bp @@ -0,0 +1,35 @@ +// https://repo1.maven.org/maven2/com/google/zxing/core/3.5.4/core-3.5.4.jar +java_import { + name: "setupwizard2-zxing-core", + jars: ["core-3.5.4.jar"], + sdk_version: "current", +} + +// https://repo1.maven.org/maven2/com/journeyapps/zxing-android-embedded/4.3.0/zxing-android-embedded-4.3.0.aar +android_library_import { + name: "setupwizard2-zxing-android", + aars: ["zxing-android-embedded-4.3.0.aar"], + sdk_version: "current", +} + +// https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-core/2.21.0/jackson-core-2.21.0.jar +java_import { + name: "setupwizard2-jackson-core", + jars: ["jackson-core-2.21.0.jar"], + sdk_version: "current", +} + +// https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-databind/2.21.0/jackson-databind-2.21.0.jar +java_import { + name: "setupwizard2-jackson-databind", + jars: ["jackson-databind-2.21.0.jar"], + sdk_version: "current", +} + +// https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-annotations/2.21/jackson-annotations-2.21.jar +java_import { + name: "setupwizard2-jackson-annotations", + jars: ["jackson-annotations-2.21.jar"], + sdk_version: "current", +} + diff --git a/libs/core-3.5.4.jar b/libs/core-3.5.4.jar new file mode 100644 index 0000000000000000000000000000000000000000..955193fb50798f79b3e25fb3d020bc8287ed50d4 Binary files /dev/null and b/libs/core-3.5.4.jar differ diff --git a/libs/jackson-annotations-2.21.jar b/libs/jackson-annotations-2.21.jar new file mode 100644 index 0000000000000000000000000000000000000000..8bcca189d5f2602cddda2face9e83062a3c6d499 Binary files /dev/null and b/libs/jackson-annotations-2.21.jar differ diff --git a/libs/jackson-core-2.21.0.jar b/libs/jackson-core-2.21.0.jar new file mode 100644 index 0000000000000000000000000000000000000000..ae05d27a5d8c58cd566f0b2943b07e3af59367f7 Binary files /dev/null and b/libs/jackson-core-2.21.0.jar differ diff --git a/libs/jackson-databind-2.21.0.jar b/libs/jackson-databind-2.21.0.jar new file mode 100644 index 0000000000000000000000000000000000000000..358b3a424dee0215d9038d33219bf40fa1fc58d0 Binary files /dev/null and b/libs/jackson-databind-2.21.0.jar differ diff --git a/libs/zxing-android-embedded-4.3.0.aar b/libs/zxing-android-embedded-4.3.0.aar new file mode 100644 index 0000000000000000000000000000000000000000..04d731a72d6a153bd1d2234d932246f62ee1a324 Binary files /dev/null and b/libs/zxing-android-embedded-4.3.0.aar differ diff --git a/privapp_whitelist_org.lineageos.setupwizard.xml b/privapp_whitelist_org.lineageos.setupwizard.xml index a13a20669109242e965c6436f0572c451768af2b..f626b6c144b6202dcc3c64aa6f81af79c5efe7c0 100644 --- a/privapp_whitelist_org.lineageos.setupwizard.xml +++ b/privapp_whitelist_org.lineageos.setupwizard.xml @@ -13,6 +13,10 @@ + + + + diff --git a/res/drawable/ic_mdm.xml b/res/drawable/ic_mdm.xml new file mode 100644 index 0000000000000000000000000000000000000000..73c11ce0ec441359b7aaf625b12e312b5d1b8208 --- /dev/null +++ b/res/drawable/ic_mdm.xml @@ -0,0 +1,9 @@ + + + diff --git a/res/layout/mdm_install_page.xml b/res/layout/mdm_install_page.xml new file mode 100644 index 0000000000000000000000000000000000000000..cd27763c45570d0e12ebcfc4052718c91f91b6bd --- /dev/null +++ b/res/layout/mdm_install_page.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/raw/lineage_wizard_script.xml b/res/raw/lineage_wizard_script.xml index fe7f1ed699a5815ada609940bb04d549a9f63e9e..f95aa80d3671b7f2df294748b85f86e9735a6f73 100644 --- a/res/raw/lineage_wizard_script.xml +++ b/res/raw/lineage_wizard_script.xml @@ -19,6 +19,7 @@ id="welcome"> + + + + + + + diff --git a/res/values-de/e_strings.xml b/res/values-de/e_strings.xml index 246c145e2331c36c84518bfa756b13f5463e9fee..90d4975c0e3acdcb6d0bef5cdcbe5bc46df9b6f7 100644 --- a/res/values-de/e_strings.xml +++ b/res/values-de/e_strings.xml @@ -32,4 +32,35 @@ Find my Device einrichten Find my Device hilft Ihnen, den Standort dieses Telefons zu ermitteln, falls Sie es verlegt haben. Erstellen Sie einen Geheimcode und senden Sie ihn einfach per SMS an dieses Gerät. Es antwortet automatisch mit den Koordinaten Ihres Telefons. - \ No newline at end of file + + + + %1$s weiteres Tippen zum Starten der QR-Code-Einrichtung + %1$s weitere Tippen zum Starten der QR-Code-Einrichtung + + QR-Bereitstellung abgebrochen + Gerätebereitstellung + Richten Sie ein Arbeitsgerät ein, indem Sie die Mobile-Device-Management-Anwendung (MDM) installieren + Powered by Headwind MDM + Gerät wird bereitgestellt… + Inhalt des QR-Codes konnte nicht verarbeitet werden! + QR-Code-Parameter fehlt:  + WLAN-Verbindung wird eingerichtet… + WLAN-Verbindung konnte nicht eingerichtet werden! + MDM-Anwendung wird heruntergeladen… + %1$.1f von %2$.1f MB + MDM-Anwendung konnte nicht heruntergeladen werden!  + MDM-Anwendung konnte nicht überprüft werden – Prüfsumme ist ungültig! + MDM-Anwendung wird installiert… + MDM-Anwendung konnte nicht installiert werden!  + MDM-Anwendung wurde installiert! + Fehler + OK + Zurücksetzen + Geräteinhaber kann nicht eingerichtet werden, da das Gerät die Funktion %1$s nicht unterstützt! + DeviceOwner-Bereitstellung ist nicht zulässig, vermutlich ist das Gerät bereits eingerichtet + Admin-Komponentenname ist null + Falsches Format des Komponentennamens:  + Gerätebereitstellung fehlgeschlagen (%1$s) und das Gerät muss auf Werkseinstellungen zurückgesetzt werden + Ungültige Antwort von der Bereitstellungs-Engine:  + diff --git a/res/values-es/e_strings.xml b/res/values-es/e_strings.xml index d36b92bfc7f9aa19cc3f9b0aa2eb4bd64546348e..618fc7de0e64066ec24394f2683aee2caa7a74ac 100644 --- a/res/values-es/e_strings.xml +++ b/res/values-es/e_strings.xml @@ -29,4 +29,35 @@ Configurar Find my Device Find my Device le ayudará a localizar este teléfono en caso de pérdida. Genere un código secreto y envíelo simplemente por SMS a este dispositivo; responderá automáticamente con las coordenadas de su teléfono. - \ No newline at end of file + + + + %1$s toque más para iniciar la configuración mediante código QR + %1$s toques más para iniciar la configuración mediante código QR + + Provisionamiento mediante QR cancelado + Provisionamiento del dispositivo + Configure un dispositivo de trabajo instalando la aplicación de gestión de dispositivos móviles (MDM) + Powered by Headwind MDM + Provisionando el dispositivo… + ¡No se pudo analizar el contenido del código QR! + Falta el parámetro del código QR:  + Configurando la conexión WiFi… + ¡No se pudo configurar la conexión WiFi! + Descargando la aplicación MDM… + %1$.1f de %2$.1f Mb + ¡No se pudo descargar la aplicación MDM!  + No se pudo validar la aplicación MDM - ¡la suma de comprobación es incorrecta! + Instalando la aplicación MDM… + ¡No se pudo instalar la aplicación MDM!  + ¡La aplicación MDM está instalada! + Error + OK + Restablecer + No se puede configurar el propietario del dispositivo porque el dispositivo no tiene la función %1$s. + El provisionamiento de DeviceOwner no está permitido, probablemente el dispositivo ya esté configurado + El nombre del componente administrador es nulo + Formato incorrecto del nombre del componente:  + El provisionamiento del dispositivo falló (%1$s) y el dispositivo debe restablecerse a valores de fábrica + Respuesta no válida del motor de provisionamiento:  + diff --git a/res/values-fr/e_strings.xml b/res/values-fr/e_strings.xml index 893e065e50b69751725a01be2424b47f7fef4025..8e4b364c2029782dc3f13c147fa8f4cfef159e3e 100644 --- a/res/values-fr/e_strings.xml +++ b/res/values-fr/e_strings.xml @@ -32,4 +32,35 @@ Configurer Find my Device Find my Device vous aidera à retrouver l\'emplacement de ce téléphone en cas de perte. Générez un code secret, envoyez-le simplement à cet appareil par SMS, et il répondra automatiquement avec les coordonnées de votre téléphone. - \ No newline at end of file + + + + %1$s appui supplémentaire pour démarrer la configuration par code QR + %1$s appuis supplémentaires pour démarrer la configuration par code QR + + Provisionnement par QR annulé + Provisionnement de l’appareil + Configurez un appareil professionnel en installant l’application de gestion des appareils mobiles (MDM) + Powered by Headwind MDM + Provisionnement de l’appareil en cours… + Impossible d’analyser le contenu du code QR! + Paramètre du code QR manquant:  + Configuration de la connexion WiFi… + Impossible de configurer la connexion WiFi! + Téléchargement de l’application MDM… + %1$.1f sur %2$.1f Mb + Impossible de télécharger l’application MDM!  + Impossible de valider l’application MDM – la somme de contrôle est incorrecte! + Installation de l’application MDM… + Impossible d’installer l’application MDM!  + L’application MDM est installée! + Erreur + OK + Réinitialiser + Impossible de configurer le propriétaire de l’appareil, car l’appareil ne dispose pas de la fonctionnalité %1$s! + Le provisionnement DeviceOwner n’est pas autorisé, l’appareil est probablement déjà configuré + Le nom du composant administrateur est nul + Format du nom du composant incorrect:  + Le provisionnement de l’appareil a échoué (%1$s) et l’appareil doit être réinitialisé aux paramètres d’usine + Réponse non valide du moteur de provisionnement:  + diff --git a/res/values-is/e_strings.xml b/res/values-is/e_strings.xml index ee7d4a80ea76eaf7cf8947bf13558590fe52ad8b..6e55039d8705d14d454a9efabf1b339d897c747d 100644 --- a/res/values-is/e_strings.xml +++ b/res/values-is/e_strings.xml @@ -27,4 +27,34 @@ Virkja barnalæsingu Setja upp barnalæsingu Þetta forrit veitir vörn gegn óviðeigandi efni fyrir börnin þín og unglinga. Þú getur virkjað þessa eiginleika þegar þú lánar símann þinn til barns eða haft þetta virkt á tækjunum þeirra. Til að sjá meira um þetta, skaltu fara á - \ No newline at end of file + + + %1$s snerting í viðbót til að hefja uppsetningu með QR-kóða + %1$s snertingar í viðbót til að hefja uppsetningu með QR-kóða + +QR-uppsetning hætt við +Uppsetning tækis +Settu upp vinnutæki með því að setja upp forrit fyrir stjórnun fartækja (MDM) +Knúið af Headwind MDM +Uppsetning tækis í gangi… +Tókst ekki að lesa innihald QR-kóðans! +QR-kóðabreytu vantar:  +Set upp WiFi-tengingu… +Tókst ekki að setja upp WiFi-tengingu! +Sæki MDM-forritið… +%1$.1f af %2$.1f Mb +Tókst ekki að sækja MDM-forritið!  +Tókst ekki að staðfesta MDM-forritið – eftirlitssumma er röng! +Set upp MDM-forritið… +Tókst ekki að setja upp MDM-forritið!  +MDM-forritið er sett upp! +Villa +OK +Endurstilla +Ekki er hægt að setja upp eiganda tækis þar sem tækið styður ekki eiginleikann %1$s! +DeviceOwner-uppsetning er ekki leyfð, líklega er tækið þegar uppsett +Heiti stjórnandahluta er null +Rangt snið á heiti hluta:  +Uppsetning tækis mistókst (%1$s) og endurstilla þarf tækið í verksmiðjustillingar +Ógilt svar frá uppsetningarvélinni:  + diff --git a/res/values-it/e_strings.xml b/res/values-it/e_strings.xml index 73fd25e577c5355740c0735a68e63bdd22fd9fc9..d06d80a8c9dcfca7cd2a6fcb7c6f12ed4be156c1 100644 --- a/res/values-it/e_strings.xml +++ b/res/values-it/e_strings.xml @@ -29,4 +29,35 @@ Configura Find my Device Find my Device ti aiuterà a individuare la posizione di questo telefono in caso di smarrimento. Genera un codice segreto e invialo semplicemente tramite SMS a questo dispositivo; risponderà automaticamente al messaggio con le coordinate del tuo telefono. - \ No newline at end of file + + + + %1$s altro tocco per avviare la configurazione tramite codice QR + %1$s altri tocchi per avviare la configurazione tramite codice QR + + Provisioning tramite QR annullato + Provisioning del dispositivo + Configura un dispositivo di lavoro installando l’applicazione di gestione dei dispositivi mobili (MDM) + Powered by Headwind MDM + Provisioning del dispositivo in corso… + Impossibile analizzare il contenuto del codice QR! + Parametro del codice QR mancante:  + Configurazione della connessione WiFi in corso… + Impossibile configurare la connessione WiFi! + Download dell’applicazione MDM in corso… + %1$.1f di %2$.1f Mb + Impossibile scaricare l’applicazione MDM!  + Impossibile verificare l’applicazione MDM - checksum non corretto! + Installazione dell’applicazione MDM in corso… + Impossibile installare l’applicazione MDM!  + Applicazione MDM installata! + Errore + OK + Ripristina + Impossibile configurare il proprietario del dispositivo perché il dispositivo non dispone della funzionalità %1$s! + Il provisioning DeviceOwner non è consentito, probabilmente il dispositivo è già configurato + Il nome del componente amministratore è nullo + Formato del nome del componente non corretto:  + Il provisioning del dispositivo non è riuscito (%1$s) e il dispositivo deve essere ripristinato alle impostazioni di fabbrica + Risposta non valida dal motore di provisioning:  + diff --git a/res/values-ja/e_strings.xml b/res/values-ja/e_strings.xml index b029acefd6cb6179273667666360aae93ea3cea8..91e39575d4fc9b26e043e4558cb17d2731acab33 100644 --- a/res/values-ja/e_strings.xml +++ b/res/values-ja/e_strings.xml @@ -10,4 +10,35 @@ e.emailまたはmurena.ioのIDでログインして、この電話とあなたのアカウントを接続すると、あなたの電子メール、連絡先、カレンダーのイベント、画像、動画、メモ、タスクを、この電話とあなたの個人用クラウドの間で同期できます。 各アップデートの直後の最初の起動時にリカバリーをアップデート。 OSとリカバリーを同時にアップデート - \ No newline at end of file + + + + QRコードのセットアップを開始するには、あと%1$s回タップしてください + QRコードのセットアップを開始するには、あと%1$s回タップしてください + + QRプロビジョニングがキャンセルされました + デバイスのプロビジョニング + モバイルデバイス管理(MDM)アプリケーションをインストールして、業務用デバイスを設定します + Powered by Headwind MDM + デバイスをプロビジョニングしています… + QRコードの内容を解析できませんでした! + QRコードのパラメータが不足しています:  + WiFi接続を設定しています… + WiFi接続の設定に失敗しました! + MDMアプリケーションをダウンロードしています… + %1$.1f / %2$.1f Mb + MDMアプリケーションのダウンロードに失敗しました!  + MDMアプリケーションの検証に失敗しました - チェックサムが正しくありません! + MDMアプリケーションをインストールしています… + MDMアプリケーションのインストールに失敗しました!  + MDMアプリケーションがインストールされました! + エラー + OK + リセット + デバイスに %1$s 機能がないため、デバイスオーナーを設定できません! + DeviceOwner のプロビジョニングは許可されていません。おそらくデバイスはすでに設定されています + 管理コンポーネント名が null です + コンポーネント名の形式が正しくありません:  + デバイスのプロビジョニングに失敗しました(%1$s)。デバイスを工場出荷時の状態にリセットする必要があります + プロビジョニングエンジンからの応答が無効です:  + diff --git a/res/values-nl/e_strings.xml b/res/values-nl/e_strings.xml index 7152aa783be73d9fd090cc5c8e4b4f9484410f81..ad4eaf7941bbaf259ca3a93f8a7e91208308ea4f 100644 --- a/res/values-nl/e_strings.xml +++ b/res/values-nl/e_strings.xml @@ -27,4 +27,35 @@ Activeer ouderlijk toezicht Ouderlijk toezicht instellen Deze app biedt bescherming tegen ongepaste inhoud voor jouw kinderen en tieners. Je kan deze functie activeren wanneer je jouw telefoon uitleent aan jouw kind of op hun toestel. - \ No newline at end of file + + + + %1$s extra tik om de QR-codeconfiguratie te starten + %1$s extra tikken om de QR-codeconfiguratie te starten + + QR-provisioning geannuleerd + Provisioning van apparaat + Stel een werkapparaat in door de Mobile Device Management-applicatie (MDM) te installeren + Powered by Headwind MDM + Bezig met provisioning van het apparaat… + Kan de inhoud van de QR-code niet verwerken! + QR-codeparameter ontbreekt:  + WiFi-verbinding wordt ingesteld… + Kan de WiFi-verbinding niet instellen! + MDM-applicatie wordt gedownload… + %1$.1f van %2$.1f Mb + Kan de MDM-applicatie niet downloaden!  + Kan de MDM-applicatie niet valideren - controlesom is onjuist! + MDM-applicatie wordt geïnstalleerd… + Kan de MDM-applicatie niet installeren!  + MDM-applicatie is geïnstalleerd! + Fout + OK + Resetten + Kan apparaateigenaar niet instellen omdat het apparaat de functie %1$s niet ondersteunt! + DeviceOwner-provisioning is niet toegestaan, waarschijnlijk is het apparaat al geconfigureerd + Naam van de beheerderscomponent is null + Onjuist formaat van componentnaam:  + Provisioning van het apparaat is mislukt (%1$s) en het apparaat moet worden teruggezet naar de fabrieksinstellingen + Ongeldige reactie van de provisioning-engine:  + diff --git a/res/values-ru/e_strings.xml b/res/values-ru/e_strings.xml index 1c341355c56ab45a8503bfda4f5478a2bebd4cfe..7581b626df2dfcf07c4c8ad6cdd4f71d35a5f347 100644 --- a/res/values-ru/e_strings.xml +++ b/res/values-ru/e_strings.xml @@ -25,4 +25,35 @@ Войдите в систему с помощью e.email или murena.io ID , чтобы подключить свою личную учётную запись к этому телефону. Это позволит синхронизировать электронную почту, контакты, события календаря, изображения, видео, заметки и задачи между этим телефоном и вашим личным облаком. Синхронизируйте свою облачную учётную запись Создать учётную запись - \ No newline at end of file + + + + Ещё %1$s нажатие для запуска настройки по QR-коду + Ещё %1$s нажатия для запуска настройки по QR-коду + + Настройка по QR-коду отменена + Настройка устройства + Настройте рабочее устройство, установив приложение управления мобильными устройствами (MDM) + Работает на Headwind MDM + Выполняется настройка устройства… + Не удалось обработать содержимое QR-кода! + Отсутствует параметр QR-кода:  + Настройка подключения WiFi… + Не удалось настроить подключение WiFi! + Загрузка MDM-приложения… + %1$.1f из %2$.1f Мб + Не удалось загрузить MDM-приложение!  + Не удалось проверить MDM-приложение — контрольная сумма неверна! + Установка MDM-приложения… + Не удалось установить MDM-приложение!  + MDM-приложение установлено! + Ошибка + OK + Сброс + Невозможно настроить владельца устройства, так как устройство не поддерживает функцию %1$s! + Настройка DeviceOwner не разрешена, вероятно, устройство уже настроено + Имя компонента администратора равно null + Неверный формат имени компонента:  + Настройка устройства не удалась (%1$s), требуется сброс к заводским настройкам + Неверный ответ от движка настройки:  + diff --git a/res/values-sv/e_strings.xml b/res/values-sv/e_strings.xml index f15d47658a25c6f0f1c94067e9c183436a00de7b..95ed4cb598f3f4a6ca6fb233735f05c105695c41 100644 --- a/res/values-sv/e_strings.xml +++ b/res/values-sv/e_strings.xml @@ -27,4 +27,35 @@ Aktivera Föräldrakontroll Ställ in föräldrakontroll Denna app erbjuder skydd mot olämpligt innehåll för barn och tonåringar. Du kan aktivera funktionen när du lånar ut din telefon till ditt barn eller om det är deras enhet. För att ta reda på mer, gå till - \ No newline at end of file + + + + %1$s tryck till för att starta QR-kodkonfiguration + %1$s tryck till för att starta QR-kodkonfiguration + + QR-etablering avbruten + Enhetskonfiguration + Konfigurera en arbetsenhet genom att installera Mobile Device Management-applikationen (MDM) + Drivs av Headwind MDM + Konfigurerar enheten… + Det gick inte att tolka innehållet i QR-koden! + QR-kodparameter saknas:  + Konfigurerar WiFi-anslutningen… + Det gick inte att konfigurera WiFi-anslutningen! + Laddar ner MDM-applikationen… + %1$.1f av %2$.1f Mb + Det gick inte att ladda ner MDM-applikationen!  + Det gick inte att verifiera MDM-applikationen – kontrollsumman är felaktig! + Installerar MDM-applikationen… + Det gick inte att installera MDM-applikationen!  + MDM-applikationen är installerad! + Fel + OK + Återställ + Det går inte att konfigurera enhetsägare eftersom enheten saknar funktionen %1$s! + DeviceOwner-konfiguration är inte tillåten, troligen är enheten redan konfigurerad + Administratörskomponentens namn är null + Felaktigt format på komponentnamnet:  + Enhetskonfigurationen misslyckades (%1$s) och enheten måste fabriksåterställas + ogiltigt svar från konfigurationsmotorn:  + diff --git a/res/values-uk/e_strings.xml b/res/values-uk/e_strings.xml index f5144ce712aff2b30704b03e544a5212f465200b..e7c10f7c04f5ede048b81c70f37885241eb8316d 100644 --- a/res/values-uk/e_strings.xml +++ b/res/values-uk/e_strings.xml @@ -10,4 +10,35 @@ \n/e/OS Увійдіть за допомогою e.email або ідентифікатора murena.io, щоб зв\'язати ваш особистий акаунт з цим телефоном. Це дозволить синхронізувати ваші електронні листи, контакти, події календаря, фотографії, відео, нотатки та завдання на цьому телефоні та у вашій особистій хмарі. Зареєструватися - \ No newline at end of file + + + + Ще %1$s натискання, щоб розпочати налаштування за QR-кодом + Ще %1$s натискань, щоб розпочати налаштування за QR-кодом + + Налаштування за QR-кодом скасовано + Підготовка пристрою + Налаштуйте робочий пристрій, встановивши застосунок керування мобільними пристроями (MDM) + Powered by Headwind MDM + Виконується підготовка пристрою… + Не вдалося обробити вміст QR-коду! + Відсутній параметр QR-коду:  + Налаштування підключення WiFi… + Не вдалося налаштувати підключення WiFi! + Завантаження застосунку MDM… + %1$.1f з %2$.1f Мб + Не вдалося завантажити застосунок MDM!  + Не вдалося перевірити застосунок MDM — контрольна сума неправильна! + Встановлення застосунку MDM… + Не вдалося встановити застосунок MDM!  + Застосунок MDM встановлено! + Помилка + OK + Скинути + Неможливо налаштувати власника пристрою, оскільки пристрій не підтримує функцію %1$s! + Налаштування DeviceOwner не дозволено, ймовірно, пристрій уже налаштований + Ім’я компонента адміністратора дорівнює null + Неправильний формат імені компонента:  + Підготовка пристрою не вдалася (%1$s), пристрій потрібно скинути до заводських налаштувань + Недійсна відповідь від механізму підготовки:  + diff --git a/res/values/e_strings.xml b/res/values/e_strings.xml index 7eb814e4006fd0a05af9febfbfa508f5174f92ee..029d7d2bc39528fe8e03f30d6bca1a00708e0d2a 100644 --- a/res/values/e_strings.xml +++ b/res/values/e_strings.xml @@ -33,4 +33,35 @@ Set up Find my Device Find my device will help you reveal this phone\'s location in case you can\'t find it. Generate a secret code and simply send it to this device via SMS and it will automatically reply to your message with your phone\'s coordinates. - \ No newline at end of file + + + + %1$s more tap to start QR code setup + %1$s more taps to start QR code setup + + QR provisioning cancelled + Device provisioning + Set up a work device by installing the mobile device management (MDM) application + Powered by Headwind MDM + Provisioning the device… + Failed to parse the QR code contents! + QR code parameter missing:  + Setting up the WiFi connection… + Failed to set up the WiFi connection! + Downloading the MDM application… + %1$.1f of %2$.1f Mb + Failed to download the MDM application!  + Failed to validate the MDM application - checksum is incorrect! + Installing the MDM application… + Failed to install the MDM application!  + MDM application is installed! + Error + OK + Reset + Cannot set up device owner because device does not have the %1$s feature! + DeviceOwner provisioning is not allowed, most like device is already provisioned + Admin component name is null + Wrong component name format:  + Device provisioning failed (%1$s) and device must be factory reset + invalid response from the provisioning engine:  + diff --git a/src/org/lineageos/setupwizard/MdmInstallActivity.java b/src/org/lineageos/setupwizard/MdmInstallActivity.java new file mode 100644 index 0000000000000000000000000000000000000000..e0abde6ef377b585e61344023cc815b8c7e88b15 --- /dev/null +++ b/src/org/lineageos/setupwizard/MdmInstallActivity.java @@ -0,0 +1,266 @@ +/* + * SPDX-FileCopyrightText: 2016 The CyanogenMod Project + * SPDX-FileCopyrightText: 2017-2024 The LineageOS Project + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.lineageos.setupwizard; + +import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_ADMIN_EXTRAS_BUNDLE; +import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME; +import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_DEVICE_ADMIN_SIGNATURE_CHECKSUM; +import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_LEAVE_ALL_SYSTEM_APPS_ENABLED; +import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_SKIP_ENCRYPTION; + +import android.app.admin.DevicePolicyManager; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageInstaller; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.os.Looper; +import android.view.View; +import android.widget.ProgressBar; +import android.widget.TextView; + +import androidx.appcompat.app.AlertDialog; + +import org.lineageos.setupwizard.mdm.AppDownloader; +import org.lineageos.setupwizard.mdm.AppInstaller; +import org.lineageos.setupwizard.mdm.ProvisioningQrParser; + +import java.io.File; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +public class MdmInstallActivity extends BaseSetupWizardActivity implements + ProvisioningQrParser.ErrorHandler, + AppDownloader.Handler { + + public static final String EXTRA_QR_CONTENTS = "EXTRA_QR_CONTENTS"; + public static final String TAG = MdmInstallActivity.class.getSimpleName(); + + private static final float BYTES_IN_MB = 1048576.0f; + private ProgressBar spinner; + private TextView message; + private ProgressBar linearProgress; + private TextView progressLegend; + + private ProvisioningQrParser.Data qrContents; + + private AppDownloader appDownloader; + private final Executor executor = Executors.newSingleThreadExecutor(); + private final android.os.Handler ui = new android.os.Handler(Looper.getMainLooper()); + + private String calculatedPackageChecksum = null; + + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setNextAllowed(false); + bindViews(); + + String qrRawContents = getIntent().getStringExtra(MdmInstallActivity.EXTRA_QR_CONTENTS); + qrContents = new ProvisioningQrParser(this).parseProvisioningQr(this, qrRawContents); + if (qrContents != null) { + message.setText(getString(R.string.downloading_admin_app)); + appDownloader = new AppDownloader(this); + appDownloader.downloadApp(this, qrContents); + } + } + + private void bindViews() { + spinner = requireViewById(R.id.spinning_progress); + message = requireViewById(R.id.text_message); + linearProgress = requireViewById(R.id.linear_progress); + progressLegend = requireViewById(R.id.progress_legend); + } + + @Override + public void handleDownloadStart(int lengthOfFile) { + message.setText(getString(R.string.downloading_admin_app)); + if (lengthOfFile == -1) { + // We don't know the content length + spinner.setVisibility(View.VISIBLE); + linearProgress.setVisibility(View.GONE); + progressLegend.setVisibility(View.GONE); + } else { + spinner.setVisibility(View.GONE); + linearProgress.setVisibility(View.VISIBLE); + progressLegend.setVisibility(View.VISIBLE); + handleDownloadProgress(0, lengthOfFile); + } + } + + @Override + public void handleDownloadProgress(int progress, int lengthOfFile) { + if (lengthOfFile != -1) { + float downloadedMb = progress / BYTES_IN_MB; + float totalMb = lengthOfFile / BYTES_IN_MB; + progressLegend.setText(getString(R.string.download_progress, + downloadedMb, totalMb)); + linearProgress.setProgress((int)(downloadedMb * 100 / totalMb)); + } + } + + @Override + public void handleDownloadComplete(File file) { + // Installing the admin app + spinner.setVisibility(View.VISIBLE); + message.setText(getString(R.string.installing_admin_app)); + registerReceiver( + appInstallReceiver, + new IntentFilter(AppInstaller.ACTION_INSTALL_COMPLETE), + Context.RECEIVER_EXPORTED + ); + executor.execute(() -> { + String error = AppInstaller.silentInstallApplication(this, file); + if (error != null) { + try { + unregisterReceiver(appInstallReceiver); + } catch (Exception ignored) { + // receiver may already be unregistered + } + ui.post(() -> { + handleError(getString(R.string.install_failed) + error); + }); + } + // Install completion is caught in the BroadcastReceiver + }); + } + + @Override + public void handleError(String message) { + try { + unregisterReceiver(appInstallReceiver); + } catch (Exception ignored) { + // receiver may not be registered + } + + new AlertDialog.Builder(this) + .setTitle(R.string.error_title) + .setMessage(message) + .setPositiveButton(R.string.button_ok, + (dialog, which) -> { + dialog.dismiss(); + finishAction(RESULT_CANCELED, new Intent().putExtra("onBackPressed", true)); + }) + .setCancelable(false) + .create() + .show(); + } + + private void handleInstallComplete() { + spinner.setVisibility(View.GONE); + message.setText(getString(R.string.install_successful)); + setNextAllowed(true); + } + + private void handleInstallError(String statusMessage, String extraMessage) { + String errorText = + getString(R.string.install_failed) + statusMessage; + if (extraMessage != null && !extraMessage.isEmpty()) { + errorText += ", extra: " + extraMessage; + } + handleError(errorText); + } + + @Override + protected void onNextPressed() { + try { + unregisterReceiver(appInstallReceiver); + } catch (Exception e) { + e.printStackTrace(); + } + startProvisioning(); + } + + private void startProvisioning() { + if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_DEVICE_ADMIN)) { + handleError(getResources().getString(R.string.provision_failed_no_feature, PackageManager.FEATURE_DEVICE_ADMIN)); + return; + } + + DevicePolicyManager dpm = (DevicePolicyManager) getSystemService(Context.DEVICE_POLICY_SERVICE); + if (dpm == null) { + handleError(getResources().getString(R.string.provision_failed_no_feature, Context.DEVICE_POLICY_SERVICE)); + return; + } + + if (!dpm.isProvisioningAllowed(DevicePolicyManager.ACTION_PROVISION_MANAGED_DEVICE)) { + handleError(getResources().getString(R.string.provision_failed_not_allowed)); + return; + } + + Intent intent = new Intent(this, ProvisionActivity.class); + + // Kotlin adminComponentName!! -> in Java we must null-check to avoid NPE + if (qrContents.adminComponentName == null) { + handleError(getResources().getString(R.string.component_name_is_null)); + return; + } + + String[] adminComponentNameParts = qrContents.adminComponentName.split("/"); + if (adminComponentNameParts.length != 2) { + handleError(getResources().getString(R.string.component_name_wrong_format) + + qrContents.adminComponentName); + return; + } + + intent.putExtra(EXTRA_PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME, + new ComponentName(adminComponentNameParts[0], adminComponentNameParts[1]) + ); + + intent.putExtra(EXTRA_PROVISIONING_DEVICE_ADMIN_SIGNATURE_CHECKSUM, qrContents.packageChecksum); + + if (qrContents.systemAppsEnabled) { + intent.putExtra(EXTRA_PROVISIONING_LEAVE_ALL_SYSTEM_APPS_ENABLED, true); + } + if (qrContents.skipEncryption) { + intent.putExtra(EXTRA_PROVISIONING_SKIP_ENCRYPTION, true); + } + if (qrContents.extrasBundle != null) { + intent.putExtra(EXTRA_PROVISIONING_ADMIN_EXTRAS_BUNDLE, qrContents.extrasBundle); + } + + startActivity(intent); + } + + private final BroadcastReceiver appInstallReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + int status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, 0); + + switch (status) { + case PackageInstaller.STATUS_SUCCESS: + handleInstallComplete(); + break; + + default: + // Installation failure + String extraMessage = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE); + String statusMessage = AppInstaller.getPackageInstallerStatusMessage(status); + handleInstallError(statusMessage, extraMessage); + } + } + }; + + @Override + protected int getLayoutResId() { + return R.layout.mdm_install_page; + } + + @Override + protected int getTitleResId() { + return R.string.provisioning_title; + } + + @Override + protected int getIconResId() { + return R.drawable.ic_mdm; + } +} diff --git a/src/org/lineageos/setupwizard/MdmNetworkSetupActivity.java b/src/org/lineageos/setupwizard/MdmNetworkSetupActivity.java new file mode 100644 index 0000000000000000000000000000000000000000..2466fd468bec25ec0518916207fd63592fe3aa02 --- /dev/null +++ b/src/org/lineageos/setupwizard/MdmNetworkSetupActivity.java @@ -0,0 +1,34 @@ +/* + * SPDX-FileCopyrightText: 2016 The CyanogenMod Project + * SPDX-FileCopyrightText: 2017-2024 The LineageOS Project + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.lineageos.setupwizard; + +import static com.google.android.setupcompat.util.ResultCodes.RESULT_ACTIVITY_NOT_FOUND; + +import android.content.Intent; + +import androidx.activity.result.ActivityResult; +import androidx.annotation.NonNull; + +public class MdmNetworkSetupActivity extends NetworkSetupActivity { + + @Override + protected void onSubactivityResult(@NonNull ActivityResult activityResult) { + int resultCode = activityResult.getResultCode(); + Intent data = activityResult.getData(); + if (resultCode != RESULT_CANCELED) { + // We should proxy the QR code contents to the MDM install activity, + // so sending getIntent() here + nextAction(resultCode, getIntent()); + } else if (mIsSubactivityNotFound) { + finishAction(RESULT_ACTIVITY_NOT_FOUND); + } else if (data != null && data.getBooleanExtra("onBackPressed", false)) { + onStartSubactivity(); + } else { + finishAction(RESULT_CANCELED); + } + } +} diff --git a/src/org/lineageos/setupwizard/NetworkSetupActivity.java b/src/org/lineageos/setupwizard/NetworkSetupActivity.java index 3c9cf2a1472b9dcbb93c3a44bd6b87dc64e16d08..bca8388c75a553b14c2a369cc981507a95c2f4f2 100644 --- a/src/org/lineageos/setupwizard/NetworkSetupActivity.java +++ b/src/org/lineageos/setupwizard/NetworkSetupActivity.java @@ -27,7 +27,7 @@ public class NetworkSetupActivity extends SubBaseActivity { protected void onStartSubactivity() { if ((!SetupWizardUtils.hasWifi(this) && !SetupWizardUtils.hasTelephony(this)) || SetupWizardUtils.isNetworkConnectedToInternetViaEthernet(this)) { - finishAction(RESULT_SKIP); + finishAction(RESULT_SKIP, getIntent()); return; } if (SetupWizardUtils.isOwner()) { diff --git a/src/org/lineageos/setupwizard/ProvisionActivity.java b/src/org/lineageos/setupwizard/ProvisionActivity.java new file mode 100644 index 0000000000000000000000000000000000000000..bd73a3d07ad586f6d7336537c31c2d14652532ae --- /dev/null +++ b/src/org/lineageos/setupwizard/ProvisionActivity.java @@ -0,0 +1,261 @@ +package org.lineageos.setupwizard; + +import android.app.Activity; +import android.app.admin.DevicePolicyManager; +import android.content.ComponentName; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.os.PersistableBundle; +import android.provider.Settings; +import android.util.Log; + +import androidx.appcompat.app.AlertDialog; + +import org.lineageos.setupwizard.util.SetupWizardUtils; + +public class ProvisionActivity extends Activity { + + private static final String TAG = "ProvisionActivity"; + + private static final int PROVISIONING_TRIGGER_QR_CODE = 2; + + // Copied from ManagedProvisioning app, as they're hidden; + private static final String PROVISION_FINALIZATION_INSIDE_SUW = + "android.app.action.PROVISION_FINALIZATION_INSIDE_SUW"; + private static final int RESULT_CODE_PROFILE_OWNER_SET = 122; + private static final int RESULT_CODE_DEVICE_OWNER_SET = 123; + + // Copied from DevicePolicyManager internal code + private static final String ACTION_PROVISION_MANAGED_DEVICE_FROM_TRUSTED_SOURCE = + "android.app.action.PROVISION_MANAGED_DEVICE_FROM_TRUSTED_SOURCE"; + private static final String EXTRA_PROVISIONING_TRIGGER = + "android.app.extra.PROVISIONING_TRIGGER"; + + + public static final int REQUEST_CODE_STEP1 = 42; + public static final int REQUEST_CODE_STEP2_PO = 43; + public static final int REQUEST_CODE_STEP2_DO = 44; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + provisionDeviceOwner(); + } + + public void provisionDeviceOwner() { + Intent provisionIntent = new Intent(ACTION_PROVISION_MANAGED_DEVICE_FROM_TRUSTED_SOURCE); + + provisionIntent.putExtra( + EXTRA_PROVISIONING_TRIGGER, + PROVISIONING_TRIGGER_QR_CODE + ); + + ComponentName adminComponent = getIntent().getParcelableExtra( + DevicePolicyManager.EXTRA_PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME); + + provisionIntent.putExtra(DevicePolicyManager.EXTRA_PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME, + adminComponent); + + provisionIntent.putExtra( + DevicePolicyManager.EXTRA_PROVISIONING_DEVICE_ADMIN_SIGNATURE_CHECKSUM, + getIntent().getStringExtra( + DevicePolicyManager.EXTRA_PROVISIONING_DEVICE_ADMIN_SIGNATURE_CHECKSUM + ) + ); + + boolean systemAppsEnabled = + getIntent().getBooleanExtra( + DevicePolicyManager.EXTRA_PROVISIONING_LEAVE_ALL_SYSTEM_APPS_ENABLED, + false + ); + + if (systemAppsEnabled) { + provisionIntent.putExtra( + DevicePolicyManager.EXTRA_PROVISIONING_LEAVE_ALL_SYSTEM_APPS_ENABLED, + true + ); + } + + boolean skipEncryption = + getIntent().getBooleanExtra( + DevicePolicyManager.EXTRA_PROVISIONING_SKIP_ENCRYPTION, + false + ); + + if (skipEncryption) { + provisionIntent.putExtra( + DevicePolicyManager.EXTRA_PROVISIONING_SKIP_ENCRYPTION, + true + ); + } + + PersistableBundle extrasBundle = + getIntent().getParcelableExtra( + DevicePolicyManager.EXTRA_PROVISIONING_ADMIN_EXTRAS_BUNDLE + ); + + if (extrasBundle != null) { + provisionIntent.putExtra( + DevicePolicyManager.EXTRA_PROVISIONING_ADMIN_EXTRAS_BUNDLE, + extrasBundle + ); + } + + startActivityForResult(provisionIntent, REQUEST_CODE_STEP1); + } + + private void disableSelfAndFinish() { + // remove this activity from the package manager. + PackageManager pm = getPackageManager(); + String name = getPackageName(); + + Log.i(TAG, "Disabling itself (" + name + ")"); + + pm.setApplicationEnabledSetting(name, + PackageManager.COMPONENT_ENABLED_STATE_DISABLED, + PackageManager.DONT_KILL_APP + ); + + // terminate the wizard. + SetupWizardUtils.finishSetupWizard(this); + } + + private void handleProvisioningStep1Result(int resultCode) { + final int requestCodeStep2; + + switch (resultCode) { + case RESULT_CODE_PROFILE_OWNER_SET: + requestCodeStep2 = REQUEST_CODE_STEP2_PO; + /*factoryReset(context, "profile owner is not supported"); + return; */ + break; + + case RESULT_CODE_DEVICE_OWNER_SET: + requestCodeStep2 = REQUEST_CODE_STEP2_DO; + break; + + default: + factoryReset( + "invalid response from the provisioning engine: " + + resultCodeToString(resultCode) + ); + return; + } + + Intent intent = new Intent(PROVISION_FINALIZATION_INSIDE_SUW) + .addCategory(Intent.CATEGORY_DEFAULT); + + Log.i(TAG, "Finalizing DPC with " + intent); + + startActivityForResult(intent, requestCodeStep2); + } + + private void handleProvisioningStep2Result(int requestCode, int resultCode) { + // Must set state before launching the intent that finalize the DPC, because the DPC + // implementation might not remove the back button + setProvisioningState(); + + boolean doMode = requestCode == REQUEST_CODE_STEP2_DO; + + if (resultCode != Activity.RESULT_OK) { + factoryReset(getResources().getString(R.string.invalid_engine_response) + + resultCodeToString(resultCode)); + return; + } + + Log.i(TAG,(doMode ? "Device owner" : "Profile owner") + " mode provisioned!"); + + SetupWizardUtils.finishSetupWizard(this); + } + + protected void setProvisioningState() { + // Add a persistent setting to allow other apps to know the device has been provisioned. + Settings.Global.putInt(getContentResolver(), Settings.Global.DEVICE_PROVISIONED, 1); + Settings.Secure.putInt(getContentResolver(), Settings.Secure.USER_SETUP_COMPLETE, 1); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + Log.d(TAG, "onActivityResult(): request=" + requestCode + + ", result=" + + resultCodeToString(resultCode) + + ", data=" + data); + + switch (requestCode) { + case REQUEST_CODE_STEP1: + handleProvisioningStep1Result(resultCode); + break; + + case REQUEST_CODE_STEP2_PO: + case REQUEST_CODE_STEP2_DO: + handleProvisioningStep2Result(requestCode, resultCode); + break; + + default: + showErrorMessage("onActivityResult(): invalid request code " + requestCode); + break; + } + } + + private void factoryReset(final String reason) { + new AlertDialog.Builder(this) + .setMessage(getResources().getString(R.string.provision_failed_need_reset, reason)) + .setPositiveButton( + getResources().getString(R.string.button_reset), + (dialog, which) -> sendFactoryResetIntent(reason) + ) + .setOnDismissListener( + dialog -> sendFactoryResetIntent(reason) + ) + .show(); + } + + private void sendFactoryResetIntent(String reason) { + Log.e(TAG, "Factory resetting: $reason"); + Intent intent = new Intent(Intent.ACTION_FACTORY_RESET); + intent.setPackage("android"); + intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); + intent.putExtra(Intent.EXTRA_REASON, reason); + sendBroadcast(intent); + + // Just in case the factory reset request fails... + setProvisioningState(); + disableSelfAndFinish(); + } + + + public String resultCodeToString(int resultCode) { + StringBuilder result = new StringBuilder(); + + switch (resultCode) { + case Activity.RESULT_OK: + result.append("RESULT_OK"); + break; + case Activity.RESULT_CANCELED: + result.append("RESULT_CANCELED"); + break; + case Activity.RESULT_FIRST_USER: + result.append("RESULT_FIRST_USER"); + break; + case RESULT_CODE_PROFILE_OWNER_SET: + result.append("RESULT_CODE_PROFILE_OWNER_SET"); + break; + case RESULT_CODE_DEVICE_OWNER_SET: + result.append("RESULT_CODE_DEVICE_OWNER_SET"); + break; + default: + result.append("UNKNOWN_CODE"); + break; + } + + return result.append('(') + .append(resultCode) + .append(')') + .toString(); + } + + private void showErrorMessage(String message) { + Log.e(TAG, "Error: " + message); + } +} \ No newline at end of file diff --git a/src/org/lineageos/setupwizard/SubBaseActivity.java b/src/org/lineageos/setupwizard/SubBaseActivity.java index 706b6573d89c623ef3905c826b876772d7772187..50b6c8a526f8a1b43e9d970ef624f4c29af7bd60 100644 --- a/src/org/lineageos/setupwizard/SubBaseActivity.java +++ b/src/org/lineageos/setupwizard/SubBaseActivity.java @@ -80,7 +80,7 @@ public abstract class SubBaseActivity extends BaseSetupWizardActivity { } catch (ActivityNotFoundException e) { Log.w(TAG, "activity not found; start next screen and finish; intent=" + intent); mIsSubactivityNotFound = true; - finishAction(RESULT_ACTIVITY_NOT_FOUND); + finishAction(RESULT_ACTIVITY_NOT_FOUND, getIntent()); } } diff --git a/src/org/lineageos/setupwizard/WelcomeActivity.java b/src/org/lineageos/setupwizard/WelcomeActivity.java index 2e12426546444da8ab8c4eeb5efe722f1143a40f..bc07c4b32261aaffd34d2d10630241755755eec8 100644 --- a/src/org/lineageos/setupwizard/WelcomeActivity.java +++ b/src/org/lineageos/setupwizard/WelcomeActivity.java @@ -10,12 +10,19 @@ import static org.lineageos.setupwizard.SetupWizardApp.ACTION_EMERGENCY_DIAL; import android.content.Intent; import android.os.Bundle; +import android.view.MotionEvent; import android.view.View; import android.widget.Button; import android.widget.TextView; +import android.widget.Toast; + +import androidx.activity.result.ActivityResultLauncher; import com.google.android.setupcompat.template.FooterButtonStyleUtils; import com.google.android.setupcompat.util.SystemBarHelper; +import com.google.android.setupdesign.gesture.ConsecutiveTapsGestureDetector; +import com.journeyapps.barcodescanner.ScanContract; +import com.journeyapps.barcodescanner.ScanOptions; import org.lineageos.setupwizard.util.SetupWizardUtils; @@ -24,6 +31,10 @@ public class WelcomeActivity extends SubBaseActivity { private static final String ACTION_ACCESSIBILITY_SETTINGS = "android.settings.ACCESSIBILITY_SETTINGS_FOR_SUW"; + private ConsecutiveTapsGestureDetector mdmUnlockDetector = null; + private Toast qrToast = null; + private ActivityResultLauncher barcodeLauncher = null; + @Override protected void onStartSubactivity() { } @@ -60,6 +71,83 @@ public class WelcomeActivity extends SubBaseActivity { welcomeTitle.setText(getString(R.string.setup_welcome_message_e, getString(R.string.os_name))); } + + initQrProvisioning(); + mdmUnlockDetector = new ConsecutiveTapsGestureDetector( + this.onConsecutiveTapsListener, + requireViewById(R.id.setup_wizard_layout) + ); + } + + @Override + public void onResume() { + super.onResume(); + mdmUnlockDetector.resetCounter(); + } + + @Override + public boolean dispatchTouchEvent(MotionEvent e) { + boolean isTouchEventHandled = super.dispatchTouchEvent(e); + if (e.getAction() == MotionEvent.ACTION_UP) { + mdmUnlockDetector.onTouchEvent(e); + } + return isTouchEventHandled; + } + + private final ConsecutiveTapsGestureDetector.OnConsecutiveTapsListener + onConsecutiveTapsListener = + welcomeTapCounter -> { + if (qrToast != null) { + qrToast.cancel(); + } + if (welcomeTapCounter >= 6) { + startQrProvisioning(); + } else { + if (welcomeTapCounter < 3) { + return; + } + int tapsRemaining = 6 - welcomeTapCounter; + String msg = getResources().getQuantityString( + R.plurals.qr_provision_toast, + tapsRemaining, + Integer.valueOf(tapsRemaining) + ); + qrToast = Toast.makeText(WelcomeActivity.this, msg, Toast.LENGTH_LONG); + qrToast.show(); + } + }; + + private void initQrProvisioning() { + barcodeLauncher = registerForActivityResult( + new ScanContract(), + result -> { + if (result.getContents() == null) { + Toast.makeText( + this, + R.string.qr_provisioning_cancelled, + Toast.LENGTH_LONG + ).show(); + } else { + launchQrProvisioning(result.getContents()); + } + } + ); + } + + public void startQrProvisioning() { + ScanOptions options = new ScanOptions(); + options.setDesiredBarcodeFormats(ScanOptions.QR_CODE); + options.setBeepEnabled(false); + + if (barcodeLauncher != null) { + barcodeLauncher.launch(options); + } + } + + public void launchQrProvisioning(String contents) { + Intent intent = new Intent(); + intent.putExtra(MdmInstallActivity.EXTRA_QR_CONTENTS, contents); + nextAction(101, intent); } @Override diff --git a/src/org/lineageos/setupwizard/mdm/AppDownloader.java b/src/org/lineageos/setupwizard/mdm/AppDownloader.java new file mode 100644 index 0000000000000000000000000000000000000000..c3983ec2ee96bf5b87503d70f82c16f8a62f5617 --- /dev/null +++ b/src/org/lineageos/setupwizard/mdm/AppDownloader.java @@ -0,0 +1,143 @@ +package org.lineageos.setupwizard.mdm; + +import android.app.Activity; +import android.os.Looper; +import android.util.Base64; + +import org.lineageos.setupwizard.R; + +import java.io.DataInputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.security.MessageDigest; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +public class AppDownloader { + + private static final int CONNECTION_TIMEOUT_MS = 10_000; + private static final String ADMIN_APK_FILE_NAME = "deviceadmin.apk"; + + public interface Handler { + void handleDownloadStart(int lengthOfFile); + void handleDownloadProgress(int progress, int lengthOfFile); + void handleDownloadComplete(File file); + void handleError(String message); + } + + private final Executor executor = Executors.newSingleThreadExecutor(); + private final android.os.Handler ui = new android.os.Handler(Looper.getMainLooper()); + private Handler handler; + private File file; + private String calculatedPackageChecksum; + + public AppDownloader(Handler handler) { + this.handler = handler; + } + + public void downloadApp(final Activity context, final ProvisioningQrParser.Data qrContents) { + + executor.execute(() -> { + if (downloadAdminAppSync(context, qrContents)) { + if (calculatedPackageChecksum == null + || qrContents.packageChecksum == null + || !calculatedPackageChecksum.equalsIgnoreCase(qrContents.packageChecksum)) { + notifyError(context.getString(R.string.checksum_failed)); + return; + } + notifyDownloadComplete(file); + } + }); + } + + private boolean downloadAdminAppSync(Activity context, ProvisioningQrParser.Data qrContents) { + file = new File(context.getFilesDir(), ADMIN_APK_FILE_NAME); + if (file.exists()) { + file.delete(); + } + + try { + try { + file.createNewFile(); + } catch (Exception e) { + e.printStackTrace(); + notifyError("Failed to create " + file.getAbsolutePath()); + return false; + } + + URL url = new URL(qrContents.downloadLocation); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + connection.setRequestProperty("Accept-Encoding", "identity"); + connection.setConnectTimeout(CONNECTION_TIMEOUT_MS); + connection.setReadTimeout(CONNECTION_TIMEOUT_MS); + connection.connect(); + + if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) { + throw new Exception("Bad server response for " + qrContents.downloadLocation + + ": " + connection.getResponseCode()); + } + + int lengthOfFile = connection.getContentLength(); + notifyDownloadStart(lengthOfFile); + + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + + DataInputStream dis = new DataInputStream(connection.getInputStream()); + byte[] buffer = new byte[1024]; + int length; + long total = 0; + + FileOutputStream fos = new FileOutputStream(file); + while ((length = dis.read(buffer)) > 0) { + digest.update(buffer, 0, length); + total += (long) length; + notifyDownloadProgress((int) total, lengthOfFile); + fos.write(buffer, 0, length); + } + + fos.flush(); + fos.close(); + dis.close(); + + calculatedPackageChecksum = Base64.encodeToString( + digest.digest(), + Base64.NO_WRAP | Base64.URL_SAFE + ); + + } catch (Exception e) { + e.printStackTrace(); + file.delete(); + notifyError(context.getString(R.string.download_failed) + e.getMessage()); + return false; + } + + return true; + } + + public void notifyDownloadStart(int lengthOfFile) { + ui.post(() -> { + handler.handleDownloadStart(lengthOfFile); + }); + } + + public void notifyDownloadProgress(int progress, int lengthOfFile) { + ui.post(() -> { + handler.handleDownloadProgress(progress, lengthOfFile); + }); + } + + public void notifyDownloadComplete(File file) { + ui.post(() -> { + handler.handleDownloadComplete(file); + }); + } + + public void notifyError(String message) { + ui.post(() -> { + handler.handleError(message); + }); + } +} diff --git a/src/org/lineageos/setupwizard/mdm/AppInstaller.java b/src/org/lineageos/setupwizard/mdm/AppInstaller.java new file mode 100644 index 0000000000000000000000000000000000000000..c24621198fee90567408f80a0f4b85790a2ef0f9 --- /dev/null +++ b/src/org/lineageos/setupwizard/mdm/AppInstaller.java @@ -0,0 +1,109 @@ +package org.lineageos.setupwizard.mdm; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.IntentSender; +import android.content.pm.PackageInfo; +import android.content.pm.PackageInstaller; +import android.content.pm.PackageManager; +import android.util.Log; + +import java.io.File; +import java.io.FileInputStream; +import java.io.OutputStream; + +public final class AppInstaller { + private static final String TAG = "AppInstaller"; + + public static final String ACTION_INSTALL_COMPLETE = "INSTALL_COMPLETE"; + public static final String PACKAGE_NAME = "PACKAGE_NAME"; + + private AppInstaller() { + // no instances + } + + /** + * @return null on success; otherwise an error message. + */ + public static String silentInstallApplication(Context context, File file) { + try { + PackageManager packageManager = context.getPackageManager(); + PackageInfo packageInfo = packageManager.getPackageArchiveInfo(file.getPath(), 0); + if (packageInfo == null) { + throw new Exception("Failed to parse the admin app package"); + } + + String packageName = packageInfo.packageName; + Log.i(TAG, "Installing " + packageName); + + FileInputStream in = new FileInputStream(file); + + PackageInstaller packageInstaller = context.getPackageManager().getPackageInstaller(); + PackageInstaller.SessionParams params = new PackageInstaller.SessionParams( + PackageInstaller.SessionParams.MODE_FULL_INSTALL); + params.setAppPackageName(packageName); + // set params + int sessionId = packageInstaller.createSession(params); + PackageInstaller.Session session = packageInstaller.openSession(sessionId); + OutputStream out = session.openWrite("COSU", 0, -1); + byte[] buffer = new byte[65536]; + int c; + while ((c = in.read(buffer)) != -1) { + out.write(buffer, 0, c); + } + session.fsync(out); + in.close(); + out.close(); + + session.commit(createIntentSender(context, sessionId, packageName)); + Log.i(TAG, "Installation session committed"); + return null; + + } catch (Exception e) { + Log.w(TAG, "PackageInstaller error: " + e.getMessage()); + e.printStackTrace(); + return e.getMessage(); + } + } + + private static IntentSender createIntentSender(Context context, int sessionId, String packageName) { + Intent intent = new Intent(ACTION_INSTALL_COMPLETE); + if (packageName != null) { + intent.putExtra(PACKAGE_NAME, packageName); + } + + PendingIntent pendingIntent = PendingIntent.getBroadcast( + context, + sessionId, + intent, + PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT + ); + return pendingIntent.getIntentSender(); + } + + public static String getPackageInstallerStatusMessage(int status) { + switch (status) { + case PackageInstaller.STATUS_PENDING_USER_ACTION: + return "PENDING_USER_ACTION"; + case PackageInstaller.STATUS_SUCCESS: + return "SUCCESS"; + case PackageInstaller.STATUS_FAILURE: + return "FAILURE_UNKNOWN"; + case PackageInstaller.STATUS_FAILURE_BLOCKED: + return "BLOCKED"; + case PackageInstaller.STATUS_FAILURE_ABORTED: + return "ABORTED"; + case PackageInstaller.STATUS_FAILURE_INVALID: + return "INVALID"; + case PackageInstaller.STATUS_FAILURE_CONFLICT: + return "CONFLICT"; + case PackageInstaller.STATUS_FAILURE_STORAGE: + return "STORAGE"; + case PackageInstaller.STATUS_FAILURE_INCOMPATIBLE: + return "INCOMPATIBLE"; + default: + return "UNKNOWN"; + } + } +} diff --git a/src/org/lineageos/setupwizard/mdm/ProvisioningQrParser.java b/src/org/lineageos/setupwizard/mdm/ProvisioningQrParser.java new file mode 100644 index 0000000000000000000000000000000000000000..1fb2a5e1ae2ecef4b89fc64b43ea7828deaf635d --- /dev/null +++ b/src/org/lineageos/setupwizard/mdm/ProvisioningQrParser.java @@ -0,0 +1,145 @@ +package org.lineageos.setupwizard.mdm; + +import android.content.Context; +import android.os.PersistableBundle; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.lineageos.setupwizard.R; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +public class ProvisioningQrParser { + + private static final String EXTRA_ADMIN_COMPONENT_NAME = + "android.app.extra.PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME"; + private static final String EXTRA_DOWNLOAD_LOCATION = + "android.app.extra.PROVISIONING_DEVICE_ADMIN_PACKAGE_DOWNLOAD_LOCATION"; + private static final String EXTRA_PACKAGE_CHECKSUM = + "android.app.extra.PROVISIONING_DEVICE_ADMIN_PACKAGE_CHECKSUM"; + private static final String EXTRA_SKIP_ENCRYPTION = + "android.app.extra.PROVISIONING_SKIP_ENCRYPTION"; + private static final String EXTRA_SYSTEM_APPS_ENABLED = + "android.app.extra.PROVISIONING_LEAVE_ALL_SYSTEM_APPS_ENABLED"; + private static final String EXTRA_EXTRAS_BUNDLE = + "android.app.extra.PROVISIONING_ADMIN_EXTRAS_BUNDLE"; + + public interface ErrorHandler { + void handleError(String message); + } + + public class Data { + public String adminComponentName = null; + public String downloadLocation = null; + public String packageChecksum = null; + public boolean skipEncryption = false; + public boolean systemAppsEnabled = false; + public PersistableBundle extrasBundle = null; + } + + private ErrorHandler errorHandler; + + public ProvisioningQrParser(ErrorHandler errorHandler) { + this.errorHandler = errorHandler; + } + + public Data parseProvisioningQr(Context context, String qrContent) { + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode jsonNode; + Data data = new Data(); + + try { + jsonNode = objectMapper.readTree(qrContent); + } catch (Exception e) { + errorHandler.handleError(context.getString(R.string.qr_parse_failed)); + return null; + } + + if (jsonNode.has(EXTRA_ADMIN_COMPONENT_NAME) + && jsonNode.get(EXTRA_ADMIN_COMPONENT_NAME).isTextual()) { + data.adminComponentName = jsonNode.get(EXTRA_ADMIN_COMPONENT_NAME).asText(); + } else { + errorHandler.handleError(context.getString(R.string.qr_missing_parameter) + EXTRA_ADMIN_COMPONENT_NAME); + return null; + } + + if (jsonNode.has(EXTRA_DOWNLOAD_LOCATION) + && jsonNode.get(EXTRA_DOWNLOAD_LOCATION).isTextual()) { + data.downloadLocation = jsonNode.get(EXTRA_DOWNLOAD_LOCATION).asText(); + } else { + errorHandler.handleError(context.getString(R.string.qr_missing_parameter) + EXTRA_DOWNLOAD_LOCATION); + return null; + } + + if (jsonNode.has(EXTRA_PACKAGE_CHECKSUM) + && jsonNode.get(EXTRA_PACKAGE_CHECKSUM).isTextual()) { + data.packageChecksum = jsonNode.get(EXTRA_PACKAGE_CHECKSUM).asText(); + } else { + errorHandler.handleError(context.getString(R.string.qr_missing_parameter) + EXTRA_PACKAGE_CHECKSUM); + return null; + } + + if (jsonNode.has(EXTRA_SKIP_ENCRYPTION) + && jsonNode.get(EXTRA_SKIP_ENCRYPTION).isBoolean()) { + data.skipEncryption = jsonNode.get(EXTRA_SKIP_ENCRYPTION).asBoolean(); + } + + if (jsonNode.has(EXTRA_SYSTEM_APPS_ENABLED) + && jsonNode.get(EXTRA_SYSTEM_APPS_ENABLED).isBoolean()) { + data.systemAppsEnabled = jsonNode.get(EXTRA_SYSTEM_APPS_ENABLED).asBoolean(); + } + + if (jsonNode.has(EXTRA_EXTRAS_BUNDLE) + && jsonNode.get(EXTRA_EXTRAS_BUNDLE).isObject()) { + data.extrasBundle = jsonToPersistableBundle(jsonNode.get(EXTRA_EXTRAS_BUNDLE)); + } + + return data; + } + + private PersistableBundle jsonToPersistableBundle(JsonNode jsonNode) { + PersistableBundle bundle = new PersistableBundle(); + + Iterator> fields = jsonNode.fields(); + while (fields.hasNext()) { + Map.Entry entry = fields.next(); + String key = entry.getKey(); + JsonNode value = entry.getValue(); + + if (value.isTextual()) { + bundle.putString(key, value.asText()); + + } else if (value.isInt()) { + bundle.putInt(key, value.asInt()); + + } else if (value.isLong()) { + bundle.putLong(key, value.asLong()); + + } else if (value.isBoolean()) { + bundle.putBoolean(key, value.asBoolean()); + + } else if (value.isDouble()) { + bundle.putDouble(key, value.asDouble()); + + } else if (value.isObject()) { + // Recursively handle nested objects + bundle.putPersistableBundle(key, jsonToPersistableBundle(value)); + + } else if (value.isArray()) { + // PersistableBundle supports only primitive arrays → use String[] + List list = new ArrayList<>(); + for (JsonNode element : value) { + list.add(element.asText()); + } + bundle.putStringArray(key, list.toArray(new String[0])); + } + } + + return bundle; + } + +} diff --git a/src/org/lineageos/setupwizard/util/SetupWizardUtils.java b/src/org/lineageos/setupwizard/util/SetupWizardUtils.java index cbfa37ac1b04a889622c95c1254b85e324ff60e9..17f6385502577550d4213901a48b81ca5636547d 100644 --- a/src/org/lineageos/setupwizard/util/SetupWizardUtils.java +++ b/src/org/lineageos/setupwizard/util/SetupWizardUtils.java @@ -26,6 +26,7 @@ import static org.lineageos.setupwizard.SetupWizardApp.LOGV; import static org.lineageos.setupwizard.SetupWizardApp.NAVIGATION_OPTION_KEY; import static org.lineageos.setupwizard.SetupWizardApp.UPDATE_RECOVERY_PROP; +import android.app.Activity; import android.app.StatusBarManager; import android.app.WallpaperManager; import android.content.ComponentName; @@ -172,7 +173,7 @@ public class SetupWizardUtils { } } - public static void finishSetupWizard(BaseSetupWizardActivity context) { + public static void finishSetupWizard(Activity context) { if (LOGV) { Log.v(TAG, "finishSetupWizard"); } @@ -200,10 +201,14 @@ public class SetupWizardUtils { disableHome(context); enableStatusBar(); context.finishAffinity(); - context.nextAction(RESULT_SKIP); Log.i(TAG, "Setup complete!"); } + public static void finishSetupWizard(BaseSetupWizardActivity context) { + finishSetupWizard((Activity)context); + context.nextAction(RESULT_SKIP); + } + public static boolean isBluetoothDisabled() { return SystemProperties.getBoolean("config.disable_bluetooth", false); }