diff --git a/Android.bp b/Android.bp index e23e12dc65bfb816758ff29e78c3a81072461cf1..aca2e0cb35ae72e35558291d444eec400058319e 100644 --- a/Android.bp +++ b/Android.bp @@ -29,6 +29,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/org.lineageos.setupwizard.xml b/org.lineageos.setupwizard.xml index a13a20669109242e965c6436f0572c451768af2b..f626b6c144b6202dcc3c64aa6f81af79c5efe7c0 100644 --- a/org.lineageos.setupwizard.xml +++ b/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 16c492e39b91be0d25cee22985d6c4081c83dd8e..91645515c1f856e2dfc4e48d3113fe122a77d8ec 100644 --- a/res/values-de/e_strings.xml +++ b/res/values-de/e_strings.xml @@ -31,4 +31,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 c0061f0b3d7c6108baba62505d542860e46b6705..9b896fa964c4950116b34242328273e7899b14d2 100644 --- a/res/values-es/e_strings.xml +++ b/res/values-es/e_strings.xml @@ -28,4 +28,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 2361efdab86a059418291f374f5b489b8f201504..054fe01c15fa7f8ccaf23fc5afc22456d46b3c6f 100644 --- a/res/values-fr/e_strings.xml +++ b/res/values-fr/e_strings.xml @@ -31,4 +31,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 869444cd54857bdf80951aa8bde70739d776d020..4c8e68e351cccf1e4da91cd9ecc76a55729f8225 100644 --- a/res/values-it/e_strings.xml +++ b/res/values-it/e_strings.xml @@ -28,4 +28,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 b1f2bfad39369075a65432c192e24d11f92d2c61..6d827d60f8834fc8e7695622cb5e07f1ca7f9fcf 100644 --- a/res/values-nl/e_strings.xml +++ b/res/values-nl/e_strings.xml @@ -24,4 +24,35 @@ Inloggen Log in met je e.email of murena.io ID om jouw persoonlijk account te verbinden met deze telefoon. Dit zal synchronisatie inschakelen voor e-mails, contacten, agenda items, afbeeldingen, video\'s, notities en taken tussen deze telefoon en je persoonlijke cloud. Synchroniseer jouw cloud account - \ 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 9cfc996a7c7e0eba8f13b5603afd7c16fb6f2aa3..138e74c30f91e2f1bfe9dc9bcf28c12a7b38c484 100644 --- a/res/values/e_strings.xml +++ b/res/values/e_strings.xml @@ -38,4 +38,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. + + + + %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 47c3747e89650559d8e04f976b4ece409d6f1949..b482a1007783af18ed73c0b3654dcc39d7220682 100644 --- a/src/org/lineageos/setupwizard/NetworkSetupActivity.java +++ b/src/org/lineageos/setupwizard/NetworkSetupActivity.java @@ -26,7 +26,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 fe6ab12fbe4ac44728119c1ef78f0f1019115d9a..ef44641a0dbb0d0e3fc13c4d419f1e0fd2224047 100644 --- a/src/org/lineageos/setupwizard/SubBaseActivity.java +++ b/src/org/lineageos/setupwizard/SubBaseActivity.java @@ -77,7 +77,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 6c7fe364fca73d8db3330e05839404b5b118d330..71b00dca0e2a94904472645597a2c8e5b7daa736 100644 --- a/src/org/lineageos/setupwizard/WelcomeActivity.java +++ b/src/org/lineageos/setupwizard/WelcomeActivity.java @@ -11,12 +11,19 @@ import static org.lineageos.setupwizard.SetupWizardApp.ACTION_EMERGENCY_DIAL; import android.content.Intent; import android.os.Bundle; import android.os.Build; +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; @@ -25,6 +32,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() { } @@ -69,6 +80,83 @@ public class WelcomeActivity extends SubBaseActivity { SetupWizardUtils.finishSetupWizard(WelcomeActivity.this); }); } + + 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 e1e2aaf6792b96bcea154a8465ac33e60a007691..4028db734276f3d7ccb79f99408bc48ea2f8b47a 100644 --- a/src/org/lineageos/setupwizard/util/SetupWizardUtils.java +++ b/src/org/lineageos/setupwizard/util/SetupWizardUtils.java @@ -23,6 +23,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; @@ -169,7 +170,7 @@ public class SetupWizardUtils { } } - public static void finishSetupWizard(BaseSetupWizardActivity context) { + public static void finishSetupWizard(Activity context) { if (LOGV) { Log.v(TAG, "finishSetupWizard"); } @@ -197,10 +198,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); }