From 17a7eef647b193bb1d5b19a129ebf7dfbffc6c31 Mon Sep 17 00:00:00 2001 From: David Braz Date: Sat, 23 Dec 2023 18:19:24 +0000 Subject: [PATCH 001/209] Update translation Portuguese Brazil --- .../src/main/res/values-pt-rBR/plurals.xml | 2 +- .../src/main/res/values-pt-rBR/strings.xml | 88 +++++++++++-------- 2 files changed, 54 insertions(+), 36 deletions(-) diff --git a/play-services-core/src/main/res/values-pt-rBR/plurals.xml b/play-services-core/src/main/res/values-pt-rBR/plurals.xml index 35222efa..ab9eb731 100644 --- a/play-services-core/src/main/res/values-pt-rBR/plurals.xml +++ b/play-services-core/src/main/res/values-pt-rBR/plurals.xml @@ -19,7 +19,7 @@ %1$d aplicativo registrado - %1$d aplicativo registrado + %1$d aplicativos registrados diff --git a/play-services-core/src/main/res/values-pt-rBR/strings.xml b/play-services-core/src/main/res/values-pt-rBR/strings.xml index 3a96eb59..26ec6c6e 100644 --- a/play-services-core/src/main/res/values-pt-rBR/strings.xml +++ b/play-services-core/src/main/res/values-pt-rBR/strings.xml @@ -15,15 +15,16 @@ ~ limitations under the License. --> - Configuração do Vanece microG. + Vanced+ microG + Configurações do Vanced+ microG. Um momento… Gerenciador de Contas do Google Desculpe… "Um aplicativo em seu dispositivo está tentando entrar em uma conta do Google. -Se isso foi intencional, use o Login (ou Huawei, se você tiver um dispositivo com esta marca) para se conectar à página de login do Google, se não, pressione Cancelar para voltar ao aplicativo que fez com que este diálogo aparecesse." - Iniciar sessão - "Estabelecendo uma conexão com os servidores do Google para fazer ‘login’. +Se isso foi intencional, use o botão Entrar (ou Huawei, se você tiver um dispositivo com esta marca) para se conectar à página de login do Google." + Entrar + "Estabelecendo uma conexão com os servidores do Google para entrar em sua conta. Isso pode levar alguns segundos." "Você não tem uma conexão de rede. @@ -35,51 +36,66 @@ Tente novamente mais tarde." "Seu dispositivo está entrando em contato com o Google para salvar informações em sua conta. Isso pode demorar alguns minutos." + Ouvir transmissões de estado interno Escutar mensagens C2DM Enviar mensagens C2DM para outros aplicativos Trocar mensagens e receber notificações de sincronização de servidores do Google Acesso estendido aos serviços do Google - Google Account Manager not fully implemented - Manage your Google account - Manage your Google account information - Manage YouTube activities - Manage YouTube activities such as watch or search history + Gerenciador de Contas do Google não foi totalmente implementado + Gerenciar sua conta do Google + Gerenciar as informações da sua conta do Google + Gerenciar atividades no YouTube + Gerenciar atividades do YouTube, como histórico de exibição ou pesquisa + Registro de dispositivos do Google Cloud Messaging + Ligado Desligado + Avançado Nenhum Veja tudo + Rede móvel Wi-Fi Roaming Outras redes + + Sobre Serviços do Google Mais + Não registrado Último registro: %1$s + Preferências da conta Informações pessoais & privacidade Entrar & segurança + Confiar permissões de aplicativos no Google Quando desativado, o usuário é solicitado antes que uma solicitação de autorização de aplicativos seja enviada ao Google. Alguns aplicativos não usarão a conta do Google se isso for desativado. Permitir que aplicativos encontrem contas Quando ativado, todos os aplicativos deste dispositivo poderão ver o endereço de e-mail de suas contas do Google sem autorização prévia. + Conta Adicionar conta do Google Receba notificações push Cloud Messaging é um provedor de notificação push usado por muitos aplicativos de terceiros. Para usá-lo, você deve ativar ao registro do dispositivo. + Sobre o Vanced+ microG Ocultar ícone no menu + Huawei + Registar dispositivo Registre seu dispositivo nos serviços do Google e cria um identificador de dispositivo exclusivo. Vanced+ microG identifica e tiras bits que não sejam o nome da sua conta do Google a partir de dados de registro. ID do Android Status + Padrão Manual segundos @@ -95,6 +111,7 @@ Isso pode demorar alguns minutos." Mensagens: %1$d (%2$d bytes) Desconectado Conectado à %1$s + Permitir registro Permita que o aplicativo se registre para notificações push. Iniciar aplicativo na mensagem push @@ -104,6 +121,7 @@ Isso pode demorar alguns minutos." Aplicativos registrados Aplicativos não registrados Redes para usar em notificações push + Perfil do dispositivo Selecionar perfil Nativo @@ -114,30 +132,30 @@ Isso pode demorar alguns minutos." Importar perfil personalizado Importar perfil do dispositivo a partir do arquivo Serial - View source code - View project source code on GitHub - To remove signed in Google account, go to Device Settings -> Account. - Check for Vanced+ microG updates on GitHub - Check for updates - If disabled, all connections to Google servers usually done by Vanced+ microG will be denied. This overrides service specific settings. - Allow connecting to Google servers - Requires Orbot to be installed\nNot yet supported - Connect to Google via Tor network - Spoofs signature key of Vanced+ microG to fix YouTube Vanced+ playback buffering issue - Spoof app signature key (EXPERIMENTAL) - Build date - Your data in YouTube - Browse or delete your YouTube activity, and discover how your data makes YouTube and other Google services work better for you - Want to remove your Google account from Vanced+ microG? - Account to remove them.]]> - Check for Vanced+ microG updates on GitLab - View project source code on GitLab - Check connection - Check your device internet connection - Back to YouTube Vanced+ - Tap to exit Vanced+ microG and back to YouTube Vanced - Package information - Manage signed in Google accounts - This will open your device settings ! - Miscellaneous settings + Ver código fonte + Veja o código fonte do projeto no GitHub + Para remover a conta do Google conectada, vá para Configurações do dispositivo -> Conta. + Verifique se há atualizações do Vanced+ microG no GitHub + Verifique se há atualizações + Se desativado, todas as conexões aos servidores do Google normalmente feitas pelo Vanced+ microG serão negadas. Isso substitui as configurações específicas do serviço. + Permitir conexão com servidores do Google + Requer que o Orbot esteja instalado\nAinda não é compatível + Conecte-se ao Google através da rede Tor + Falsificar chave de assinatura do Vanced+ microG para corrigir o problema de buffer de reprodução do YouTube + Falsificar chave de assinatura do aplicativo (EXPERIMENTAL) + Data da Build + Seus dados no YouTube + Navegue ou exclua sua atividade no YouTube e descubra como seus dados fazem com que o YouTube e outros serviços do Google funcionem melhor para você + Deseja remover sua conta do Google do Vanced+ microG? + Conta para removê-las.]]> + Verificar se há atualizações do Vanced + microG no GitLab + Ver código fonte do projeto no GitLab + Verifique a conexão + Verifique a conexão com a Internet do seu dispositivo + Voltar ao YouTube + Toque para sair do Vanced+ microG e voltar ao YouTube + Informações do pacote + Gerenciar contas do Google conectadas + Isso abrirá as configurações do seu dispositivo ! + Configurações diversas From 4367d286a4b7edcbc7911570be9dafe16164beae Mon Sep 17 00:00:00 2001 From: Cuynu Date: Thu, 28 Dec 2023 15:37:32 +0000 Subject: [PATCH 002/209] remove minor version --- build.gradle | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 318c51b6..4c186ed1 100644 --- a/build.gradle +++ b/build.gradle @@ -47,9 +47,8 @@ def gmsVersion = "23.49.14" def gmsVersionCode = Integer.parseInt(gmsVersion.replaceAll('\\.', '')) def ourVersionBase = "0.3.0" -def ourVersionMinor = 73 -def ourVersionCode = gmsVersionCode * 1000 + ourVersionMinor +def ourVersionCode = gmsVersionCode * 1000 def ourVersionName = "$ourVersionBase.$gmsVersionCode" allprojects { From b37074d80eedc372b5629ca741b46eaf75fb5bcd Mon Sep 17 00:00:00 2001 From: Cuynu Date: Sat, 6 Jan 2024 22:24:41 +0000 Subject: [PATCH 003/209] remove donation method --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index a9b81f9a..9807f892 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,6 @@ This fork tweaks MicroG to be usable by applications that require Google authentication such as [YouTube Vanced+](https://gitlab.com/cuynu/ytvancedx) or [ReVanced](https://github.com/revanced), [ReVanced Extended](https://github.com/inotia00) / [ReX](https://github.com/YT-Advanced). -## Donate this project (Not required) -

- Badage - ## Warning! Vanced+ microG probably not work in ReVanced/RVX in future as recent changes from ReVanced/RVX to use with they specific MicroG called ReVanced MicroG with different package name ! From 645dc4ada5886b0acf884eef22ebeac086c61346 Mon Sep 17 00:00:00 2001 From: Cuynu Date: Thu, 11 Jan 2024 00:07:26 +0000 Subject: [PATCH 004/209] bump 23.50.14 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 4c186ed1..f1d01621 100644 --- a/build.gradle +++ b/build.gradle @@ -43,7 +43,7 @@ buildscript { } -def gmsVersion = "23.49.14" +def gmsVersion = "23.50.14" def gmsVersionCode = Integer.parseInt(gmsVersion.replaceAll('\\.', '')) def ourVersionBase = "0.3.0" From 4053850080b9224295a311f94a41c7614f741168 Mon Sep 17 00:00:00 2001 From: Cuynu Date: Thu, 11 Jan 2024 00:10:30 +0000 Subject: [PATCH 005/209] bump 23.50.14 --- .../src/main/res/xml/preferences_google_more.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/play-services-core/src/main/res/xml/preferences_google_more.xml b/play-services-core/src/main/res/xml/preferences_google_more.xml index 7f7a9fdd..06c8c3b6 100755 --- a/play-services-core/src/main/res/xml/preferences_google_more.xml +++ b/play-services-core/src/main/res/xml/preferences_google_more.xml @@ -49,8 +49,8 @@ android:title="@string/package_information" android:summary="Package name : com.mgoogle.android.gms \n Version code : 234914073 \n minSdk : 23 \n targetSdk : 30 - \n GMS version : 23.49.14 - \n build type : release" /> + \n GMS version : 23.50.14 + \n build type : " /> Date: Thu, 11 Jan 2024 00:11:17 +0000 Subject: [PATCH 006/209] revert back to actual base --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index f1d01621..c67d1cef 100644 --- a/build.gradle +++ b/build.gradle @@ -46,7 +46,7 @@ buildscript { def gmsVersion = "23.50.14" def gmsVersionCode = Integer.parseInt(gmsVersion.replaceAll('\\.', '')) -def ourVersionBase = "0.3.0" +def ourVersionBase = "0.2.29" def ourVersionCode = gmsVersionCode * 1000 def ourVersionName = "$ourVersionBase.$gmsVersionCode" From bf5ea4ec0ee22d554f7a40c67726f1daf3f07164 Mon Sep 17 00:00:00 2001 From: Cuynu Date: Sat, 13 Jan 2024 14:54:23 +0700 Subject: [PATCH 007/209] bump v0.3.0.235054 --- build.gradle | 4 ++-- .../src/main/res/xml/preferences_google_more.xml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index c67d1cef..949698a6 100644 --- a/build.gradle +++ b/build.gradle @@ -43,10 +43,10 @@ buildscript { } -def gmsVersion = "23.50.14" +def gmsVersion = "23.50.54" def gmsVersionCode = Integer.parseInt(gmsVersion.replaceAll('\\.', '')) -def ourVersionBase = "0.2.29" +def ourVersionBase = "0.3.0" def ourVersionCode = gmsVersionCode * 1000 def ourVersionName = "$ourVersionBase.$gmsVersionCode" diff --git a/play-services-core/src/main/res/xml/preferences_google_more.xml b/play-services-core/src/main/res/xml/preferences_google_more.xml index 06c8c3b6..6fd704c3 100755 --- a/play-services-core/src/main/res/xml/preferences_google_more.xml +++ b/play-services-core/src/main/res/xml/preferences_google_more.xml @@ -47,9 +47,9 @@ android:selectable="false" android:icon="@drawable/ic_info_outline" android:title="@string/package_information" - android:summary="Package name : com.mgoogle.android.gms \n Version code : 234914073 + android:summary="Package name : com.mgoogle.android.gms \n Version code : 235054000 \n minSdk : 23 \n targetSdk : 30 - \n GMS version : 23.50.14 + \n GMS version : 23.50.54 \n build type : " /> Date: Sat, 13 Jan 2024 15:03:16 +0700 Subject: [PATCH 008/209] Declare com.mgoogle.android.providers.gsf.permission.READ_GSERVICES github microg@833e294 --- play-services-core/src/main/AndroidManifest.xml | 4 ++++ play-services-core/src/main/res/values/strings.xml | 1 + 2 files changed, 5 insertions(+) diff --git a/play-services-core/src/main/AndroidManifest.xml b/play-services-core/src/main/AndroidManifest.xml index fd9bd743..23545666 100644 --- a/play-services-core/src/main/AndroidManifest.xml +++ b/play-services-core/src/main/AndroidManifest.xml @@ -32,6 +32,10 @@ android:label="@string/perm_gtalk_svc_label" android:permissionGroup="android.permission-group.MESSAGES" android:protectionLevel="signature" /> + This can take a couple of minutes." + read Google service configuration listen to internal status broadcasts listen to C2DM messages send C2DM messages to other apps From e63153afe1452a66e31871c4ad4c086f5d35e6f6 Mon Sep 17 00:00:00 2001 From: Cuynu Date: Sat, 13 Jan 2024 15:16:37 +0700 Subject: [PATCH 009/209] use right base package name --- play-services-core/src/main/AndroidManifest.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/play-services-core/src/main/AndroidManifest.xml b/play-services-core/src/main/AndroidManifest.xml index 23545666..da613f0d 100644 --- a/play-services-core/src/main/AndroidManifest.xml +++ b/play-services-core/src/main/AndroidManifest.xml @@ -17,6 +17,10 @@ + - Date: Sat, 13 Jan 2024 15:20:32 +0700 Subject: [PATCH 010/209] modify colors.xml --- play-services-core/src/main/res/values/colors.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/play-services-core/src/main/res/values/colors.xml b/play-services-core/src/main/res/values/colors.xml index 4d85e40c..12402999 100755 --- a/play-services-core/src/main/res/values/colors.xml +++ b/play-services-core/src/main/res/values/colors.xml @@ -16,9 +16,9 @@ - #ffffab40 - #ffffab40 - #ffffab40 + #ff9a82db + #ff9a82db + #ff9a82db #ff9a82db From 029765e8b2432662c665d06b7277386fab6dd790 Mon Sep 17 00:00:00 2001 From: cuynu Date: Mon, 29 Jan 2024 02:06:52 +0700 Subject: [PATCH 011/209] bump 24.03.15 --- build.gradle | 2 +- .../src/main/res/xml/preferences_google_more.xml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 949698a6..59962a20 100644 --- a/build.gradle +++ b/build.gradle @@ -43,7 +43,7 @@ buildscript { } -def gmsVersion = "23.50.54" +def gmsVersion = "24.03.15" def gmsVersionCode = Integer.parseInt(gmsVersion.replaceAll('\\.', '')) def ourVersionBase = "0.3.0" diff --git a/play-services-core/src/main/res/xml/preferences_google_more.xml b/play-services-core/src/main/res/xml/preferences_google_more.xml index 6fd704c3..641708d2 100755 --- a/play-services-core/src/main/res/xml/preferences_google_more.xml +++ b/play-services-core/src/main/res/xml/preferences_google_more.xml @@ -47,9 +47,9 @@ android:selectable="false" android:icon="@drawable/ic_info_outline" android:title="@string/package_information" - android:summary="Package name : com.mgoogle.android.gms \n Version code : 235054000 + android:summary="Package name : com.mgoogle.android.gms \n Version code : 240315000 \n minSdk : 23 \n targetSdk : 30 - \n GMS version : 23.50.54 + \n GMS version : 24.03.15 \n build type : " /> Date: Tue, 30 Jan 2024 00:42:48 +0700 Subject: [PATCH 012/209] new facts --- .../src/main/res/xml/preferences_google_more.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/play-services-core/src/main/res/xml/preferences_google_more.xml b/play-services-core/src/main/res/xml/preferences_google_more.xml index 641708d2..3340b1a2 100755 --- a/play-services-core/src/main/res/xml/preferences_google_more.xml +++ b/play-services-core/src/main/res/xml/preferences_google_more.xml @@ -50,7 +50,7 @@ android:summary="Package name : com.mgoogle.android.gms \n Version code : 240315000 \n minSdk : 23 \n targetSdk : 30 \n GMS version : 24.03.15 - \n build type : " /> + \n build type : " /> + android:summary="Cuynu said they are dummy ✨"> + android:data="https://gitlab.com/cuynu" /> From 9dfe067eed2d2b05b489136d7827281e16bbc0cb Mon Sep 17 00:00:00 2001 From: cuynu Date: Thu, 29 Feb 2024 17:47:52 +0700 Subject: [PATCH 013/209] Bump gms version to 24.07.54 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 59962a20..7ffc2ca0 100644 --- a/build.gradle +++ b/build.gradle @@ -43,7 +43,7 @@ buildscript { } -def gmsVersion = "24.03.15" +def gmsVersion = "24.07.54" def gmsVersionCode = Integer.parseInt(gmsVersion.replaceAll('\\.', '')) def ourVersionBase = "0.3.0" From 8b13b69a22a75b75ffecbc5bbcc884a0ed50d40f Mon Sep 17 00:00:00 2001 From: cuynu Date: Thu, 29 Feb 2024 17:56:49 +0700 Subject: [PATCH 014/209] remove useless facts --- .../src/main/res/xml/preferences_google_more.xml | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/play-services-core/src/main/res/xml/preferences_google_more.xml b/play-services-core/src/main/res/xml/preferences_google_more.xml index 3340b1a2..001083d1 100755 --- a/play-services-core/src/main/res/xml/preferences_google_more.xml +++ b/play-services-core/src/main/res/xml/preferences_google_more.xml @@ -60,16 +60,5 @@ android:action="android.intent.action.VIEW" android:data="https://gitlab.com/cuynu/VancedxMicroG/-/commits/master/?ref_type=HEADS" /> - - - - From badaa4ea66ba3570cdf7b5a8cb14f6802eb2997b Mon Sep 17 00:00:00 2001 From: Cuynu Date: Tue, 19 Mar 2024 23:55:33 +0700 Subject: [PATCH 015/209] refactor: remove pkg info --- .../src/main/res/xml/preferences_google_more.xml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/play-services-core/src/main/res/xml/preferences_google_more.xml b/play-services-core/src/main/res/xml/preferences_google_more.xml index 001083d1..879f0a18 100755 --- a/play-services-core/src/main/res/xml/preferences_google_more.xml +++ b/play-services-core/src/main/res/xml/preferences_google_more.xml @@ -43,14 +43,6 @@ android:targetClass="com.google.android.youtube.HomeActivity" android:targetPackage="com.vancedx.android.youtube" /> - Date: Tue, 19 Mar 2024 23:56:16 +0700 Subject: [PATCH 016/209] feat: bump gms 24.09.13 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 7ffc2ca0..2c713c7c 100644 --- a/build.gradle +++ b/build.gradle @@ -43,7 +43,7 @@ buildscript { } -def gmsVersion = "24.07.54" +def gmsVersion = "24.09.13" def gmsVersionCode = Integer.parseInt(gmsVersion.replaceAll('\\.', '')) def ourVersionBase = "0.3.0" From 3b31ad554814f6fd4ac7935ad325eeb13e23913d Mon Sep 17 00:00:00 2001 From: Cuynu Date: Mon, 25 Mar 2024 03:07:53 +0700 Subject: [PATCH 017/209] bump 24.10.17 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 2c713c7c..0e5cb8fe 100644 --- a/build.gradle +++ b/build.gradle @@ -43,7 +43,7 @@ buildscript { } -def gmsVersion = "24.09.13" +def gmsVersion = "24.10.17" def gmsVersionCode = Integer.parseInt(gmsVersion.replaceAll('\\.', '')) def ourVersionBase = "0.3.0" From b3536a805a418016f3117cfea72c318caf8c0153 Mon Sep 17 00:00:00 2001 From: Cuynu Date: Tue, 26 Mar 2024 15:13:33 +0000 Subject: [PATCH 018/209] --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9807f892..77c8d416 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Vanced+ microG probably not work in ReVanced/RVX in future as recent changes fro ## Notable changes - No longer a system app -- Package name changed from `com.google.android.gms` to `com.mgoogle.android.gms` to support installation alongside the official MicroG +- Package name changed from `com.google.android.gms` to `com.mgoogle.android.gms` to support installation alongside the official MicroG or device with Google Play Services pre-installed - Removed unnecessary features: - Ads - Analytics From ae20e47f56a5180dfb9a05531b4884feabffdf5e Mon Sep 17 00:00:00 2001 From: Cuynu Date: Thu, 28 Mar 2024 12:17:51 +0000 Subject: [PATCH 019/209] --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 77c8d416..713453d0 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # Vanced+ MicroG -This fork tweaks MicroG to be usable by applications that require Google authentication such as [YouTube Vanced+](https://gitlab.com/cuynu/ytvancedx) or [ReVanced](https://github.com/revanced), [ReVanced Extended](https://github.com/inotia00) / [ReX](https://github.com/YT-Advanced). +This fork tweaks MicroG to be usable by applications that require Google authentication such as [YouTube Vanced+](https://gitlab.com/cuynu/ytvancedx). ## Warning! -Vanced+ microG probably not work in ReVanced/RVX in future as recent changes from ReVanced/RVX to use with they specific MicroG called ReVanced MicroG with different package name ! +Vanced+ microG probably does not works in ReVanced/RVX in future as recent changes from ReVanced/RVX to use with they specific MicroG called ReVanced MicroG with different package name ! ## Notable changes From e16080bce2177d28130d58faf92ed7274cba581e Mon Sep 17 00:00:00 2001 From: DaVinci9196 <150454414+DaVinci9196@users.noreply.github.com> Date: Thu, 4 Jan 2024 21:35:08 +0800 Subject: [PATCH 020/209] Add PoToken service (#2129) --- .../google/android/gms/potokens/PoToken.aidl | 8 + .../potokens/internal/IPoTokensService.aidl | 14 ++ .../potokens/internal/ITokenCallbacks.aidl | 13 ++ .../google/android/gms/potokens/PoToken.java | 20 ++ .../src/main/proto/potoken.proto | 63 +++++ .../src/main/AndroidManifest.xml | 8 + .../potokens/internal/PoTokensApiService.kt | 32 +++ .../internal/PoTokensApiServiceImpl.kt | 41 ++++ .../gms/potokens/utils/PoTokenConstants.kt | 19 ++ .../gms/potokens/utils/PoTokenHelper.kt | 217 ++++++++++++++++++ .../gms/potokens/utils/PoTokenPreferences.kt | 47 ++++ 11 files changed, 482 insertions(+) create mode 100644 play-services-api/src/main/aidl/com/google/android/gms/potokens/PoToken.aidl create mode 100644 play-services-api/src/main/aidl/com/google/android/gms/potokens/internal/IPoTokensService.aidl create mode 100644 play-services-api/src/main/aidl/com/google/android/gms/potokens/internal/ITokenCallbacks.aidl create mode 100644 play-services-api/src/main/java/com/google/android/gms/potokens/PoToken.java create mode 100644 play-services-core-proto/src/main/proto/potoken.proto create mode 100644 play-services-core/src/main/kotlin/com/google/android/gms/potokens/internal/PoTokensApiService.kt create mode 100644 play-services-core/src/main/kotlin/com/google/android/gms/potokens/internal/PoTokensApiServiceImpl.kt create mode 100644 play-services-core/src/main/kotlin/com/google/android/gms/potokens/utils/PoTokenConstants.kt create mode 100644 play-services-core/src/main/kotlin/com/google/android/gms/potokens/utils/PoTokenHelper.kt create mode 100644 play-services-core/src/main/kotlin/com/google/android/gms/potokens/utils/PoTokenPreferences.kt diff --git a/play-services-api/src/main/aidl/com/google/android/gms/potokens/PoToken.aidl b/play-services-api/src/main/aidl/com/google/android/gms/potokens/PoToken.aidl new file mode 100644 index 00000000..8cf0bb7e --- /dev/null +++ b/play-services-api/src/main/aidl/com/google/android/gms/potokens/PoToken.aidl @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.potokens; + +parcelable PoToken; \ No newline at end of file diff --git a/play-services-api/src/main/aidl/com/google/android/gms/potokens/internal/IPoTokensService.aidl b/play-services-api/src/main/aidl/com/google/android/gms/potokens/internal/IPoTokensService.aidl new file mode 100644 index 00000000..938c848a --- /dev/null +++ b/play-services-api/src/main/aidl/com/google/android/gms/potokens/internal/IPoTokensService.aidl @@ -0,0 +1,14 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.potokens.internal; + +import com.google.android.gms.common.api.internal.IStatusCallback; +import com.google.android.gms.potokens.internal.ITokenCallbacks; + +interface IPoTokensService { + void responseStatus(IStatusCallback call, int code) = 1; + void responseStatusToken(ITokenCallbacks call, int i, in byte[] bArr) = 2; +} \ No newline at end of file diff --git a/play-services-api/src/main/aidl/com/google/android/gms/potokens/internal/ITokenCallbacks.aidl b/play-services-api/src/main/aidl/com/google/android/gms/potokens/internal/ITokenCallbacks.aidl new file mode 100644 index 00000000..55c71dfe --- /dev/null +++ b/play-services-api/src/main/aidl/com/google/android/gms/potokens/internal/ITokenCallbacks.aidl @@ -0,0 +1,13 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.potokens.internal; + +import com.google.android.gms.common.api.Status; +import com.google.android.gms.potokens.PoToken; + +interface ITokenCallbacks { + void responseToken(in Status status, in PoToken token) = 1; +} \ No newline at end of file diff --git a/play-services-api/src/main/java/com/google/android/gms/potokens/PoToken.java b/play-services-api/src/main/java/com/google/android/gms/potokens/PoToken.java new file mode 100644 index 00000000..9f5a3f4c --- /dev/null +++ b/play-services-api/src/main/java/com/google/android/gms/potokens/PoToken.java @@ -0,0 +1,20 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.potokens; + +import org.microg.safeparcel.AutoSafeParcelable; + +public class PoToken extends AutoSafeParcelable { + + @Field(1) + public byte[] data; + + public PoToken(byte[] data) { + this.data = data; + } + + public static Creator CREATOR = findCreator(PoToken.class); +} diff --git a/play-services-core-proto/src/main/proto/potoken.proto b/play-services-core-proto/src/main/proto/potoken.proto new file mode 100644 index 00000000..98475912 --- /dev/null +++ b/play-services-core-proto/src/main/proto/potoken.proto @@ -0,0 +1,63 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +option java_package = "com.google.android.gms.potokens"; + +option java_multiple_files = true; + +message CipherKey { + optional int32 key = 1; + optional bytes value = 3; +} + +message KeyData { + optional string typeUrl = 1; + optional CipherKey value = 2; + optional int32 keyMaterialType = 3; +} + +message Key { + optional KeyData data = 1; + optional int32 status = 2; + optional int32 keyId = 3; + optional int32 outputPrefixType = 4; +} + +message KeySet { + optional int32 keyId = 1; + repeated Key keyList = 2; +} + +message PoTokenInfo { + optional int32 key = 6; + optional int32 time = 1; + optional bytes inputData = 2; + optional string pkgName = 3; + optional bytes pkgSignSha256 = 4; + optional bytes tokenData = 5; +} + +message GetPoIntegrityTokenRequest { + optional int32 mode = 1; + optional bytes dgResult = 2; + optional bytes dgRandKey = 3; +} + +message GetPoIntegrityTokenResponse { + optional bytes desc = 1; + optional int32 code = 2; + optional bytes backup = 3; +// optional bytes d = 4; +// optional bytes e = 5; +} + +message PoTokenResult { + optional bytes encryptData = 1; + optional bytes tokenData = 2; +} + +message PoTokenResultWrap { + optional PoTokenResult data = 1; +} diff --git a/play-services-core/src/main/AndroidManifest.xml b/play-services-core/src/main/AndroidManifest.xml index da613f0d..175f0256 100644 --- a/play-services-core/src/main/AndroidManifest.xml +++ b/play-services-core/src/main/AndroidManifest.xml @@ -394,6 +394,14 @@ + + + + + + + + + ): String? { + try { + val resultTask = DroidGuard.getClient(context).getResults(PoTokenConstants.KEY_TOKEN, map, request) + return Tasks.await(resultTask, 15, TimeUnit.SECONDS) + } catch (e: Throwable) { + Log.w(TAG, "PoTokenHelper getDroidGuardResult exception: $e") + } + return null + } + + private fun getPoIntegrityToken(context: Context, keySet: KeySet): GetPoIntegrityTokenResponse? { + try { + val droidGuardResultsRequest = DroidGuardResultsRequest().apply { + bundle.putByteArray(PoTokenConstants.KEY_FAST, keySet.encode()) + bundle.putStringArrayList(PoTokenConstants.KEY_FALLBACK, arrayListOf(PoTokenConstants.KEY_FAST)) + } + val randKeyBuf = ByteArray(0x20).also { SecureRandom().nextBytes(it) } + val randKey = Base64.encodeToString(randKeyBuf, Base64.NO_WRAP) + val map: MutableMap = HashMap() + map["b"] = randKey + val dgResult = + getDroidGuardResult(context, droidGuardResultsRequest, map)?.encodeToByteArray()?.toByteString() + val tokenRequest = + GetPoIntegrityTokenRequest(dgResult = dgResult, dgRandKey = randKeyBuf.toByteString(), mode = 1) + return postPoTokenForGms(tokenRequest) + } catch (e: Throwable) { + Log.w(TAG, "PoTokenHelper getPoIntegrityToken exception: $e") + } + return null + } + + private fun postPoTokenForGms(request: GetPoIntegrityTokenRequest): GetPoIntegrityTokenResponse? { + val future = RequestFuture.newFuture() + volleyQueue.add(object : Request( + Method.POST, PoTokenConstants.TOKEN_URL, future + ) { + + override fun deliverResponse(response: GetPoIntegrityTokenResponse?) { + Log.d(TAG, "PoTokenHelper postPoTokenForGms response: $response") + future.onResponse(response) + } + + override fun getHeaders(): Map { + return mapOf("User-Agent" to "GmsCore/${Constants.GMS_VERSION_CODE} (${Build.DEVICE} ${Build.ID}); gzip") + } + + override fun getBody(): ByteArray { + return request.encode() + } + + override fun getBodyContentType(): String = "application/x-protobuf" + + override fun parseNetworkResponse(response: NetworkResponse): Response { + return try { + Response.success(GetPoIntegrityTokenResponse.ADAPTER.decode(response.data), null) + } catch (e: Exception) { + Response.error(VolleyError(e)) + } + } + }) + return future.get() + } + + suspend fun callPoToken(context: Context, packageName: String, inputData: ByteArray): ByteArray { + var tokenDesc = PoTokenPreferences.get(context).getString(PoTokenConstants.KEY_DESC, "") + var tokenBackup = PoTokenPreferences.get(context).getString(PoTokenConstants.KEY_BACKUP, "") + var keySetStr = PoTokenPreferences.get(context).getString(PoTokenConstants.KEY_SET_STR, "") + + val keySet = + if (TextUtils.isEmpty(tokenDesc) || TextUtils.isEmpty(tokenBackup) || TextUtils.isEmpty(keySetStr)) { + buildKeySet().also { + Log.d(TAG, "PoTokenHelper postPoTokenForGms start") + val response = withContext(Dispatchers.IO) { getPoIntegrityToken(context, it) } + Log.d(TAG, "PoTokenHelper postPoTokenForGms end") + tokenDesc = Base64.encodeToString(response?.desc?.toByteArray(), Base64.DEFAULT) + tokenBackup = Base64.encodeToString(response?.backup?.toByteArray(), Base64.DEFAULT) + keySetStr = Base64.encodeToString(it.encode(), Base64.DEFAULT) + PoTokenPreferences.get(context).save(PoTokenConstants.KEY_DESC, tokenDesc) + PoTokenPreferences.get(context).save(PoTokenConstants.KEY_BACKUP, tokenBackup) + PoTokenPreferences.get(context).save(PoTokenConstants.KEY_SET_STR, keySetStr) + } + } else { + val result = Base64.decode(keySetStr, Base64.DEFAULT) + KeySet.ADAPTER.decode(result) + } + + val poTokenInfoData: ByteArray = PoTokenInfo( + inputData = inputData.toByteString(), + pkgName = packageName, + pkgSignSha256 = context.packageManager.getFirstSignatureDigest(packageName, "SHA-256")?.toByteString(), + tokenData = Base64.decode(tokenDesc, Base64.DEFAULT).toByteString() + ).encode() + + val keyObj: Key = keySet.keyList[0] + val keyId: Int? = keyObj.keyId + val key: ByteArray? = keyObj.data_?.value_?.value_?.toByteArray() + val iv = ByteArray(12).also { SecureRandom().nextBytes(it) } + + var data = aesEncrypt(key, iv, poTokenInfoData) + keyId?.let { data = concatCipherIdentifier(it, data) } + + val poTokenResult = PoTokenResult( + encryptData = data.toByteString(), + tokenData = Base64.decode(tokenBackup, Base64.DEFAULT).toByteString() + ) + + return PoTokenResultWrap(poTokenResult).encode() + } + + companion object { + @Volatile + private var instance: PoTokenHelper? = null + fun get(context: Context): PoTokenHelper { + return instance ?: synchronized(this) { + instance ?: PoTokenHelper(context).also { instance = it } + } + } + } + +} diff --git a/play-services-core/src/main/kotlin/com/google/android/gms/potokens/utils/PoTokenPreferences.kt b/play-services-core/src/main/kotlin/com/google/android/gms/potokens/utils/PoTokenPreferences.kt new file mode 100644 index 00000000..3eca2cd3 --- /dev/null +++ b/play-services-core/src/main/kotlin/com/google/android/gms/potokens/utils/PoTokenPreferences.kt @@ -0,0 +1,47 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.potokens.utils + +import android.content.Context +import android.content.SharedPreferences +import com.android.volley.toolbox.Volley +import org.microg.gms.droidguard.core.HandleProxyFactory + +class PoTokenPreferences(context: Context) { + + private val sp: SharedPreferences = + context.getSharedPreferences("com.google.android.gms.potokens", Context.MODE_PRIVATE) + + fun save(key: String, value: Any?) { + when (value) { + is String -> { + sp.edit().putString(key, value).apply() + } + + is Int -> { + sp.edit().putInt(key, value).apply() + } + } + } + + fun getString(key: String, defValue: String?): String? { + return sp.getString(key, defValue) + } + + fun getInt(key: String, defValue: Int): Int { + return sp.getInt(key, defValue) + } + + companion object { + @Volatile + private var instance: PoTokenPreferences? = null + fun get(context: Context): PoTokenPreferences { + return instance ?: synchronized(this) { + instance ?: PoTokenPreferences(context).also { instance = it } + } + } + } +} \ No newline at end of file From 9890ccebbb46b6d3e6b8a5b227a7cd86766c2502 Mon Sep 17 00:00:00 2001 From: DaVinci9196 <150454414+DaVinci9196@users.noreply.github.com> Date: Thu, 4 Jan 2024 21:39:48 +0800 Subject: [PATCH 021/209] Add identity sign-in service (#2130) --- .../auth/api/identity/BeginSignInRequest.aidl | 8 + .../auth/api/identity/BeginSignInResult.aidl | 8 + .../GetPhoneNumberHintIntentRequest.aidl | 8 + .../api/identity/GetSignInIntentRequest.aidl | 8 + .../auth/api/identity/SignInCredential.aidl | 8 + .../internal/IBeginSignInCallback.aidl | 13 ++ .../IGetPhoneNumberHintIntentCallback.aidl | 12 ++ .../internal/IGetSignInIntentCallback.aidl | 12 ++ .../api/identity/internal/ISignInService.aidl | 21 ++ .../auth/api/identity/BeginSignInRequest.java | 35 ++++ .../auth/api/identity/BeginSignInResult.java | 28 +++ .../GetPhoneNumberHintIntentRequest.java | 27 +++ .../api/identity/GetSignInIntentRequest.java | 38 ++++ .../identity/GoogleIdTokenRequestOptions.java | 41 ++++ .../api/identity/PasswordRequestOptions.java | 27 +++ .../auth/api/identity/SignInCredential.java | 41 ++++ .../org/microg/gms/auth/AuthConstants.java | 26 --- .../src/main/AndroidManifest.xml | 180 ++++++++++++++++++ .../identity/IdentitySignInService.kt | 89 +++++++++ 19 files changed, 604 insertions(+), 26 deletions(-) create mode 100644 play-services-auth/src/main/aidl/com/google/android/gms/auth/api/identity/BeginSignInRequest.aidl create mode 100644 play-services-auth/src/main/aidl/com/google/android/gms/auth/api/identity/BeginSignInResult.aidl create mode 100644 play-services-auth/src/main/aidl/com/google/android/gms/auth/api/identity/GetPhoneNumberHintIntentRequest.aidl create mode 100644 play-services-auth/src/main/aidl/com/google/android/gms/auth/api/identity/GetSignInIntentRequest.aidl create mode 100644 play-services-auth/src/main/aidl/com/google/android/gms/auth/api/identity/SignInCredential.aidl create mode 100644 play-services-auth/src/main/aidl/com/google/android/gms/auth/api/identity/internal/IBeginSignInCallback.aidl create mode 100644 play-services-auth/src/main/aidl/com/google/android/gms/auth/api/identity/internal/IGetPhoneNumberHintIntentCallback.aidl create mode 100644 play-services-auth/src/main/aidl/com/google/android/gms/auth/api/identity/internal/IGetSignInIntentCallback.aidl create mode 100644 play-services-auth/src/main/aidl/com/google/android/gms/auth/api/identity/internal/ISignInService.aidl create mode 100644 play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/BeginSignInRequest.java create mode 100644 play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/BeginSignInResult.java create mode 100644 play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/GetPhoneNumberHintIntentRequest.java create mode 100644 play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/GetSignInIntentRequest.java create mode 100644 play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/GoogleIdTokenRequestOptions.java create mode 100644 play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/PasswordRequestOptions.java create mode 100644 play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/SignInCredential.java delete mode 100644 play-services-basement/src/main/java/org/microg/gms/auth/AuthConstants.java create mode 100644 play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/identity/IdentitySignInService.kt diff --git a/play-services-auth/src/main/aidl/com/google/android/gms/auth/api/identity/BeginSignInRequest.aidl b/play-services-auth/src/main/aidl/com/google/android/gms/auth/api/identity/BeginSignInRequest.aidl new file mode 100644 index 00000000..843cdbfa --- /dev/null +++ b/play-services-auth/src/main/aidl/com/google/android/gms/auth/api/identity/BeginSignInRequest.aidl @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.auth.api.identity; + +parcelable BeginSignInRequest; \ No newline at end of file diff --git a/play-services-auth/src/main/aidl/com/google/android/gms/auth/api/identity/BeginSignInResult.aidl b/play-services-auth/src/main/aidl/com/google/android/gms/auth/api/identity/BeginSignInResult.aidl new file mode 100644 index 00000000..994924a7 --- /dev/null +++ b/play-services-auth/src/main/aidl/com/google/android/gms/auth/api/identity/BeginSignInResult.aidl @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.auth.api.identity; + +parcelable BeginSignInResult; \ No newline at end of file diff --git a/play-services-auth/src/main/aidl/com/google/android/gms/auth/api/identity/GetPhoneNumberHintIntentRequest.aidl b/play-services-auth/src/main/aidl/com/google/android/gms/auth/api/identity/GetPhoneNumberHintIntentRequest.aidl new file mode 100644 index 00000000..fc8071fc --- /dev/null +++ b/play-services-auth/src/main/aidl/com/google/android/gms/auth/api/identity/GetPhoneNumberHintIntentRequest.aidl @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.auth.api.identity; + +parcelable GetPhoneNumberHintIntentRequest; \ No newline at end of file diff --git a/play-services-auth/src/main/aidl/com/google/android/gms/auth/api/identity/GetSignInIntentRequest.aidl b/play-services-auth/src/main/aidl/com/google/android/gms/auth/api/identity/GetSignInIntentRequest.aidl new file mode 100644 index 00000000..69650b83 --- /dev/null +++ b/play-services-auth/src/main/aidl/com/google/android/gms/auth/api/identity/GetSignInIntentRequest.aidl @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.auth.api.identity; + +parcelable GetSignInIntentRequest; \ No newline at end of file diff --git a/play-services-auth/src/main/aidl/com/google/android/gms/auth/api/identity/SignInCredential.aidl b/play-services-auth/src/main/aidl/com/google/android/gms/auth/api/identity/SignInCredential.aidl new file mode 100644 index 00000000..d2de2746 --- /dev/null +++ b/play-services-auth/src/main/aidl/com/google/android/gms/auth/api/identity/SignInCredential.aidl @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.auth.api.identity; + +parcelable SignInCredential; \ No newline at end of file diff --git a/play-services-auth/src/main/aidl/com/google/android/gms/auth/api/identity/internal/IBeginSignInCallback.aidl b/play-services-auth/src/main/aidl/com/google/android/gms/auth/api/identity/internal/IBeginSignInCallback.aidl new file mode 100644 index 00000000..41b3e006 --- /dev/null +++ b/play-services-auth/src/main/aidl/com/google/android/gms/auth/api/identity/internal/IBeginSignInCallback.aidl @@ -0,0 +1,13 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.auth.api.identity.internal; + +import com.google.android.gms.auth.api.identity.BeginSignInResult; +import com.google.android.gms.common.api.Status; + +interface IBeginSignInCallback { + void onResult(in Status status, in BeginSignInResult beginSignInResult); +} \ No newline at end of file diff --git a/play-services-auth/src/main/aidl/com/google/android/gms/auth/api/identity/internal/IGetPhoneNumberHintIntentCallback.aidl b/play-services-auth/src/main/aidl/com/google/android/gms/auth/api/identity/internal/IGetPhoneNumberHintIntentCallback.aidl new file mode 100644 index 00000000..a07988a2 --- /dev/null +++ b/play-services-auth/src/main/aidl/com/google/android/gms/auth/api/identity/internal/IGetPhoneNumberHintIntentCallback.aidl @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.auth.api.identity.internal; + +import com.google.android.gms.common.api.Status; + +interface IGetPhoneNumberHintIntentCallback { + void onResult(in Status status, in PendingIntent pendingIntent); +} \ No newline at end of file diff --git a/play-services-auth/src/main/aidl/com/google/android/gms/auth/api/identity/internal/IGetSignInIntentCallback.aidl b/play-services-auth/src/main/aidl/com/google/android/gms/auth/api/identity/internal/IGetSignInIntentCallback.aidl new file mode 100644 index 00000000..ba6c5850 --- /dev/null +++ b/play-services-auth/src/main/aidl/com/google/android/gms/auth/api/identity/internal/IGetSignInIntentCallback.aidl @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.auth.api.identity.internal; + +import com.google.android.gms.common.api.Status; + +interface IGetSignInIntentCallback { + void onResult(in Status status, in PendingIntent pendingIntent); +} \ No newline at end of file diff --git a/play-services-auth/src/main/aidl/com/google/android/gms/auth/api/identity/internal/ISignInService.aidl b/play-services-auth/src/main/aidl/com/google/android/gms/auth/api/identity/internal/ISignInService.aidl new file mode 100644 index 00000000..cbb5bb66 --- /dev/null +++ b/play-services-auth/src/main/aidl/com/google/android/gms/auth/api/identity/internal/ISignInService.aidl @@ -0,0 +1,21 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.auth.api.identity.internal; + +import com.google.android.gms.common.api.internal.IStatusCallback; +import com.google.android.gms.auth.api.identity.internal.IBeginSignInCallback; +import com.google.android.gms.auth.api.identity.internal.IGetSignInIntentCallback; +import com.google.android.gms.auth.api.identity.internal.IGetPhoneNumberHintIntentCallback; +import com.google.android.gms.auth.api.identity.BeginSignInRequest; +import com.google.android.gms.auth.api.identity.GetSignInIntentRequest; +import com.google.android.gms.auth.api.identity.GetPhoneNumberHintIntentRequest; + +interface ISignInService { + void beginSignIn(in IBeginSignInCallback callback, in BeginSignInRequest request) = 0; + void signOut(in IStatusCallback callback, String userId) = 1; + void getSignInIntent(in IGetSignInIntentCallback callback, in GetSignInIntentRequest request) = 2; + void getPhoneNumberHintIntent(in IGetPhoneNumberHintIntentCallback callback, in GetPhoneNumberHintIntentRequest request) = 3; +} \ No newline at end of file diff --git a/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/BeginSignInRequest.java b/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/BeginSignInRequest.java new file mode 100644 index 00000000..22d74516 --- /dev/null +++ b/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/BeginSignInRequest.java @@ -0,0 +1,35 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.auth.api.identity; + +import android.os.Parcel; + +import androidx.annotation.NonNull; + +import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; + +@SafeParcelable.Class +public class BeginSignInRequest extends AbstractSafeParcelable { + @Field(1) + public PasswordRequestOptions passwordRequestOptions; + @Field(2) + public GoogleIdTokenRequestOptions googleIdTokenRequestOptions; + @Field(3) + public String score; + @Field(4) + public boolean isPrimary; + @Field(5) + public int code; + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } + + public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(BeginSignInRequest.class); +} diff --git a/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/BeginSignInResult.java b/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/BeginSignInResult.java new file mode 100644 index 00000000..7c9b887d --- /dev/null +++ b/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/BeginSignInResult.java @@ -0,0 +1,28 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.auth.api.identity; + +import android.app.PendingIntent; +import android.os.Parcel; + +import androidx.annotation.NonNull; + +import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; + +@SafeParcelable.Class +public class BeginSignInResult extends AbstractSafeParcelable { + @Field(1) + public PendingIntent pendingIntent; + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } + + public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(BeginSignInResult.class); +} diff --git a/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/GetPhoneNumberHintIntentRequest.java b/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/GetPhoneNumberHintIntentRequest.java new file mode 100644 index 00000000..11343886 --- /dev/null +++ b/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/GetPhoneNumberHintIntentRequest.java @@ -0,0 +1,27 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.auth.api.identity; + +import android.os.Parcel; + +import androidx.annotation.NonNull; + +import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; + +@SafeParcelable.Class +public class GetPhoneNumberHintIntentRequest extends AbstractSafeParcelable { + @Field(1) + public int code; + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } + + public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(GetPhoneNumberHintIntentRequest.class); +} diff --git a/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/GetSignInIntentRequest.java b/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/GetSignInIntentRequest.java new file mode 100644 index 00000000..bbb02c78 --- /dev/null +++ b/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/GetSignInIntentRequest.java @@ -0,0 +1,38 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.auth.api.identity; + +import android.os.Parcel; + +import androidx.annotation.NonNull; + +import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; + +@SafeParcelable.Class +public class GetSignInIntentRequest extends AbstractSafeParcelable { + @Field(1) + public String clientId; + @Field(2) + public String scope; + @Field(3) + public String requestTag; + @Field(4) + public String requestToken; + @Field(5) + public boolean isPrimary; + @Field(6) + public int code; + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } + + public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(GetSignInIntentRequest.class); + +} diff --git a/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/GoogleIdTokenRequestOptions.java b/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/GoogleIdTokenRequestOptions.java new file mode 100644 index 00000000..ffdf5c5e --- /dev/null +++ b/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/GoogleIdTokenRequestOptions.java @@ -0,0 +1,41 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.auth.api.identity; + +import android.os.Parcel; + +import androidx.annotation.NonNull; + +import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; + +import java.util.ArrayList; + +@SafeParcelable.Class +public class GoogleIdTokenRequestOptions extends AbstractSafeParcelable { + @Field(1) + public boolean idTokenRequested; + @Field(2) + public String clientId; + @Field(3) + public String requestToken; + @Field(4) + public boolean serverAuthCodeRequested; + @Field(5) + public String serverClientId; + @Field(6) + public ArrayList scopes; + @Field(7) + public boolean forceCodeForRefreshToken; + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } + + public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(GoogleIdTokenRequestOptions.class); +} diff --git a/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/PasswordRequestOptions.java b/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/PasswordRequestOptions.java new file mode 100644 index 00000000..dfca1e06 --- /dev/null +++ b/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/PasswordRequestOptions.java @@ -0,0 +1,27 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.auth.api.identity; + +import android.os.Parcel; + +import androidx.annotation.NonNull; + +import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; + +@SafeParcelable.Class +public class PasswordRequestOptions extends AbstractSafeParcelable { + @Field(1) + public boolean primary; + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } + + public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(PasswordRequestOptions.class); +} diff --git a/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/SignInCredential.java b/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/SignInCredential.java new file mode 100644 index 00000000..5d4c8d38 --- /dev/null +++ b/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/SignInCredential.java @@ -0,0 +1,41 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.auth.api.identity; + +import android.os.Parcel; + +import androidx.annotation.NonNull; + +import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; + +@SafeParcelable.Class +public class SignInCredential extends AbstractSafeParcelable { + @Field(1) + public String email; + @Field(2) + public String displayName; + @Field(3) + public String familyName; + @Field(4) + public String givenName; + @Field(5) + public String avatar; + @Field(6) + public String serverAuthCode; + @Field(7) + public String idToken; + @Field(8) + public String obfuscatedIdentifier; + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } + + public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(SignInCredential.class); +} diff --git a/play-services-basement/src/main/java/org/microg/gms/auth/AuthConstants.java b/play-services-basement/src/main/java/org/microg/gms/auth/AuthConstants.java deleted file mode 100644 index 774f5756..00000000 --- a/play-services-basement/src/main/java/org/microg/gms/auth/AuthConstants.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright (C) 2013-2017 microG Project Team - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.microg.gms.auth; - -public class AuthConstants { - public static final String SCOPE_GET_ACCOUNT_ID = "^^_account_id_^^"; - public static final String PROVIDER_METHOD_GET_ACCOUNTS = "get_accounts"; - public static final String PROVIDER_METHOD_CLEAR_PASSWORD = "clear_password"; - public static final String PROVIDER_EXTRA_CLEAR_PASSWORD = "clear_password"; - public static final String PROVIDER_EXTRA_ACCOUNTS = "accounts"; - public static final String DEFAULT_ACCOUNT_TYPE = "com.mgoogle"; -} diff --git a/play-services-core/src/main/AndroidManifest.xml b/play-services-core/src/main/AndroidManifest.xml index 175f0256..cec102fb 100644 --- a/play-services-core/src/main/AndroidManifest.xml +++ b/play-services-core/src/main/AndroidManifest.xml @@ -319,6 +319,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + >>>>>> 048a6e59 (Add identity sign-in service (#2130)) android:name="org.microg.gms.ui.SettingsActivity" android:icon="@mipmap/ic_launcher" android:label="@string/gms_settings_name" @@ -416,6 +529,73 @@ android:exported="true"> +<<<<<<< HEAD +======= + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +>>>>>>> 048a6e59 (Add identity sign-in service (#2130)) diff --git a/play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/identity/IdentitySignInService.kt b/play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/identity/IdentitySignInService.kt new file mode 100644 index 00000000..bf09ec58 --- /dev/null +++ b/play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/identity/IdentitySignInService.kt @@ -0,0 +1,89 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.auth.credentials.identity + +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.util.Log +import com.google.android.gms.auth.api.identity.BeginSignInRequest +import com.google.android.gms.auth.api.identity.GetPhoneNumberHintIntentRequest +import com.google.android.gms.auth.api.identity.GetSignInIntentRequest +import com.google.android.gms.auth.api.identity.internal.IBeginSignInCallback +import com.google.android.gms.auth.api.identity.internal.IGetPhoneNumberHintIntentCallback +import com.google.android.gms.auth.api.identity.internal.IGetSignInIntentCallback +import com.google.android.gms.auth.api.identity.internal.ISignInService +import com.google.android.gms.auth.api.signin.GoogleSignInOptions +import com.google.android.gms.auth.api.signin.internal.SignInConfiguration +import com.google.android.gms.common.ConnectionResult +import com.google.android.gms.common.api.Status +import com.google.android.gms.common.api.internal.IStatusCallback +import com.google.android.gms.common.internal.ConnectionInfo +import com.google.android.gms.common.internal.GetServiceRequest +import com.google.android.gms.common.internal.IGmsCallbacks +import org.microg.gms.BaseService +import org.microg.gms.auth.credentials.FEATURES +import org.microg.gms.auth.signin.AuthSignInActivity +import org.microg.gms.common.Constants +import org.microg.gms.common.GmsService + +const val TAG = "IdentitySignInService" + +class IdentitySignInService : BaseService(TAG, GmsService.IDENTITY_SIGN_IN) { + + override fun handleServiceRequest(callback: IGmsCallbacks, request: GetServiceRequest, service: GmsService) { + val connectionInfo = ConnectionInfo() + connectionInfo.features = FEATURES + callback.onPostInitCompleteWithConnectionInfo( + ConnectionResult.SUCCESS, + IdentitySignInServiceImpl(this, request.packageName).asBinder(), + connectionInfo + ) + } +} + +class IdentitySignInServiceImpl(private val mContext: Context, private val clientPackageName: String) : + ISignInService.Stub() { + override fun beginSignIn(callback: IBeginSignInCallback, request: BeginSignInRequest) { + Log.d(TAG, "method 'beginSignIn' not fully implemented, return status is CANCELED") + callback.onResult(Status.CANCELED, null) + } + + override fun signOut(callback: IStatusCallback, requestTag: String) { + Log.d(TAG, "method signOut called, requestTag=$requestTag") + callback.onResult(Status.SUCCESS) + } + + override fun getSignInIntent( + callback: IGetSignInIntentCallback, + request: GetSignInIntentRequest + ) { + val signInConfiguration = SignInConfiguration().apply { + options = GoogleSignInOptions.Builder() + .requestIdToken(request.clientId) + .requestId() + .requestEmail() + .requestProfile() + .build() + packageName = clientPackageName + } + val intent = Intent(mContext, AuthSignInActivity::class.java).apply { + `package` = Constants.GMS_PACKAGE_NAME + putExtra("config", signInConfiguration) + } + val flags = PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_UPDATE_CURRENT + val activity = PendingIntent.getActivity(mContext, 0, intent, flags) + callback.onResult(Status.SUCCESS, activity) + } + + override fun getPhoneNumberHintIntent( + callback: IGetPhoneNumberHintIntentCallback, + request: GetPhoneNumberHintIntentRequest + ) { + Log.w(TAG, "method 'getPhoneNumberHintIntent' not fully implemented, return status is CANCELED.") + callback.onResult(Status.CANCELED, null) + } +} \ No newline at end of file From 89d0050ca4ad1350cb031ffcb1b63dfef7879f53 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Sun, 1 Oct 2023 18:43:25 +0200 Subject: [PATCH 022/209] Replace hardcoded auth token with auth token fetched via account --- vending-app/build.gradle | 84 +++++++ vending-app/src/main/AndroidManifest.xml | 68 +++++ .../vending/licensing/LicenseRequest.java | 217 ++++++++++++++++ .../vending/licensing/LicensingService.java | 232 ++++++++++++++++++ 4 files changed, 601 insertions(+) create mode 100644 vending-app/build.gradle create mode 100644 vending-app/src/main/AndroidManifest.xml create mode 100644 vending-app/src/main/java/com/android/vending/licensing/LicenseRequest.java create mode 100644 vending-app/src/main/java/com/android/vending/licensing/LicensingService.java diff --git a/vending-app/build.gradle b/vending-app/build.gradle new file mode 100644 index 00000000..8031b91d --- /dev/null +++ b/vending-app/build.gradle @@ -0,0 +1,84 @@ +/* + * SPDX-FileCopyrightText: 2015 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'com.squareup.wire' + +android { + namespace "com.android.vending" + compileSdkVersion androidCompileSdk + buildToolsVersion "$androidBuildVersionTools" + + defaultConfig { + versionName vendingAppVersionName + versionCode vendingAppVersionCode + minSdkVersion androidMinSdk + targetSdkVersion androidTargetSdk + } + + flavorDimensions = ['target'] + productFlavors { + "default" { + dimension 'target' + } + "huawei" { + dimension 'target' + versionNameSuffix "-hw" + } + "huaweilh" { + dimension 'target' + versionNameSuffix "-lh" + versionCode vendingAppVersionCode - 1000 + matchingFallbacks = ['huawei'] + } + } + + sourceSets { + main { + java { + srcDirs += "build/generated/source/proto/main/java" + } + } + } + + + buildFeatures { + aidl = true + } + + lintOptions { + disable 'MissingTranslation' + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } +} + +dependencies { + implementation project(':fake-signature') + implementation project(':play-services-auth') + + implementation "com.squareup.wire:wire-runtime:$wireVersion" + implementation "com.android.volley:volley:$volleyVersion" +} + +wire { + kotlin { + javaInterop = true + } +} + +if (file('user.gradle').exists()) { + apply from: 'user.gradle' +} + +android.applicationVariants.all { variant -> + variant.outputs.each { output -> + output.outputFileName = variant.applicationId + "-" + variant.versionCode + variant.versionName.substring(vendingAppVersionName.length()) + ".apk" + } +} \ No newline at end of file diff --git a/vending-app/src/main/AndroidManifest.xml b/vending-app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..7156d709 --- /dev/null +++ b/vending-app/src/main/AndroidManifest.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vending-app/src/main/java/com/android/vending/licensing/LicenseRequest.java b/vending-app/src/main/java/com/android/vending/licensing/LicenseRequest.java new file mode 100644 index 00000000..08d6e28a --- /dev/null +++ b/vending-app/src/main/java/com/android/vending/licensing/LicenseRequest.java @@ -0,0 +1,217 @@ +package com.android.vending.licensing; + +import static com.android.volley.Request.Method.GET; + +import android.util.Base64; +import android.util.Log; + +import com.android.vending.AndroidVersionMeta; +import com.android.vending.DeviceMeta; +import com.android.vending.EncodedTriple; +import com.android.vending.EncodedTripleWrapper; +import com.android.vending.IntWrapper; +import com.android.vending.LicenseRequestHeader; +import com.android.vending.LicenseResult; +import com.android.vending.Locality; +import com.android.vending.LocalityWrapper; +import com.android.vending.StringWrapper; +import com.android.vending.Timestamp; +import com.android.vending.TimestampContainer; +import com.android.vending.TimestampContainer1; +import com.android.vending.TimestampContainer1Wrapper; +import com.android.vending.TimestampContainer2; +import com.android.vending.TimestampStringWrapper; +import com.android.vending.TimestampWrapper; +import com.android.vending.UnknownByte12; +import com.android.vending.UserAgent; +import com.android.vending.Util; +import com.android.vending.Uuid; +import com.android.vending.V1Container; +import com.android.volley.NetworkResponse; +import com.android.volley.Request; +import com.android.volley.Response; +import com.android.volley.VolleyError; + +import java.io.IOException; +import java.util.Map; +import java.util.UUID; + +import okio.ByteString; + +public abstract class LicenseRequest extends Request { + + private final String xPsRh; + private final String auth; + private static final String TAG = "FakeLicenseRequest"; + + private static final int BASE64_FLAGS = Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING; + private static final long ANDROID_ID = 1; + + private final Response.Listener successListener; + + + protected LicenseRequest(String url, String auth, Response.Listener successListener, Response.ErrorListener errorListener) { + super(GET, url, errorListener); + this.auth = auth; + + this.successListener = successListener; + + long millis = System.currentTimeMillis(); + TimestampContainer.Builder timestamp = new TimestampContainer.Builder() + .container2(new TimestampContainer2.Builder() + .wrapper(new TimestampWrapper.Builder().timestamp(makeTimestamp(millis)).build()) + .timestamp(makeTimestamp(millis)) + .build()); + millis = System.currentTimeMillis(); + timestamp + .container1Wrapper(new TimestampContainer1Wrapper.Builder() + .androidId(String.valueOf(ANDROID_ID)) + .container(new TimestampContainer1.Builder() + .timestamp(millis + "000") + .wrapper(makeTimestamp(millis)) + .build()) + .build() + ); + String encodedTimestamps = new String( + Base64.encode(Util.encodeGzip(timestamp.build().encode()), BASE64_FLAGS) + ); + + Locality locality = new Locality.Builder() + .unknown1(1) + .unknown2(2) + .countryCode("") + .region(new TimestampStringWrapper.Builder() + .string("").timestamp(makeTimestamp(System.currentTimeMillis())).build()) + .country(new TimestampStringWrapper.Builder() + .string("").timestamp(makeTimestamp(System.currentTimeMillis())).build()) + .unknown3(0) + .build(); + String encodedLocality = new String( + Base64.encode(locality.encode(), BASE64_FLAGS) + ); + + byte[] header = new LicenseRequestHeader.Builder() + .encodedTimestamps(new StringWrapper.Builder().string(encodedTimestamps).build()) + .triple( + new EncodedTripleWrapper.Builder().triple( + new EncodedTriple.Builder() + .encoded1("") + .encoded2("") + .empty("") + .build() + ).build() + ) + .locality(new LocalityWrapper.Builder().encodedLocalityProto(encodedLocality).build()) + .unknown(new IntWrapper.Builder().integer(5).build()) + .empty("") + .deviceMeta(new DeviceMeta.Builder() + .android( + new AndroidVersionMeta.Builder() + .androidSdk(0) + .buildNumber("") + .androidVersion("") + .unknown(0) + .build() + ) + .unknown1(new UnknownByte12.Builder().bytes(new ByteString(new byte[]{} + )).build()) + .unknown2(1) + .build() + ) + .userAgent(new UserAgent.Builder() + .deviceProductName("") + .deviceSoc("") + .deviceModelName("") + .finskyVersion("") + .deviceName("") + .androidId(ANDROID_ID) // must not be 0 + .deviceSignature("") + .build() + ) + .uuid(new Uuid.Builder() + .uuid(UUID.randomUUID().toString()) + .unknown(2) + .build() + ) + .build().encode(); + this.xPsRh = new String(Base64.encode(Util.encodeGzip(header), BASE64_FLAGS)); + + //Log.d(TAG, "Product " + Build.PRODUCT + ", Board " + Build.BOARD + " Model " + Build.MODEL + " Device " + Build.DEVICE); + + Log.v(TAG, "X-PS-RH: " + xPsRh); + } + + @Override + public Map getHeaders() { + return Map.of( + "X-PS-RH", xPsRh, + "Authorization", "Bearer " + auth, + "Connection", "Keep-Alive" + ); + } + + @Override + protected void deliverResponse(T response) { + successListener.onResponse(response); + } + + private static Timestamp makeTimestamp(long millis) { + return new Timestamp.Builder() + .seconds((int) (millis / 1000)) + .nanos(Math.floorMod(millis, 1000) * 1000000) + .build(); + } + + public static class V1 extends LicenseRequest { + + public V1(String packageName, String auth, int versionCode, long nonce, Response.Listener successListener, Response.ErrorListener errorListener) { + super("https://play-fe.googleapis.com/fdfe/apps/checkLicense?pkgn=" + packageName + "&vc=" + versionCode + "&nnc=" + nonce, + auth, successListener, errorListener + ); + } + + @Override + protected Response parseNetworkResponse(NetworkResponse response) { + if (response != null && response.data != null) { + try { + LicenseResult result = LicenseResult.ADAPTER.decode(response.data); + return Response.success(result.information.v1, null); + } catch (IOException e) { + return Response.error(new VolleyError(e)); + } catch (NullPointerException e) { + // A field does not exist → user has no license + return Response.success(null, null); + } + } else { + return Response.error(new VolleyError("No response was returned")); + } + } + } + + public static class V2 extends LicenseRequest { + public V2(String packageName, String auth, int versionCode, Response.Listener successListener, + Response.ErrorListener errorListener) { + super( + "https://play-fe.googleapis.com/fdfe/apps/checkLicenseServerFallback?pkgn=" + packageName + "&vc=" + versionCode, + auth, successListener, errorListener + ); + } + + @Override + protected Response parseNetworkResponse(NetworkResponse response) { + if (response != null && response.data != null) { + try { + LicenseResult result = LicenseResult.ADAPTER.decode(response.data); + return Response.success(result.information.v2.license.jwt, null); + } catch (IOException e) { + return Response.error(new VolleyError(e)); + } catch (NullPointerException e) { + // A field does not exist → user has no license + return Response.success(null, null); + } + } else { + return Response.error(new VolleyError("No response was returned")); + } + } + } +} diff --git a/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java b/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java new file mode 100644 index 00000000..267ddc84 --- /dev/null +++ b/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java @@ -0,0 +1,232 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.android.vending.licensing; + +import static android.accounts.AccountManager.KEY_AUTHTOKEN; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.accounts.AuthenticatorException; +import android.accounts.OperationCanceledException; +import android.app.Service; +import android.content.Intent; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; + +import com.android.volley.Request; +import com.android.volley.RequestQueue; +import com.android.volley.toolbox.Volley; + +import org.microg.gms.auth.AuthConstants; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +public class LicensingService extends Service { + private static final String TAG = "FakeLicenseService"; + private RequestQueue queue; + private AccountManager accountManager; + + private static final String KEY_V2_RESULT_JWT = "LICENSE_DATA"; + + private static final String AUTH_TOKEN_SCOPE = "oauth2:https://www.googleapis.com/auth/googleplay"; + + /* Possible response codes for checkLicense v1, from + * https://developer.android.com/google/play/licensing/licensing-reference#server-response-codes and + * the LVL library. + */ + + /** + * The application is licensed to the user. The user has purchased the application, or is authorized to + * download and install the alpha or beta version of the application. + */ + private static final int LICENSED = 0x0; + /** + * The application is licensed to the user, but there is an updated application version available that is + * signed with a different key. + */ + private static final int NOT_LICENSED = 0x1; + /** + * The application is not licensed to the user. + */ + private static final int LICENSED_OLD_KEY = 0x2; + /** + * Server error — the application (package name) was not recognized by Google Play. + */ + private static final int ERROR_NOT_MARKET_MANAGED = 0x3; + /** + * Server error — the server could not load the application's key pair for licensing. + */ + private static final int ERROR_SERVER_FAILURE = 0x4; + private static final int ERROR_OVER_QUOTA = 0x5; + + /** + * Local error — the Google Play application was not able to reach the licensing server, possibly because + * of network availability problems. + */ + private static final int ERROR_CONTACTING_SERVER = 0x101; + /** + * Local error — the application requested a license check for a package that is not installed on the device. + */ + private static final int ERROR_INVALID_PACKAGE_NAME = 0x102; + /** + * Local error — the application requested a license check for a package whose UID (package, user ID pair) + * does not match that of the requesting application. + */ + private static final int ERROR_NON_MATCHING_UID = 0x103; + + private final ILicensingService.Stub mLicenseService = new ILicensingService.Stub() { + + + @Override + public void checkLicense(long nonce, String packageName, ILicenseResultListener listener) throws RemoteException { + Log.v(TAG, "checkLicense(" + nonce + ", " + packageName + ")"); + try { + PackageInfo packageInfo = getPackageManager().getPackageInfo(packageName, 0); + int versionCode = packageInfo.versionCode; + + // Verify caller identity + if (packageInfo.applicationInfo.uid != getCallingUid()) { + Log.e(TAG, "an app illegally tried to request v1 licenses for another app (caller: " + getCallingUid() + ")"); + listener.verifyLicense(ERROR_NON_MATCHING_UID, null, null); + } else { + + Account[] accounts = accountManager.getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE); + + if (accounts.length == 0) { + Log.e(TAG, "not checking license, as user is not signed in"); + } else accountManager.getAuthToken( + accounts[0], AUTH_TOKEN_SCOPE, false, + future -> { + Request request = null; + try { + request = new LicenseRequest.V1( + packageName, + future.getResult().getString(KEY_AUTHTOKEN), + versionCode, nonce, data -> { + if (data != null) { + Log.v(TAG, "licenseV1 result was " + data.result + " with signed data " + + data.signedData); + + try { + if (data.result != null) { + listener.verifyLicense(data.result, data.signedData, data.signature); + } else { + listener.verifyLicense(LICENSED, data.signedData, data.signature); + } + } catch (RemoteException e) { + Log.e(TAG, + "After telling it the licenseV1 result, remote threw an Exception."); + e.printStackTrace(); + } + } else { + Log.v(TAG, "licenseV1 result was that user has no license"); + sendError(listener, NOT_LICENSED); + } + }, error -> { + Log.e(TAG, "licenseV1 request failed with " + error.toString()); + sendError(listener, ERROR_CONTACTING_SERVER); + }); + } catch (AuthenticatorException | IOException | OperationCanceledException e) { + sendError(listener, ERROR_CONTACTING_SERVER); + } + + request.setShouldCache(false); + queue.add(request); + }, null); + } + } catch (PackageManager.NameNotFoundException e) { + Log.e(TAG, "an app tried to request v1 licenses for package " + packageName + ", which does not exist"); + listener.verifyLicense(ERROR_INVALID_PACKAGE_NAME, null, null); + } + } + + @Override + public void checkLicenseV2(String packageName, ILicenseV2ResultListener listener, Bundle extraParams) throws RemoteException { + Log.v(TAG, "checkLicenseV2(" + packageName + ", " + extraParams + ")"); + + try { + PackageInfo packageInfo = getPackageManager().getPackageInfo(packageName, 0); + int versionCode = packageInfo.versionCode; + + // Verify caller identity + if (packageInfo.applicationInfo.uid != getCallingUid()) { + Log.e(TAG, "an app illegally tried to request v2 licenses for another app (caller: " + getCallingUid() + ")"); + listener.verifyLicense(ERROR_NON_MATCHING_UID, new Bundle()); + } else { + Account[] accounts = accountManager.getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE); + + if (accounts.length == 0) { + Log.e(TAG, "not checking license, as user is not signed in"); + } else accountManager.getAuthToken( + accounts[0], AUTH_TOKEN_SCOPE, false, + future -> { + try { + Bundle result = future.getResult(10, TimeUnit.SECONDS); + String auth = result.getString(KEY_AUTHTOKEN); + + Request request = new LicenseRequest.V2(packageName, auth, versionCode, jwt -> { + Log.v(TAG, "LicenseV2 returned JWT license value " + jwt); + Bundle bundle = new Bundle(); + bundle.putString(KEY_V2_RESULT_JWT, jwt); + try { + listener.verifyLicense(jwt == null ? NOT_LICENSED : LICENSED, bundle); + } catch (RemoteException e) { + Log.e(TAG, "After returning licenseV2 result, remote threw an Exception."); + e.printStackTrace(); + } + }, error -> { + Log.e(TAG, "licenseV2 request failed with " + error.toString()); + sendError(listener, ERROR_CONTACTING_SERVER); + }); + + request.setShouldCache(false); + queue.add(request); + + } catch (AuthenticatorException | IOException | OperationCanceledException e) { + sendError(listener, ERROR_CONTACTING_SERVER); + e.printStackTrace(); + } + }, null + ); + } + } catch (PackageManager.NameNotFoundException e) { + Log.e(TAG, "an app tried to request v1 licenses for package " + packageName + ", which does not exist"); + listener.verifyLicense(ERROR_INVALID_PACKAGE_NAME, new Bundle()); + } + + } + }; + + private static void sendError(ILicenseResultListener listener, int error) { + try { + listener.verifyLicense(error, null, null); + } catch (RemoteException e) { + Log.e(TAG, "After telling it that licenseV1 had an error (" + error + "), remote threw an Exception."); + } + } + + private static void sendError(ILicenseV2ResultListener listener, int error) { + try { + listener.verifyLicense(error, new Bundle()); + } catch (RemoteException e) { + Log.e(TAG, "After telling it that licenseV2 had an error (" + error + "), remote threw an Exception."); + } + } + + public IBinder onBind(Intent intent) { + queue = Volley.newRequestQueue(this); + accountManager = AccountManager.get(this); + + return mLicenseService; + } + + +} From c70a7d4c71139a9d9f5d0abe58a1351d17dd038c Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Tue, 3 Oct 2023 16:37:23 +0200 Subject: [PATCH 023/209] Notify is user not signed in to any Google account --- vending-app/src/main/AndroidManifest.xml | 7 + .../LicenseServiceNotificationRunnable.java | 157 ++++++++++++++++++ .../vending/licensing/LicensingService.java | 16 +- vending-app/src/main/res/values/strings.xml | 19 +++ 4 files changed, 197 insertions(+), 2 deletions(-) create mode 100644 vending-app/src/main/java/com/android/vending/licensing/LicenseServiceNotificationRunnable.java create mode 100644 vending-app/src/main/res/values/strings.xml diff --git a/vending-app/src/main/AndroidManifest.xml b/vending-app/src/main/AndroidManifest.xml index 7156d709..36bf475c 100644 --- a/vending-app/src/main/AndroidManifest.xml +++ b/vending-app/src/main/AndroidManifest.xml @@ -10,6 +10,7 @@ android:protectionLevel="normal" /> + + + + + diff --git a/vending-app/src/main/java/com/android/vending/licensing/LicenseServiceNotificationRunnable.java b/vending-app/src/main/java/com/android/vending/licensing/LicenseServiceNotificationRunnable.java new file mode 100644 index 00000000..f692c413 --- /dev/null +++ b/vending-app/src/main/java/com/android/vending/licensing/LicenseServiceNotificationRunnable.java @@ -0,0 +1,157 @@ +package com.android.vending.licensing; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Build; +import android.util.Log; + +import androidx.annotation.CallSuper; +import androidx.core.app.NotificationCompat; +import androidx.core.app.NotificationManagerCompat; + +import com.android.vending.R; + +import java.util.Collections; +import java.util.Set; +import java.util.TreeSet; + +public class LicenseServiceNotificationRunnable implements Runnable { + + private final Context context; + + public String callerPackageName; + public CharSequence callerAppName; + public int callerUid; + + private static final String TAG = "FakeLicenseNotification"; + private static final String GMS_PACKAGE_NAME = "com.google.android.gms"; + private static final String GMS_AUTH_INTENT_ACTION = "com.google.android.gms.auth.login.LOGIN"; + + private static final String PREFERENCES_KEY_IGNORE_PACKAGES_LIST = "ignorePackages"; + private static final String PREFERENCES_FILE_NAME = "licensing"; + + private static final String INTENT_KEY_IGNORE_PACKAGE_NAME = "package"; + private static final String INTENT_KEY_NOTIFICATION_ID = "id"; + + + public LicenseServiceNotificationRunnable(Context context) { + this.context = context; + } + + private static final String CHANNEL_ID = "LicenseNotification"; + + @Override + public void run() { + registerNotificationChannel(); + + SharedPreferences preferences = context.getSharedPreferences(PREFERENCES_FILE_NAME, Context.MODE_PRIVATE); + + Set ignoreList = preferences.getStringSet(PREFERENCES_KEY_IGNORE_PACKAGES_LIST, Collections.emptySet()); + for (String ignoredPackage : ignoreList) { + if (callerPackageName.equals(ignoredPackage)) { + Log.d(TAG, "Not notifying about license check, as user has ignored notifications for package " + ignoredPackage); + return; + } + } + + Intent authIntent = new Intent(context, LicenseServiceNotificationRunnable.SignInReceiver.class); + authIntent.putExtra(INTENT_KEY_NOTIFICATION_ID, callerUid); + PendingIntent authPendingIntent = PendingIntent.getBroadcast( + context, callerUid * 2, authIntent, PendingIntent.FLAG_IMMUTABLE + ); + + Intent ignoreIntent = new Intent(context, LicenseServiceNotificationRunnable.IgnoreReceiver.class); + ignoreIntent.putExtra(INTENT_KEY_IGNORE_PACKAGE_NAME, callerPackageName); + ignoreIntent.putExtra(INTENT_KEY_NOTIFICATION_ID, callerUid); + PendingIntent ignorePendingIntent = PendingIntent.getBroadcast( + context, callerUid * 2 + 1, ignoreIntent, PendingIntent.FLAG_IMMUTABLE + ); + + Notification notification = new NotificationCompat.Builder(context, CHANNEL_ID) + .setSmallIcon(R.drawable.ic_app_foreground) + .setContentTitle(context.getString(R.string.license_notification_title, callerAppName)) + .setContentText(context.getString(R.string.license_notification_body)) + .addAction( + new NotificationCompat.Action.Builder( + null, context.getString(R.string.license_notification_sign_in), authPendingIntent + ).build() + ) + .addAction( + new NotificationCompat.Action.Builder( + null, context.getString(R.string.license_notification_ignore), ignorePendingIntent + ).build() + ) + .setAutoCancel(true) + .build(); + + NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); + notificationManager.notify(callerUid, notification); + + } + + private void registerNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel channel = new NotificationChannel( + CHANNEL_ID, + context.getString(R.string.license_notification_channel_name), + NotificationManager.IMPORTANCE_HIGH + ); + channel.setDescription(context.getString(R.string.license_notification_channel_description)); + + NotificationManager notificationManager = context.getSystemService(NotificationManager.class); + notificationManager.createNotificationChannel(channel); + } + + } + + private static class Receiver extends BroadcastReceiver { + + @Override + @CallSuper + public void onReceive(Context context, Intent intent) { + // Dismiss notification + NotificationManagerCompat.from(context) + .cancel(intent.getIntExtra(INTENT_KEY_NOTIFICATION_ID, -1)); + } + } + + public static final class IgnoreReceiver extends Receiver { + + @Override + public void onReceive(Context context, Intent intent) { + super.onReceive(context, intent); + + SharedPreferences preferences = context.getSharedPreferences(PREFERENCES_FILE_NAME, Context.MODE_PRIVATE); + + Set ignoreList = new TreeSet<>( + preferences.getStringSet(PREFERENCES_KEY_IGNORE_PACKAGES_LIST, Collections.emptySet()) + ); + + String newIgnorePackage = intent.getStringExtra(INTENT_KEY_IGNORE_PACKAGE_NAME); + Log.d(TAG, "Adding package " + newIgnorePackage + " to ignore list"); + + ignoreList.add(newIgnorePackage); + preferences.edit().putStringSet(PREFERENCES_KEY_IGNORE_PACKAGES_LIST, ignoreList).apply(); + } + } + + public static final class SignInReceiver extends Receiver { + @Override + public void onReceive(Context context, Intent intent) { + super.onReceive(context, intent); + + Log.d(TAG, "Starting sign in activity"); + Intent authIntent = new Intent(GMS_AUTH_INTENT_ACTION); + authIntent.setPackage(GMS_PACKAGE_NAME); + authIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(authIntent); + } + } + +} diff --git a/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java b/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java index 267ddc84..f6df3b71 100644 --- a/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java +++ b/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java @@ -33,6 +33,7 @@ public class LicensingService extends Service { private static final String TAG = "FakeLicenseService"; private RequestQueue queue; private AccountManager accountManager; + private LicenseServiceNotificationRunnable notificationRunnable; private static final String KEY_V2_RESULT_JWT = "LICENSE_DATA"; @@ -89,7 +90,8 @@ public class LicensingService extends Service { public void checkLicense(long nonce, String packageName, ILicenseResultListener listener) throws RemoteException { Log.v(TAG, "checkLicense(" + nonce + ", " + packageName + ")"); try { - PackageInfo packageInfo = getPackageManager().getPackageInfo(packageName, 0); + PackageManager packageManager = getPackageManager(); + PackageInfo packageInfo = packageManager.getPackageInfo(packageName, 0); int versionCode = packageInfo.versionCode; // Verify caller identity @@ -102,6 +104,10 @@ public class LicensingService extends Service { if (accounts.length == 0) { Log.e(TAG, "not checking license, as user is not signed in"); + notificationRunnable.callerPackageName = packageName; + notificationRunnable.callerUid = packageInfo.applicationInfo.uid; + notificationRunnable.callerAppName = packageManager.getApplicationLabel(packageInfo.applicationInfo); + notificationRunnable.run(); } else accountManager.getAuthToken( accounts[0], AUTH_TOKEN_SCOPE, false, future -> { @@ -153,7 +159,8 @@ public class LicensingService extends Service { Log.v(TAG, "checkLicenseV2(" + packageName + ", " + extraParams + ")"); try { - PackageInfo packageInfo = getPackageManager().getPackageInfo(packageName, 0); + PackageManager packageManager = getPackageManager(); + PackageInfo packageInfo = packageManager.getPackageInfo(packageName, 0); int versionCode = packageInfo.versionCode; // Verify caller identity @@ -165,6 +172,10 @@ public class LicensingService extends Service { if (accounts.length == 0) { Log.e(TAG, "not checking license, as user is not signed in"); + notificationRunnable.callerPackageName = packageName; + notificationRunnable.callerUid = packageInfo.applicationInfo.uid; + notificationRunnable.callerAppName = packageManager.getApplicationLabel(packageInfo.applicationInfo); + notificationRunnable.run(); } else accountManager.getAuthToken( accounts[0], AUTH_TOKEN_SCOPE, false, future -> { @@ -224,6 +235,7 @@ public class LicensingService extends Service { public IBinder onBind(Intent intent) { queue = Volley.newRequestQueue(this); accountManager = AccountManager.get(this); + notificationRunnable = new LicenseServiceNotificationRunnable(this); return mLicenseService; } diff --git a/vending-app/src/main/res/values/strings.xml b/vending-app/src/main/res/values/strings.xml new file mode 100644 index 00000000..07c309f9 --- /dev/null +++ b/vending-app/src/main/res/values/strings.xml @@ -0,0 +1,19 @@ + + + + + microG Companion + microG Companion cannot be used standalone. Opened microG Services settings instead. + microG Companion cannot be used standalone. Please install microG Services to use microG. + + License notifications + Notifies when an app tries to validate its license, but you are not signed in to any Google account. + %1$s could not verify license + If the app is misbehaving, sign in to a Google account with which you have bought the app. + + Sign In + Ignore + \ No newline at end of file From 5ddfab5a57ec51d4b029960598401969af105f4a Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Tue, 3 Oct 2023 16:42:23 +0200 Subject: [PATCH 024/209] Don't provide most negative results in V2 licensing --- .../vending/licensing/LicensingService.java | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java b/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java index f6df3b71..1324b491 100644 --- a/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java +++ b/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java @@ -166,6 +166,10 @@ public class LicensingService extends Service { // Verify caller identity if (packageInfo.applicationInfo.uid != getCallingUid()) { Log.e(TAG, "an app illegally tried to request v2 licenses for another app (caller: " + getCallingUid() + ")"); + /* This negative result is provided even if users are not signed in; we expect apps + * will usually behave correctly in practise so this will not prevent users from + * using the app. + */ listener.verifyLicense(ERROR_NON_MATCHING_UID, new Bundle()); } else { Account[] accounts = accountManager.getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE); @@ -188,21 +192,32 @@ public class LicensingService extends Service { Bundle bundle = new Bundle(); bundle.putString(KEY_V2_RESULT_JWT, jwt); try { - listener.verifyLicense(jwt == null ? NOT_LICENSED : LICENSED, bundle); + if (jwt == null) { + /* + * Suppress failures on V2. V2 is commonly used by free apps whose checker + * will not throw users out of the app if it never receives a response. + * + * This means that users who are signed in to a Google account will not + * get a worse experience in these apps than users that are not signed in. + */ + Log.i(TAG, "Suppressed negative result for package " + packageName); + } else { + listener.verifyLicense(jwt == null ? NOT_LICENSED : LICENSED, bundle); + } } catch (RemoteException e) { Log.e(TAG, "After returning licenseV2 result, remote threw an Exception."); e.printStackTrace(); } }, error -> { Log.e(TAG, "licenseV2 request failed with " + error.toString()); - sendError(listener, ERROR_CONTACTING_SERVER); + //sendError(listener, ERROR_CONTACTING_SERVER); – see above }); request.setShouldCache(false); queue.add(request); } catch (AuthenticatorException | IOException | OperationCanceledException e) { - sendError(listener, ERROR_CONTACTING_SERVER); + //sendError(listener, ERROR_CONTACTING_SERVER); – see above e.printStackTrace(); } }, null From 4f80b939ba96b8f5ebd641eb198bfdc7181a6b65 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Tue, 3 Oct 2023 16:56:30 +0200 Subject: [PATCH 025/209] Improve license notification icon & sound --- .../LicenseServiceNotificationRunnable.java | 4 +- .../src/main/res/drawable/ic_notification.xml | 42 +++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 vending-app/src/main/res/drawable/ic_notification.xml diff --git a/vending-app/src/main/java/com/android/vending/licensing/LicenseServiceNotificationRunnable.java b/vending-app/src/main/java/com/android/vending/licensing/LicenseServiceNotificationRunnable.java index f692c413..193c755d 100644 --- a/vending-app/src/main/java/com/android/vending/licensing/LicenseServiceNotificationRunnable.java +++ b/vending-app/src/main/java/com/android/vending/licensing/LicenseServiceNotificationRunnable.java @@ -74,7 +74,8 @@ public class LicenseServiceNotificationRunnable implements Runnable { ); Notification notification = new NotificationCompat.Builder(context, CHANNEL_ID) - .setSmallIcon(R.drawable.ic_app_foreground) + .setSmallIcon(R.drawable.ic_notification) + .setSound(null) .setContentTitle(context.getString(R.string.license_notification_title, callerAppName)) .setContentText(context.getString(R.string.license_notification_body)) .addAction( @@ -103,6 +104,7 @@ public class LicenseServiceNotificationRunnable implements Runnable { NotificationManager.IMPORTANCE_HIGH ); channel.setDescription(context.getString(R.string.license_notification_channel_description)); + channel.setSound(null, null); NotificationManager notificationManager = context.getSystemService(NotificationManager.class); notificationManager.createNotificationChannel(channel); diff --git a/vending-app/src/main/res/drawable/ic_notification.xml b/vending-app/src/main/res/drawable/ic_notification.xml new file mode 100644 index 00000000..a1fdf972 --- /dev/null +++ b/vending-app/src/main/res/drawable/ic_notification.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + From ec6da6c6bee1fe96ae4f6a9bf8c7f9330e552962 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Wed, 4 Oct 2023 13:51:02 +0200 Subject: [PATCH 026/209] Query licenses from multiple Google accounts in a row --- .../vending/licensing/LicenseChecker.java | 202 +++++++++++++ .../vending/licensing/LicensingService.java | 267 +++++------------- 2 files changed, 275 insertions(+), 194 deletions(-) create mode 100644 vending-app/src/main/java/com/android/vending/licensing/LicenseChecker.java diff --git a/vending-app/src/main/java/com/android/vending/licensing/LicenseChecker.java b/vending-app/src/main/java/com/android/vending/licensing/LicenseChecker.java new file mode 100644 index 00000000..bb8ed027 --- /dev/null +++ b/vending-app/src/main/java/com/android/vending/licensing/LicenseChecker.java @@ -0,0 +1,202 @@ +package com.android.vending.licensing; + +import static android.accounts.AccountManager.KEY_AUTHTOKEN; +import static android.os.Binder.getCallingUid; + + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.accounts.AuthenticatorException; +import android.accounts.OperationCanceledException; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.os.RemoteException; +import android.util.Log; + +import com.android.vending.V1Container; +import com.android.volley.Request; +import com.android.volley.RequestQueue; +import com.android.volley.Response; + +import java.io.IOException; + +import kotlin.Unit; + +/** + * Performs license check including caller UID verification, using a given account, for which + * an auth token is fetched. + * + * @param Request parameter data value type + * @param Result type + */ +public abstract class LicenseChecker { + + private static final String TAG = "FakeLicenseChecker"; + + /* Possible response codes for checkLicense v1, from + * https://developer.android.com/google/play/licensing/licensing-reference#server-response-codes and + * the LVL library. + */ + + /** + * The application is licensed to the user. The user has purchased the application, or is authorized to + * download and install the alpha or beta version of the application. + */ + static final int LICENSED = 0x0; + /** + * The application is licensed to the user, but there is an updated application version available that is + * signed with a different key. + */ + static final int NOT_LICENSED = 0x1; + /** + * The application is not licensed to the user. + */ + static final int LICENSED_OLD_KEY = 0x2; + /** + * Server error — the application (package name) was not recognized by Google Play. + */ + static final int ERROR_NOT_MARKET_MANAGED = 0x3; + /** + * Server error — the server could not load the application's key pair for licensing. + */ + static final int ERROR_SERVER_FAILURE = 0x4; + static final int ERROR_OVER_QUOTA = 0x5; + + /** + * Local error — the Google Play application was not able to reach the licensing server, possibly because + * of network availability problems. + */ + static final int ERROR_CONTACTING_SERVER = 0x101; + /** + * Local error — the application requested a license check for a package that is not installed on the device. + */ + static final int ERROR_INVALID_PACKAGE_NAME = 0x102; + /** + * Local error — the application requested a license check for a package whose UID (package, user ID pair) + * does not match that of the requesting application. + */ + static final int ERROR_NON_MATCHING_UID = 0x103; + + static final String AUTH_TOKEN_SCOPE = "oauth2:https://www.googleapis.com/auth/googleplay"; + + public void checkLicense(Account account, AccountManager accountManager, + String packageName, PackageManager packageManager, + RequestQueue queue, D queryData, + BiConsumerWithException onResult) + throws RemoteException { + try { + PackageInfo packageInfo = packageManager.getPackageInfo(packageName, 0); + int versionCode = packageInfo.versionCode; + + // Verify caller identity + if (packageInfo.applicationInfo.uid != getCallingUid()) { + Log.e(TAG, + "an app illegally tried to request licenses for another app (caller: " + getCallingUid() + ")"); + onResult.accept(ERROR_NON_MATCHING_UID, null); + } else { + + accountManager.getAuthToken( + account, AUTH_TOKEN_SCOPE, false, + future -> { + try { + String auth = future.getResult().getString(KEY_AUTHTOKEN); + Request request = createRequest(packageName, auth, + versionCode, queryData, (Integer integer, R r) -> { + + try { + onResult.accept(integer, r); + } catch (RemoteException e) { + Log.e(TAG, + "After telling it the license check result, remote threw an Exception."); + e.printStackTrace(); + } + }, error -> { + Log.e(TAG, "license request failed with " + error.toString()); + safeSendResult(onResult, ERROR_CONTACTING_SERVER, null); + }); + request.setShouldCache(false); + queue.add(request); + } catch (AuthenticatorException | IOException | OperationCanceledException e) { + safeSendResult(onResult, ERROR_CONTACTING_SERVER, null); + } + + }, null); + } + } catch (PackageManager.NameNotFoundException e) { + Log.e(TAG, "an app tried to request licenses for package " + packageName + ", which does not exist"); + onResult.accept(ERROR_INVALID_PACKAGE_NAME, null); + } + } + + private static void safeSendResult( + BiConsumerWithException consumerWithException, A a, B b) { + try { + consumerWithException.accept(a, b); + } catch (Exception e) { + Log.e(TAG, "While sending result " + a + ", " + b + ", remote encountered an exception."); + e.printStackTrace(); + } + } + + public abstract Request createRequest(String packageName, String auth, int versionCode, D data, BiConsumer then, Response.ErrorListener errorListener); + + // Functional interfaces + + interface BiConsumerWithException { + void accept(A a, B b) throws T; + } + + interface BiConsumer { + void accept(A a, B b); + } + + static class Tuple { + public final A a; + public final B b; + + public Tuple(A a, B b) { + this.a = a; + this.b = b; + } + } + + // Implementations + + public static class V1 extends LicenseChecker> { + + @Override + public Request createRequest(String packageName, String auth, int versionCode, Long nonce, BiConsumer> then, + Response.ErrorListener errorListener) { + return new LicenseRequest.V1( + packageName, auth, versionCode, nonce, response -> { + if (response != null) { + Log.v(TAG, "licenseV1 result was " + response.result + " with signed data " + + response.signedData); + + if (response.result != null) { + then.accept(response.result, new Tuple<>(response.signedData, response.signature)); + } else { + then.accept(LICENSED, new Tuple<>(response.signedData, response.signature)); + } + } + }, errorListener + ); + } + } + + public static class V2 extends LicenseChecker { + @Override + public Request createRequest(String packageName, String auth, int versionCode, Unit data, + BiConsumer then, Response.ErrorListener errorListener) { + return new LicenseRequest.V2( + packageName, auth, versionCode, response -> { + if (response != null) { + then.accept(LICENSED, response); + } else { + then.accept(NOT_LICENSED, null); + } + }, errorListener + ); + } + } +} diff --git a/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java b/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java index 1324b491..8b58ca22 100644 --- a/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java +++ b/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java @@ -5,12 +5,10 @@ package com.android.vending.licensing; -import static android.accounts.AccountManager.KEY_AUTHTOKEN; +import static com.android.vending.licensing.LicenseChecker.LICENSED; import android.accounts.Account; import android.accounts.AccountManager; -import android.accounts.AuthenticatorException; -import android.accounts.OperationCanceledException; import android.app.Service; import android.content.Intent; import android.content.pm.PackageInfo; @@ -20,14 +18,16 @@ import android.os.IBinder; import android.os.RemoteException; import android.util.Log; -import com.android.volley.Request; import com.android.volley.RequestQueue; import com.android.volley.toolbox.Volley; import org.microg.gms.auth.AuthConstants; -import java.io.IOException; -import java.util.concurrent.TimeUnit; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.Queue; + +import kotlin.Unit; public class LicensingService extends Service { private static final String TAG = "FakeLicenseService"; @@ -37,51 +37,6 @@ public class LicensingService extends Service { private static final String KEY_V2_RESULT_JWT = "LICENSE_DATA"; - private static final String AUTH_TOKEN_SCOPE = "oauth2:https://www.googleapis.com/auth/googleplay"; - - /* Possible response codes for checkLicense v1, from - * https://developer.android.com/google/play/licensing/licensing-reference#server-response-codes and - * the LVL library. - */ - - /** - * The application is licensed to the user. The user has purchased the application, or is authorized to - * download and install the alpha or beta version of the application. - */ - private static final int LICENSED = 0x0; - /** - * The application is licensed to the user, but there is an updated application version available that is - * signed with a different key. - */ - private static final int NOT_LICENSED = 0x1; - /** - * The application is not licensed to the user. - */ - private static final int LICENSED_OLD_KEY = 0x2; - /** - * Server error — the application (package name) was not recognized by Google Play. - */ - private static final int ERROR_NOT_MARKET_MANAGED = 0x3; - /** - * Server error — the server could not load the application's key pair for licensing. - */ - private static final int ERROR_SERVER_FAILURE = 0x4; - private static final int ERROR_OVER_QUOTA = 0x5; - - /** - * Local error — the Google Play application was not able to reach the licensing server, possibly because - * of network availability problems. - */ - private static final int ERROR_CONTACTING_SERVER = 0x101; - /** - * Local error — the application requested a license check for a package that is not installed on the device. - */ - private static final int ERROR_INVALID_PACKAGE_NAME = 0x102; - /** - * Local error — the application requested a license check for a package whose UID (package, user ID pair) - * does not match that of the requesting application. - */ - private static final int ERROR_NON_MATCHING_UID = 0x103; private final ILicensingService.Stub mLicenseService = new ILicensingService.Stub() { @@ -89,164 +44,88 @@ public class LicensingService extends Service { @Override public void checkLicense(long nonce, String packageName, ILicenseResultListener listener) throws RemoteException { Log.v(TAG, "checkLicense(" + nonce + ", " + packageName + ")"); - try { - PackageManager packageManager = getPackageManager(); - PackageInfo packageInfo = packageManager.getPackageInfo(packageName, 0); - int versionCode = packageInfo.versionCode; - // Verify caller identity - if (packageInfo.applicationInfo.uid != getCallingUid()) { - Log.e(TAG, "an app illegally tried to request v1 licenses for another app (caller: " + getCallingUid() + ")"); - listener.verifyLicense(ERROR_NON_MATCHING_UID, null, null); - } else { + Account[] accounts = accountManager.getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE); + PackageManager packageManager = getPackageManager(); - Account[] accounts = accountManager.getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE); - - if (accounts.length == 0) { - Log.e(TAG, "not checking license, as user is not signed in"); - notificationRunnable.callerPackageName = packageName; - notificationRunnable.callerUid = packageInfo.applicationInfo.uid; - notificationRunnable.callerAppName = packageManager.getApplicationLabel(packageInfo.applicationInfo); - notificationRunnable.run(); - } else accountManager.getAuthToken( - accounts[0], AUTH_TOKEN_SCOPE, false, - future -> { - Request request = null; - try { - request = new LicenseRequest.V1( - packageName, - future.getResult().getString(KEY_AUTHTOKEN), - versionCode, nonce, data -> { - if (data != null) { - Log.v(TAG, "licenseV1 result was " + data.result + " with signed data " + - data.signedData); - - try { - if (data.result != null) { - listener.verifyLicense(data.result, data.signedData, data.signature); - } else { - listener.verifyLicense(LICENSED, data.signedData, data.signature); - } - } catch (RemoteException e) { - Log.e(TAG, - "After telling it the licenseV1 result, remote threw an Exception."); - e.printStackTrace(); - } - } else { - Log.v(TAG, "licenseV1 result was that user has no license"); - sendError(listener, NOT_LICENSED); - } - }, error -> { - Log.e(TAG, "licenseV1 request failed with " + error.toString()); - sendError(listener, ERROR_CONTACTING_SERVER); - }); - } catch (AuthenticatorException | IOException | OperationCanceledException e) { - sendError(listener, ERROR_CONTACTING_SERVER); - } - - request.setShouldCache(false); - queue.add(request); - }, null); - } - } catch (PackageManager.NameNotFoundException e) { - Log.e(TAG, "an app tried to request v1 licenses for package " + packageName + ", which does not exist"); - listener.verifyLicense(ERROR_INVALID_PACKAGE_NAME, null, null); + if (accounts.length == 0) { + handleNoAccounts(packageName, packageManager); + } else { + checkLicense(nonce, packageName, packageManager, listener, new LinkedList<>(Arrays.asList(accounts))); } } + private void checkLicense(long nonce, String packageName, PackageManager packageManager, + ILicenseResultListener listener, Queue remainingAccounts) throws RemoteException { + new LicenseChecker.V1().checkLicense( + remainingAccounts.poll(), accountManager, packageName, packageManager, + queue, nonce, + (responseCode, stringTuple) -> { + if (responseCode != LICENSED && !remainingAccounts.isEmpty()) { + checkLicense(nonce, packageName, packageManager, listener, remainingAccounts); + } else { + listener.verifyLicense(responseCode, stringTuple.a, stringTuple.b); + } + } + ); + } + @Override public void checkLicenseV2(String packageName, ILicenseV2ResultListener listener, Bundle extraParams) throws RemoteException { Log.v(TAG, "checkLicenseV2(" + packageName + ", " + extraParams + ")"); - try { - PackageManager packageManager = getPackageManager(); - PackageInfo packageInfo = packageManager.getPackageInfo(packageName, 0); - int versionCode = packageInfo.versionCode; + Account[] accounts = accountManager.getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE); + PackageManager packageManager = getPackageManager(); - // Verify caller identity - if (packageInfo.applicationInfo.uid != getCallingUid()) { - Log.e(TAG, "an app illegally tried to request v2 licenses for another app (caller: " + getCallingUid() + ")"); - /* This negative result is provided even if users are not signed in; we expect apps - * will usually behave correctly in practise so this will not prevent users from - * using the app. - */ - listener.verifyLicense(ERROR_NON_MATCHING_UID, new Bundle()); - } else { - Account[] accounts = accountManager.getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE); - - if (accounts.length == 0) { - Log.e(TAG, "not checking license, as user is not signed in"); - notificationRunnable.callerPackageName = packageName; - notificationRunnable.callerUid = packageInfo.applicationInfo.uid; - notificationRunnable.callerAppName = packageManager.getApplicationLabel(packageInfo.applicationInfo); - notificationRunnable.run(); - } else accountManager.getAuthToken( - accounts[0], AUTH_TOKEN_SCOPE, false, - future -> { - try { - Bundle result = future.getResult(10, TimeUnit.SECONDS); - String auth = result.getString(KEY_AUTHTOKEN); - - Request request = new LicenseRequest.V2(packageName, auth, versionCode, jwt -> { - Log.v(TAG, "LicenseV2 returned JWT license value " + jwt); - Bundle bundle = new Bundle(); - bundle.putString(KEY_V2_RESULT_JWT, jwt); - try { - if (jwt == null) { - /* - * Suppress failures on V2. V2 is commonly used by free apps whose checker - * will not throw users out of the app if it never receives a response. - * - * This means that users who are signed in to a Google account will not - * get a worse experience in these apps than users that are not signed in. - */ - Log.i(TAG, "Suppressed negative result for package " + packageName); - } else { - listener.verifyLicense(jwt == null ? NOT_LICENSED : LICENSED, bundle); - } - } catch (RemoteException e) { - Log.e(TAG, "After returning licenseV2 result, remote threw an Exception."); - e.printStackTrace(); - } - }, error -> { - Log.e(TAG, "licenseV2 request failed with " + error.toString()); - //sendError(listener, ERROR_CONTACTING_SERVER); – see above - }); - - request.setShouldCache(false); - queue.add(request); - - } catch (AuthenticatorException | IOException | OperationCanceledException e) { - //sendError(listener, ERROR_CONTACTING_SERVER); – see above - e.printStackTrace(); - } - }, null - ); - } - } catch (PackageManager.NameNotFoundException e) { - Log.e(TAG, "an app tried to request v1 licenses for package " + packageName + ", which does not exist"); - listener.verifyLicense(ERROR_INVALID_PACKAGE_NAME, new Bundle()); + if (accounts.length == 0) { + handleNoAccounts(packageName, packageManager); + } else { + checkLicenseV2(packageName, packageManager, listener, extraParams, new LinkedList<>(Arrays.asList(accounts))); } + } + private void checkLicenseV2(String packageName, PackageManager packageManager, + ILicenseV2ResultListener listener, Bundle extraParams, + Queue remainingAccounts) throws RemoteException { + new LicenseChecker.V2().checkLicense( + remainingAccounts.poll(), accountManager, packageName, packageManager, queue, Unit.INSTANCE, + (responseCode, data) -> { + /* + * Suppress failures on V2. V2 is commonly used by free apps whose checker + * will not throw users out of the app if it never receives a response. + * + * This means that users who are signed in to a Google account will not + * get a worse experience in these apps than users that are not signed in. + */ + if (responseCode == LICENSED) { + Bundle bundle = new Bundle(); + bundle.putString(KEY_V2_RESULT_JWT, data); + + listener.verifyLicense(responseCode, bundle); + } else if (!remainingAccounts.isEmpty()) { + checkLicenseV2(packageName, packageManager, listener, extraParams, remainingAccounts); + } else { + Log.i(TAG, "Suppressed negative license result for package " + packageName); + } + } + ); + + } + + private void handleNoAccounts(String packageName, PackageManager packageManager) { + try { + Log.e(TAG, "not checking license, as user is not signed in"); + PackageInfo packageInfo = packageManager.getPackageInfo(packageName, 0); + notificationRunnable.callerUid = packageInfo.applicationInfo.uid; + notificationRunnable.callerAppName = packageManager.getApplicationLabel(packageInfo.applicationInfo); + } catch (PackageManager.NameNotFoundException e) { + Log.e(TAG, "ignored license request, but package name " + packageName + " was not known!"); + notificationRunnable.callerAppName = packageName; + } + notificationRunnable.run(); } }; - private static void sendError(ILicenseResultListener listener, int error) { - try { - listener.verifyLicense(error, null, null); - } catch (RemoteException e) { - Log.e(TAG, "After telling it that licenseV1 had an error (" + error + "), remote threw an Exception."); - } - } - - private static void sendError(ILicenseV2ResultListener listener, int error) { - try { - listener.verifyLicense(error, new Bundle()); - } catch (RemoteException e) { - Log.e(TAG, "After telling it that licenseV2 had an error (" + error + "), remote threw an Exception."); - } - } - public IBinder onBind(Intent intent) { queue = Volley.newRequestQueue(this); accountManager = AccountManager.get(this); From 4708750721b63c74b8da9d0a21e04761cd772882 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Wed, 4 Oct 2023 13:58:05 +0200 Subject: [PATCH 027/209] Dismiss all notifications if user wants to sign in --- .../LicenseServiceNotificationRunnable.java | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/vending-app/src/main/java/com/android/vending/licensing/LicenseServiceNotificationRunnable.java b/vending-app/src/main/java/com/android/vending/licensing/LicenseServiceNotificationRunnable.java index 193c755d..7a82341b 100644 --- a/vending-app/src/main/java/com/android/vending/licensing/LicenseServiceNotificationRunnable.java +++ b/vending-app/src/main/java/com/android/vending/licensing/LicenseServiceNotificationRunnable.java @@ -112,22 +112,14 @@ public class LicenseServiceNotificationRunnable implements Runnable { } - private static class Receiver extends BroadcastReceiver { + public static final class IgnoreReceiver extends BroadcastReceiver { @Override - @CallSuper public void onReceive(Context context, Intent intent) { - // Dismiss notification + + // Dismiss ignored notification NotificationManagerCompat.from(context) .cancel(intent.getIntExtra(INTENT_KEY_NOTIFICATION_ID, -1)); - } - } - - public static final class IgnoreReceiver extends Receiver { - - @Override - public void onReceive(Context context, Intent intent) { - super.onReceive(context, intent); SharedPreferences preferences = context.getSharedPreferences(PREFERENCES_FILE_NAME, Context.MODE_PRIVATE); @@ -143,10 +135,12 @@ public class LicenseServiceNotificationRunnable implements Runnable { } } - public static final class SignInReceiver extends Receiver { + public static final class SignInReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { - super.onReceive(context, intent); + + // Dismiss all notifications + NotificationManagerCompat.from(context).cancelAll(); Log.d(TAG, "Starting sign in activity"); Intent authIntent = new Intent(GMS_AUTH_INTENT_ACTION); From 9fe7f21ccc998b4046dde9aa0db0def859332d88 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Wed, 4 Oct 2023 15:16:08 +0200 Subject: [PATCH 028/209] Always request `android.permission.GET_ACCOUNTS` --- vending-app/src/main/AndroidManifest.xml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/vending-app/src/main/AndroidManifest.xml b/vending-app/src/main/AndroidManifest.xml index 36bf475c..2e4fec3e 100644 --- a/vending-app/src/main/AndroidManifest.xml +++ b/vending-app/src/main/AndroidManifest.xml @@ -11,10 +11,8 @@ + - From 556f2ba1293dfdd9d0acd04e945c8d0060eddbec Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Wed, 4 Oct 2023 15:38:45 +0200 Subject: [PATCH 029/209] Target JVM 1.8 instead of 17 in fakestore --- vending-app/build.gradle | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/vending-app/build.gradle b/vending-app/build.gradle index 8031b91d..06c974c7 100644 --- a/vending-app/build.gradle +++ b/vending-app/build.gradle @@ -54,8 +54,12 @@ android { } compileOptions { - sourceCompatibility JavaVersion.VERSION_17 - targetCompatibility JavaVersion.VERSION_17 + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = 1.8 } } From 14450c345d66672aad5138009969cf3395b1dc57 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Thu, 12 Oct 2023 13:34:17 +0200 Subject: [PATCH 030/209] Require permission to check license --- vending-app/src/main/AndroidManifest.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/vending-app/src/main/AndroidManifest.xml b/vending-app/src/main/AndroidManifest.xml index 2e4fec3e..b0ec60f5 100644 --- a/vending-app/src/main/AndroidManifest.xml +++ b/vending-app/src/main/AndroidManifest.xml @@ -38,6 +38,7 @@ From c924517cb787fb63e3f5b1d17a13d725e07d20fd Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Thu, 26 Oct 2023 15:22:46 +0200 Subject: [PATCH 031/209] Apply review Reviewed-at: https://gitlab.e.foundation/e/os/GmsCore/-/merge_requests/91#note_458642 --- .../vending/licensing/LicenseChecker.java | 112 +++++++++--------- 1 file changed, 58 insertions(+), 54 deletions(-) diff --git a/vending-app/src/main/java/com/android/vending/licensing/LicenseChecker.java b/vending-app/src/main/java/com/android/vending/licensing/LicenseChecker.java index bb8ed027..ff9992f7 100644 --- a/vending-app/src/main/java/com/android/vending/licensing/LicenseChecker.java +++ b/vending-app/src/main/java/com/android/vending/licensing/LicenseChecker.java @@ -79,6 +79,9 @@ public abstract class LicenseChecker { static final String AUTH_TOKEN_SCOPE = "oauth2:https://www.googleapis.com/auth/googleplay"; + public abstract Request createRequest(String packageName, String auth, int versionCode, D data, + BiConsumer then, Response.ErrorListener errorListener); + public void checkLicense(Account account, AccountManager accountManager, String packageName, PackageManager packageManager, RequestQueue queue, D queryData, @@ -95,25 +98,28 @@ public abstract class LicenseChecker { onResult.accept(ERROR_NON_MATCHING_UID, null); } else { + BiConsumer onRequestFinished = (Integer integer, R r) -> { + try { + onResult.accept(integer, r); + } catch (RemoteException e) { + Log.e(TAG, + "After telling it the license check result, remote threw an Exception."); + e.printStackTrace(); + } + }; + + Response.ErrorListener onRequestError = error -> { + Log.e(TAG, "license request failed with " + error.toString()); + safeSendResult(onResult, ERROR_CONTACTING_SERVER, null); + }; + accountManager.getAuthToken( account, AUTH_TOKEN_SCOPE, false, future -> { try { String auth = future.getResult().getString(KEY_AUTHTOKEN); Request request = createRequest(packageName, auth, - versionCode, queryData, (Integer integer, R r) -> { - - try { - onResult.accept(integer, r); - } catch (RemoteException e) { - Log.e(TAG, - "After telling it the license check result, remote threw an Exception."); - e.printStackTrace(); - } - }, error -> { - Log.e(TAG, "license request failed with " + error.toString()); - safeSendResult(onResult, ERROR_CONTACTING_SERVER, null); - }); + versionCode, queryData, onRequestFinished, onRequestError); request.setShouldCache(false); queue.add(request); } catch (AuthenticatorException | IOException | OperationCanceledException e) { @@ -138,7 +144,45 @@ public abstract class LicenseChecker { } } - public abstract Request createRequest(String packageName, String auth, int versionCode, D data, BiConsumer then, Response.ErrorListener errorListener); + // Implementations + + public static class V1 extends LicenseChecker> { + + @Override + public Request createRequest(String packageName, String auth, int versionCode, Long nonce, BiConsumer> then, + Response.ErrorListener errorListener) { + return new LicenseRequest.V1( + packageName, auth, versionCode, nonce, response -> { + if (response != null) { + Log.v(TAG, "licenseV1 result was " + response.result + " with signed data " + + response.signedData); + + if (response.result != null) { + then.accept(response.result, new Tuple<>(response.signedData, response.signature)); + } else { + then.accept(LICENSED, new Tuple<>(response.signedData, response.signature)); + } + } + }, errorListener + ); + } + } + + public static class V2 extends LicenseChecker { + @Override + public Request createRequest(String packageName, String auth, int versionCode, Unit data, + BiConsumer then, Response.ErrorListener errorListener) { + return new LicenseRequest.V2( + packageName, auth, versionCode, response -> { + if (response != null) { + then.accept(LICENSED, response); + } else { + then.accept(NOT_LICENSED, null); + } + }, errorListener + ); + } + } // Functional interfaces @@ -159,44 +203,4 @@ public abstract class LicenseChecker { this.b = b; } } - - // Implementations - - public static class V1 extends LicenseChecker> { - - @Override - public Request createRequest(String packageName, String auth, int versionCode, Long nonce, BiConsumer> then, - Response.ErrorListener errorListener) { - return new LicenseRequest.V1( - packageName, auth, versionCode, nonce, response -> { - if (response != null) { - Log.v(TAG, "licenseV1 result was " + response.result + " with signed data " + - response.signedData); - - if (response.result != null) { - then.accept(response.result, new Tuple<>(response.signedData, response.signature)); - } else { - then.accept(LICENSED, new Tuple<>(response.signedData, response.signature)); - } - } - }, errorListener - ); - } - } - - public static class V2 extends LicenseChecker { - @Override - public Request createRequest(String packageName, String auth, int versionCode, Unit data, - BiConsumer then, Response.ErrorListener errorListener) { - return new LicenseRequest.V2( - packageName, auth, versionCode, response -> { - if (response != null) { - then.accept(LICENSED, response); - } else { - then.accept(NOT_LICENSED, null); - } - }, errorListener - ); - } - } } From 3c8a40a634794454eb688bb72f71d306f8edbed6 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Tue, 24 Oct 2023 07:43:47 +0200 Subject: [PATCH 032/209] Check preference from within Licensing service --- vending-app/src/main/AndroidManifest.xml | 2 + .../vending/licensing/LicensingService.java | 40 +++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/vending-app/src/main/AndroidManifest.xml b/vending-app/src/main/AndroidManifest.xml index b0ec60f5..bd61b73b 100644 --- a/vending-app/src/main/AndroidManifest.xml +++ b/vending-app/src/main/AndroidManifest.xml @@ -13,6 +13,8 @@ + + diff --git a/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java b/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java index 8b58ca22..a193199b 100644 --- a/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java +++ b/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java @@ -13,9 +13,11 @@ import android.app.Service; import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; +import android.database.Cursor; import android.os.Bundle; import android.os.IBinder; import android.os.RemoteException; +import android.net.Uri; import android.util.Log; import com.android.volley.RequestQueue; @@ -37,14 +39,47 @@ public class LicensingService extends Service { private static final String KEY_V2_RESULT_JWT = "LICENSE_DATA"; + private static final Uri SETTINGS_PROVIDER = Uri.parse("content://com.google.android.gms.microg.settings/play"); + + private static final String PREFERENCE_LICENSING_ENABLED = "play_licensing"; + + private String androidId; private final ILicensingService.Stub mLicenseService = new ILicensingService.Stub() { + private boolean shouldCheckLicense() { + + Cursor cursor = null; + try { + cursor = getContentResolver().query( + SETTINGS_PROVIDER, new String[]{PREFERENCE_LICENSING_ENABLED}, null, null, null + ); + + if (cursor == null || cursor.getColumnCount() != 1) { + Log.e(TAG, "settings provider not available"); + return false; + } else { + cursor.moveToNext(); + return cursor.getInt(0) != 0; + } + + } finally { + if (cursor != null) { + cursor.close(); + } + } + } + @Override public void checkLicense(long nonce, String packageName, ILicenseResultListener listener) throws RemoteException { Log.v(TAG, "checkLicense(" + nonce + ", " + packageName + ")"); + if (!shouldCheckLicense()) { + Log.d(TAG, "not checking license, as it is disabled by user"); + return; + } + Account[] accounts = accountManager.getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE); PackageManager packageManager = getPackageManager(); @@ -74,6 +109,11 @@ public class LicensingService extends Service { public void checkLicenseV2(String packageName, ILicenseV2ResultListener listener, Bundle extraParams) throws RemoteException { Log.v(TAG, "checkLicenseV2(" + packageName + ", " + extraParams + ")"); + if (!shouldCheckLicense()) { + Log.d(TAG, "not checking license, as it is disabled by user"); + return; + } + Account[] accounts = accountManager.getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE); PackageManager packageManager = getPackageManager(); From 7b3fa963f2b5232bcb02abe612a3c64b40333aed Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Sat, 11 Nov 2023 16:49:04 +0100 Subject: [PATCH 033/209] Load device profile data for licensing user agent --- vending-app/build.gradle | 1 + .../vending/licensing/LicenseChecker.java | 11 +-- .../vending/licensing/LicenseRequest.java | 51 ++++++++----- .../vending/licensing/LicensingService.java | 41 ++++++++++- .../src/main/proto/LicenseRequest.proto | 71 +++++++++++++++++++ 5 files changed, 151 insertions(+), 24 deletions(-) create mode 100644 vending-app/src/main/proto/LicenseRequest.proto diff --git a/vending-app/build.gradle b/vending-app/build.gradle index 06c974c7..5c15378b 100644 --- a/vending-app/build.gradle +++ b/vending-app/build.gradle @@ -66,6 +66,7 @@ android { dependencies { implementation project(':fake-signature') implementation project(':play-services-auth') + implementation project(':play-services-base-core') implementation "com.squareup.wire:wire-runtime:$wireVersion" implementation "com.android.volley:volley:$volleyVersion" diff --git a/vending-app/src/main/java/com/android/vending/licensing/LicenseChecker.java b/vending-app/src/main/java/com/android/vending/licensing/LicenseChecker.java index ff9992f7..6e4dcbb9 100644 --- a/vending-app/src/main/java/com/android/vending/licensing/LicenseChecker.java +++ b/vending-app/src/main/java/com/android/vending/licensing/LicenseChecker.java @@ -79,10 +79,10 @@ public abstract class LicenseChecker { static final String AUTH_TOKEN_SCOPE = "oauth2:https://www.googleapis.com/auth/googleplay"; - public abstract Request createRequest(String packageName, String auth, int versionCode, D data, + public abstract LicenseRequest createRequest(String packageName, String auth, int versionCode, D data, BiConsumer then, Response.ErrorListener errorListener); - public void checkLicense(Account account, AccountManager accountManager, + public void checkLicense(Account account, AccountManager accountManager, String androidId, String packageName, PackageManager packageManager, RequestQueue queue, D queryData, BiConsumerWithException onResult) @@ -118,8 +118,9 @@ public abstract class LicenseChecker { future -> { try { String auth = future.getResult().getString(KEY_AUTHTOKEN); - Request request = createRequest(packageName, auth, + LicenseRequest request = createRequest(packageName, auth, versionCode, queryData, onRequestFinished, onRequestError); + request.ANDROID_ID = Long.decode("0x" + androidId); request.setShouldCache(false); queue.add(request); } catch (AuthenticatorException | IOException | OperationCanceledException e) { @@ -149,7 +150,7 @@ public abstract class LicenseChecker { public static class V1 extends LicenseChecker> { @Override - public Request createRequest(String packageName, String auth, int versionCode, Long nonce, BiConsumer> then, + public LicenseRequest createRequest(String packageName, String auth, int versionCode, Long nonce, BiConsumer> then, Response.ErrorListener errorListener) { return new LicenseRequest.V1( packageName, auth, versionCode, nonce, response -> { @@ -170,7 +171,7 @@ public abstract class LicenseChecker { public static class V2 extends LicenseChecker { @Override - public Request createRequest(String packageName, String auth, int versionCode, Unit data, + public LicenseRequest createRequest(String packageName, String auth, int versionCode, Unit data, BiConsumer then, Response.ErrorListener errorListener) { return new LicenseRequest.V2( packageName, auth, versionCode, response -> { diff --git a/vending-app/src/main/java/com/android/vending/licensing/LicenseRequest.java b/vending-app/src/main/java/com/android/vending/licensing/LicenseRequest.java index 08d6e28a..71ec31cf 100644 --- a/vending-app/src/main/java/com/android/vending/licensing/LicenseRequest.java +++ b/vending-app/src/main/java/com/android/vending/licensing/LicenseRequest.java @@ -31,21 +31,27 @@ import com.android.volley.NetworkResponse; import com.android.volley.Request; import com.android.volley.Response; import com.android.volley.VolleyError; +import com.google.android.gms.common.BuildConfig; + +import org.microg.gms.profile.Build; import java.io.IOException; +import java.net.URLEncoder; +import java.util.Arrays; import java.util.Map; import java.util.UUID; +import java.util.stream.Collectors; import okio.ByteString; public abstract class LicenseRequest extends Request { - private final String xPsRh; private final String auth; private static final String TAG = "FakeLicenseRequest"; private static final int BASE64_FLAGS = Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING; - private static final long ANDROID_ID = 1; + long ANDROID_ID = 1; + private static final String FINSKY_VERSION = "Finsky/37.5.24-29%20%5B0%5D%20%5BPR%5D%20565477504"; private final Response.Listener successListener; @@ -55,6 +61,10 @@ public abstract class LicenseRequest extends Request { this.auth = auth; this.successListener = successListener; + } + + @Override + public Map getHeaders() { long millis = System.currentTimeMillis(); TimestampContainer.Builder timestamp = new TimestampContainer.Builder() @@ -107,9 +117,9 @@ public abstract class LicenseRequest extends Request { .deviceMeta(new DeviceMeta.Builder() .android( new AndroidVersionMeta.Builder() - .androidSdk(0) - .buildNumber("") - .androidVersion("") + .androidSdk(Build.VERSION.SDK_INT) + .buildNumber(Build.ID) + .androidVersion(Build.VERSION.RELEASE) .unknown(0) .build() ) @@ -119,13 +129,13 @@ public abstract class LicenseRequest extends Request { .build() ) .userAgent(new UserAgent.Builder() - .deviceProductName("") - .deviceSoc("") - .deviceModelName("") - .finskyVersion("") - .deviceName("") + .deviceName(Build.DEVICE) + .deviceHardware(Build.HARDWARE) + .deviceModelName(Build.MODEL) + .finskyVersion(FINSKY_VERSION) + .deviceProductName(Build.MODEL) .androidId(ANDROID_ID) // must not be 0 - .deviceSignature("") + .buildFingerprint(Build.FINGERPRINT) .build() ) .uuid(new Uuid.Builder() @@ -134,22 +144,29 @@ public abstract class LicenseRequest extends Request { .build() ) .build().encode(); - this.xPsRh = new String(Base64.encode(Util.encodeGzip(header), BASE64_FLAGS)); - - //Log.d(TAG, "Product " + Build.PRODUCT + ", Board " + Build.BOARD + " Model " + Build.MODEL + " Device " + Build.DEVICE); + String xPsRh = new String(Base64.encode(Util.encodeGzip(header), BASE64_FLAGS)); Log.v(TAG, "X-PS-RH: " + xPsRh); - } - @Override - public Map getHeaders() { + String userAgent = FINSKY_VERSION + " (api=3,versionCode=" + BuildConfig.VERSION_CODE + ",sdk=" + Build.VERSION.SDK + + ",device=" + encodeString(Build.DEVICE) + ",hardware=" + encodeString(Build.HARDWARE) + ",product=" + encodeString(Build.PRODUCT) + + ",platformVersionRelease=" + encodeString(Build.VERSION.RELEASE) + ",model=" + encodeString(Build.MODEL) + ",buildId=" + encodeString(Build.ID) + + ",isWideScreen=" + 0 + ",supportedAbis=" + String.join(";", Build.SUPPORTED_ABIS) + ")"; + Log.v(TAG, "User-Agent: " + userAgent); + return Map.of( "X-PS-RH", xPsRh, + "User-Agent", userAgent, "Authorization", "Bearer " + auth, + "Accept-Language", "en-US", "Connection", "Keep-Alive" ); } + private static String encodeString(String s) { + return URLEncoder.encode(s).replace("+", "%20"); + } + @Override protected void deliverResponse(T response) { successListener.onResponse(response); diff --git a/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java b/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java index a193199b..9f060226 100644 --- a/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java +++ b/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java @@ -18,15 +18,24 @@ import android.os.Bundle; import android.os.IBinder; import android.os.RemoteException; import android.net.Uri; +import android.net.Uri; +import android.os.Bundle; +import android.os.IBinder; +import android.os.RemoteException; +import android.provider.Settings; import android.util.Log; import com.android.volley.RequestQueue; import com.android.volley.toolbox.Volley; import org.microg.gms.auth.AuthConstants; +import org.microg.gms.profile.Build; +import org.microg.gms.profile.ProfileManager; import java.util.Arrays; +import java.util.HashMap; import java.util.LinkedList; +import java.util.Map; import java.util.Queue; import kotlin.Unit; @@ -43,6 +52,8 @@ public class LicensingService extends Service { private static final String PREFERENCE_LICENSING_ENABLED = "play_licensing"; + private static final Uri PROFILE_PROVIDER = Uri.parse("content://com.google.android.gms.microg.profile"); + private String androidId; private final ILicensingService.Stub mLicenseService = new ILicensingService.Stub() { @@ -93,7 +104,7 @@ public class LicensingService extends Service { private void checkLicense(long nonce, String packageName, PackageManager packageManager, ILicenseResultListener listener, Queue remainingAccounts) throws RemoteException { new LicenseChecker.V1().checkLicense( - remainingAccounts.poll(), accountManager, packageName, packageManager, + remainingAccounts.poll(), accountManager, androidId, packageName, packageManager, queue, nonce, (responseCode, stringTuple) -> { if (responseCode != LICENSED && !remainingAccounts.isEmpty()) { @@ -128,7 +139,7 @@ public class LicensingService extends Service { ILicenseV2ResultListener listener, Bundle extraParams, Queue remainingAccounts) throws RemoteException { new LicenseChecker.V2().checkLicense( - remainingAccounts.poll(), accountManager, packageName, packageManager, queue, Unit.INSTANCE, + remainingAccounts.poll(), accountManager, androidId, packageName, packageManager, queue, Unit.INSTANCE, (responseCode, data) -> { /* * Suppress failures on V2. V2 is commonly used by free apps whose checker @@ -167,6 +178,32 @@ public class LicensingService extends Service { }; public IBinder onBind(Intent intent) { + + + Cursor cursor = null; + try { + cursor = getContentResolver().query( + PROFILE_PROVIDER, null, null, null, null + ); + + if (cursor == null || cursor.getColumnCount() != 2) { + Log.e(TAG, "profile provider not available"); + } else { + Map profileData = new HashMap<>(); + while (cursor.moveToNext()) { + profileData.put(cursor.getString(0), cursor.getString(1)); + } + ProfileManager.INSTANCE.applyProfileData(profileData); + } + + } finally { + if (cursor != null) { + cursor.close(); + } + } + + androidId = String.valueOf(Settings.Secure.getString(this.getContentResolver(), Settings.Secure.ANDROID_ID)); + queue = Volley.newRequestQueue(this); accountManager = AccountManager.get(this); notificationRunnable = new LicenseServiceNotificationRunnable(this); diff --git a/vending-app/src/main/proto/LicenseRequest.proto b/vending-app/src/main/proto/LicenseRequest.proto new file mode 100644 index 00000000..dc0b7133 --- /dev/null +++ b/vending-app/src/main/proto/LicenseRequest.proto @@ -0,0 +1,71 @@ +syntax = "proto2"; + +option java_package = "com.android.vending"; +option java_multiple_files = true; + +message LicenseRequestHeader { + optional StringWrapper encodedTimestamps = 1; + optional EncodedTripleWrapper triple = 10; + optional LocalityWrapper locality = 11; + optional IntWrapper unknown = 12; + optional string empty = 14; + optional DeviceMeta deviceMeta = 20; + optional UserAgent userAgent = 21; + optional Uuid uuid = 27; +} + +message StringWrapper { + optional string string = 1; +} + +message EncodedTripleWrapper { + optional EncodedTriple triple = 1; +} + +message EncodedTriple { + optional string encoded1 = 1; + optional string encoded2 = 2; + optional string empty = 3; +} + +message LocalityWrapper { + optional string encodedLocalityProto = 1; +} + +message IntWrapper { + optional uint32 integer = 1; +} + +message DeviceMeta { + optional AndroidVersionMeta android = 1; + optional UnknownByte12 unknown1 = 2; + optional uint32 unknown2 = 3; // observed value: 1 + +} + +message AndroidVersionMeta { + optional uint32 androidSdk = 1; + optional string buildNumber = 2; + optional string androidVersion = 3; + optional uint32 unknown = 4; +} + +message UnknownByte12 { + optional bytes bytes = 12; +} + +message UserAgent { + // The names of these attributes are vague guesses and should be adapted if needed. + optional string deviceName = 1; // e.g. "OnePlusNord" + optional string deviceHardware = 2; // e.g. "qcom" + optional string deviceModelName = 3; // e.g. "OnePlus Nord" + optional string finskyVersion = 4; // e.g. "Finsky/37.5.24-29%20%5B0%5D%20%5BPR%5D%20565477504" + optional string deviceProductName = 5; // e.g. "OnePlusNord" + optional uint64 androidId = 6; + optional string buildFingerprint = 7; // e.g. "google/walleye/walleye:8.1.0/OPM1.171019.011/4448085:user/release-keys" +} + +message Uuid { + optional string uuid = 1; + optional uint32 unknown = 2; // might be a constant, e.g. format ID. Observed value: 2. +} From 807ccbd6035ca325a553ac16f2759189ac0b264a Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Sun, 10 Dec 2023 00:38:40 +0100 Subject: [PATCH 034/209] Apply review --- .../main/java/com/android/vending/licensing/LicenseRequest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/vending-app/src/main/java/com/android/vending/licensing/LicenseRequest.java b/vending-app/src/main/java/com/android/vending/licensing/LicenseRequest.java index 71ec31cf..843716f0 100644 --- a/vending-app/src/main/java/com/android/vending/licensing/LicenseRequest.java +++ b/vending-app/src/main/java/com/android/vending/licensing/LicenseRequest.java @@ -40,7 +40,6 @@ import java.net.URLEncoder; import java.util.Arrays; import java.util.Map; import java.util.UUID; -import java.util.stream.Collectors; import okio.ByteString; From b5b603a340ee9b31f02c40655f4560cc231e0110 Mon Sep 17 00:00:00 2001 From: Jonathan Klee Date: Tue, 19 Dec 2023 09:32:25 +0100 Subject: [PATCH 035/209] Use Google androidId for Licensing Licensing app feature expects a Google AndroidId. --- .../vending/licensing/LicenseChecker.java | 6 ++++- .../vending/licensing/LicensingService.java | 25 ++++++++++++++----- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/vending-app/src/main/java/com/android/vending/licensing/LicenseChecker.java b/vending-app/src/main/java/com/android/vending/licensing/LicenseChecker.java index 6e4dcbb9..1b38e23c 100644 --- a/vending-app/src/main/java/com/android/vending/licensing/LicenseChecker.java +++ b/vending-app/src/main/java/com/android/vending/licensing/LicenseChecker.java @@ -120,7 +120,11 @@ public abstract class LicenseChecker { String auth = future.getResult().getString(KEY_AUTHTOKEN); LicenseRequest request = createRequest(packageName, auth, versionCode, queryData, onRequestFinished, onRequestError); - request.ANDROID_ID = Long.decode("0x" + androidId); + + if (androidId != null) { + request.ANDROID_ID = Long.parseLong(androidId, 16); + } + request.setShouldCache(false); queue.add(request); } catch (AuthenticatorException | IOException | OperationCanceledException e) { diff --git a/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java b/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java index 9f060226..f821dd55 100644 --- a/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java +++ b/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java @@ -22,7 +22,6 @@ import android.net.Uri; import android.os.Bundle; import android.os.IBinder; import android.os.RemoteException; -import android.provider.Settings; import android.util.Log; import com.android.volley.RequestQueue; @@ -45,17 +44,18 @@ public class LicensingService extends Service { private RequestQueue queue; private AccountManager accountManager; private LicenseServiceNotificationRunnable notificationRunnable; + private String androidId; private static final String KEY_V2_RESULT_JWT = "LICENSE_DATA"; + private static final Uri PROFILE_PROVIDER = Uri.parse("content://com.google.android.gms.microg.profile"); + + private static final Uri CHECKIN_SETTINGS_PROVIDER = Uri.parse("content://com.google.android.gms.microg.settings/check-in"); + private static final Uri SETTINGS_PROVIDER = Uri.parse("content://com.google.android.gms.microg.settings/play"); private static final String PREFERENCE_LICENSING_ENABLED = "play_licensing"; - private static final Uri PROFILE_PROVIDER = Uri.parse("content://com.google.android.gms.microg.profile"); - - private String androidId; - private final ILicensingService.Stub mLicenseService = new ILicensingService.Stub() { private boolean shouldCheckLicense() { @@ -202,7 +202,20 @@ public class LicensingService extends Service { } } - androidId = String.valueOf(Settings.Secure.getString(this.getContentResolver(), Settings.Secure.ANDROID_ID)); + try { + cursor = getContentResolver().query( + CHECKIN_SETTINGS_PROVIDER, new String[] { "androidId" }, null, null, null + ); + + if (cursor != null) { + cursor.moveToNext(); + androidId = Long.toHexString(cursor.getLong(0)); + } + } finally { + if (cursor != null) { + cursor.close(); + } + } queue = Volley.newRequestQueue(this); accountManager = AccountManager.get(this); From ed36df64634a6ee4310e39db4f4cbb3bd2316653 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Tue, 24 Oct 2023 07:43:47 +0200 Subject: [PATCH 036/209] Check preference from within Licensing service --- vending-app/src/main/AndroidManifest.xml | 1 + .../java/com/android/vending/licensing/LicensingService.java | 5 ++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/vending-app/src/main/AndroidManifest.xml b/vending-app/src/main/AndroidManifest.xml index bd61b73b..bfc54213 100644 --- a/vending-app/src/main/AndroidManifest.xml +++ b/vending-app/src/main/AndroidManifest.xml @@ -14,6 +14,7 @@ + Date: Thu, 11 Jan 2024 23:15:09 +0100 Subject: [PATCH 037/209] Fix swapped docstrings --- .../java/com/android/vending/licensing/LicenseChecker.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/vending-app/src/main/java/com/android/vending/licensing/LicenseChecker.java b/vending-app/src/main/java/com/android/vending/licensing/LicenseChecker.java index 1b38e23c..83e76c2f 100644 --- a/vending-app/src/main/java/com/android/vending/licensing/LicenseChecker.java +++ b/vending-app/src/main/java/com/android/vending/licensing/LicenseChecker.java @@ -44,12 +44,12 @@ public abstract class LicenseChecker { */ static final int LICENSED = 0x0; /** - * The application is licensed to the user, but there is an updated application version available that is - * signed with a different key. + * The application is not licensed to the user. */ static final int NOT_LICENSED = 0x1; /** - * The application is not licensed to the user. + * The application is licensed to the user, but there is an updated application version available that is + * signed with a different key. */ static final int LICENSED_OLD_KEY = 0x2; /** From ce4c4d23d26bb4d5b084d8df4bce0c0c14d680af Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Thu, 11 Jan 2024 23:38:34 +0100 Subject: [PATCH 038/209] Fix Ignore on notification not working --- .../licensing/LicenseServiceNotificationRunnable.java | 7 ++++++- .../com/android/vending/licensing/LicensingService.java | 4 ++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/vending-app/src/main/java/com/android/vending/licensing/LicenseServiceNotificationRunnable.java b/vending-app/src/main/java/com/android/vending/licensing/LicenseServiceNotificationRunnable.java index 7a82341b..34567d1d 100644 --- a/vending-app/src/main/java/com/android/vending/licensing/LicenseServiceNotificationRunnable.java +++ b/vending-app/src/main/java/com/android/vending/licensing/LicenseServiceNotificationRunnable.java @@ -70,7 +70,7 @@ public class LicenseServiceNotificationRunnable implements Runnable { ignoreIntent.putExtra(INTENT_KEY_IGNORE_PACKAGE_NAME, callerPackageName); ignoreIntent.putExtra(INTENT_KEY_NOTIFICATION_ID, callerUid); PendingIntent ignorePendingIntent = PendingIntent.getBroadcast( - context, callerUid * 2 + 1, ignoreIntent, PendingIntent.FLAG_IMMUTABLE + context, callerUid * 2 + 1, ignoreIntent, PendingIntent.FLAG_MUTABLE ); Notification notification = new NotificationCompat.Builder(context, CHANNEL_ID) @@ -128,6 +128,11 @@ public class LicenseServiceNotificationRunnable implements Runnable { ); String newIgnorePackage = intent.getStringExtra(INTENT_KEY_IGNORE_PACKAGE_NAME); + if (newIgnorePackage == null) { + Log.e(TAG, "Received no ignore package; can't add to ignore list."); + return; + } + Log.d(TAG, "Adding package " + newIgnorePackage + " to ignore list"); ignoreList.add(newIgnorePackage); diff --git a/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java b/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java index 95b4a027..c66e9b72 100644 --- a/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java +++ b/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java @@ -163,11 +163,15 @@ public class LicensingService extends Service { } private void handleNoAccounts(String packageName, PackageManager packageManager) { + notificationRunnable.callerPackageName = packageName; try { Log.e(TAG, "not checking license, as user is not signed in"); PackageInfo packageInfo = packageManager.getPackageInfo(packageName, 0); notificationRunnable.callerUid = packageInfo.applicationInfo.uid; notificationRunnable.callerAppName = packageManager.getApplicationLabel(packageInfo.applicationInfo); + if (notificationRunnable.callerAppName == null) { + notificationRunnable.callerAppName = packageName; + } } catch (PackageManager.NameNotFoundException e) { Log.e(TAG, "ignored license request, but package name " + packageName + " was not known!"); notificationRunnable.callerAppName = packageName; From 2a2793c466d8249c03708651533e1df44bc9b6a5 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Thu, 11 Jan 2024 23:49:35 +0100 Subject: [PATCH 039/209] Enable multidex --- vending-app/build.gradle | 3 +++ vending-app/src/main/AndroidManifest.xml | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/vending-app/build.gradle b/vending-app/build.gradle index 5c15378b..96dac40c 100644 --- a/vending-app/build.gradle +++ b/vending-app/build.gradle @@ -17,6 +17,8 @@ android { versionCode vendingAppVersionCode minSdkVersion androidMinSdk targetSdkVersion androidTargetSdk + + multiDexEnabled true } flavorDimensions = ['target'] @@ -70,6 +72,7 @@ dependencies { implementation "com.squareup.wire:wire-runtime:$wireVersion" implementation "com.android.volley:volley:$volleyVersion" + implementation "androidx.multidex:multidex:$multidexVersion" } wire { diff --git a/vending-app/src/main/AndroidManifest.xml b/vending-app/src/main/AndroidManifest.xml index bfc54213..6916b5df 100644 --- a/vending-app/src/main/AndroidManifest.xml +++ b/vending-app/src/main/AndroidManifest.xml @@ -24,7 +24,8 @@ android:forceQueryable="true" android:icon="@mipmap/ic_app" android:roundIcon="@mipmap/ic_app" - android:label="@string/app_name"> + android:label="@string/app_name" + android:name="androidx.multidex.MultiDexApplication"> Date: Mon, 22 Jan 2024 19:34:06 +0100 Subject: [PATCH 040/209] Check for `POST_NOTIFICATIONS` permission --- .../licensing/LicenseServiceNotificationRunnable.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/vending-app/src/main/java/com/android/vending/licensing/LicenseServiceNotificationRunnable.java b/vending-app/src/main/java/com/android/vending/licensing/LicenseServiceNotificationRunnable.java index 34567d1d..daec6121 100644 --- a/vending-app/src/main/java/com/android/vending/licensing/LicenseServiceNotificationRunnable.java +++ b/vending-app/src/main/java/com/android/vending/licensing/LicenseServiceNotificationRunnable.java @@ -1,5 +1,6 @@ package com.android.vending.licensing; +import android.Manifest; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; @@ -8,10 +9,11 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; +import android.content.pm.PackageManager; import android.os.Build; import android.util.Log; -import androidx.annotation.CallSuper; +import androidx.core.app.ActivityCompat; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; @@ -92,8 +94,10 @@ public class LicenseServiceNotificationRunnable implements Runnable { .build(); NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); - notificationManager.notify(callerUid, notification); - + if (ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) == + PackageManager.PERMISSION_GRANTED) { + notificationManager.notify(callerUid, notification); + } } private void registerNotificationChannel() { From ddd0968e99b17a2b56ff4f9fce6daa05905318a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C2=A8Jonathan?= <¨jonathan.klee@e.email¨> Date: Wed, 17 Jan 2024 09:09:20 +0100 Subject: [PATCH 041/209] Use big text style so that the text is displayed fully --- .../vending/licensing/LicenseServiceNotificationRunnable.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/vending-app/src/main/java/com/android/vending/licensing/LicenseServiceNotificationRunnable.java b/vending-app/src/main/java/com/android/vending/licensing/LicenseServiceNotificationRunnable.java index daec6121..84a17dac 100644 --- a/vending-app/src/main/java/com/android/vending/licensing/LicenseServiceNotificationRunnable.java +++ b/vending-app/src/main/java/com/android/vending/licensing/LicenseServiceNotificationRunnable.java @@ -75,11 +75,13 @@ public class LicenseServiceNotificationRunnable implements Runnable { context, callerUid * 2 + 1, ignoreIntent, PendingIntent.FLAG_MUTABLE ); + String contentText = context.getString(R.string.license_notification_body); Notification notification = new NotificationCompat.Builder(context, CHANNEL_ID) .setSmallIcon(R.drawable.ic_notification) .setSound(null) .setContentTitle(context.getString(R.string.license_notification_title, callerAppName)) - .setContentText(context.getString(R.string.license_notification_body)) + .setContentText(contentText) + .setStyle(new NotificationCompat.BigTextStyle().bigText(contentText)) .addAction( new NotificationCompat.Action.Builder( null, context.getString(R.string.license_notification_sign_in), authPendingIntent From 9a1391a7a978c3e74b2f0775e683bbea23c60bce Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Mon, 22 Jan 2024 20:43:14 +0100 Subject: [PATCH 042/209] Disable `GetLocales` lint Like f0b15d275a0c4ec3625033d651414d396e368320 --- vending-app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vending-app/build.gradle b/vending-app/build.gradle index 96dac40c..8cc4f93a 100644 --- a/vending-app/build.gradle +++ b/vending-app/build.gradle @@ -52,7 +52,7 @@ android { } lintOptions { - disable 'MissingTranslation' + disable 'MissingTranslation', 'GetLocales' } compileOptions { From 3ebc4f23ee7cb236eacb589abf60c0e95b410254 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Mon, 29 Jan 2024 13:40:06 +0100 Subject: [PATCH 043/209] Enable postprocessing in vending-app --- vending-app/build.gradle | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/vending-app/build.gradle b/vending-app/build.gradle index 8cc4f93a..468e45c1 100644 --- a/vending-app/build.gradle +++ b/vending-app/build.gradle @@ -21,6 +21,25 @@ android { multiDexEnabled true } + buildTypes { + debug { + postprocessing { + removeUnusedCode true + removeUnusedResources true + obfuscate false + optimizeCode true + } + } + release { + postprocessing { + removeUnusedCode true + removeUnusedResources true + obfuscate false + optimizeCode true + } + } + } + flavorDimensions = ['target'] productFlavors { "default" { From 5594e31c11cb3eee990c53f1497e09b21cf1f6f4 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Mon, 29 Jan 2024 13:40:24 +0100 Subject: [PATCH 044/209] Revert "Enable multidex" This reverts commit da7bb3d46bc25f8b53c68f3c56197cbf7b6993f5. --- vending-app/build.gradle | 3 --- vending-app/src/main/AndroidManifest.xml | 3 +-- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/vending-app/build.gradle b/vending-app/build.gradle index 468e45c1..f57b700b 100644 --- a/vending-app/build.gradle +++ b/vending-app/build.gradle @@ -17,8 +17,6 @@ android { versionCode vendingAppVersionCode minSdkVersion androidMinSdk targetSdkVersion androidTargetSdk - - multiDexEnabled true } buildTypes { @@ -91,7 +89,6 @@ dependencies { implementation "com.squareup.wire:wire-runtime:$wireVersion" implementation "com.android.volley:volley:$volleyVersion" - implementation "androidx.multidex:multidex:$multidexVersion" } wire { diff --git a/vending-app/src/main/AndroidManifest.xml b/vending-app/src/main/AndroidManifest.xml index 6916b5df..bfc54213 100644 --- a/vending-app/src/main/AndroidManifest.xml +++ b/vending-app/src/main/AndroidManifest.xml @@ -24,8 +24,7 @@ android:forceQueryable="true" android:icon="@mipmap/ic_app" android:roundIcon="@mipmap/ic_app" - android:label="@string/app_name" - android:name="androidx.multidex.MultiDexApplication"> + android:label="@string/app_name"> Date: Sat, 17 Feb 2024 04:41:08 +0800 Subject: [PATCH 045/209] Add dummy service for PANORAMA(3) (#2173) Co-authored-by: Marvin W --- play-services-core/build.gradle | 63 ++++++++++++++ .../src/main/AndroidManifest.xml | 87 +++++++++++++++++++ play-services-panorama/build.gradle | 41 +++++++++ play-services-panorama/core/build.gradle | 56 ++++++++++++ .../core/src/main/AndroidManifest.xml | 15 ++++ .../microg/gms/panorama/PanoramaService.kt | 31 +++++++ .../src/AndroidManifest.xml | 6 ++ .../google/android/gms/panorama/Panorama.java | 30 +++++++ .../android/gms/panorama/PanoramaApi.java | 50 +++++++++++ .../panorama/internal/IPanoramaCallbacks.aidl | 11 +++ .../panorama/internal/IPanoramaService.aidl | 13 +++ settings.gradle | 69 +++++++++++++++ 12 files changed, 472 insertions(+) create mode 100644 play-services-panorama/build.gradle create mode 100644 play-services-panorama/core/build.gradle create mode 100644 play-services-panorama/core/src/main/AndroidManifest.xml create mode 100644 play-services-panorama/core/src/main/kotlin/org/microg/gms/panorama/PanoramaService.kt create mode 100644 play-services-panorama/src/AndroidManifest.xml create mode 100644 play-services-panorama/src/main/aidl/com/google/android/gms/panorama/Panorama.java create mode 100644 play-services-panorama/src/main/aidl/com/google/android/gms/panorama/PanoramaApi.java create mode 100644 play-services-panorama/src/main/aidl/com/google/android/gms/panorama/internal/IPanoramaCallbacks.aidl create mode 100644 play-services-panorama/src/main/aidl/com/google/android/gms/panorama/internal/IPanoramaService.aidl diff --git a/play-services-core/build.gradle b/play-services-core/build.gradle index 2449e81f..6ce62d2c 100644 --- a/play-services-core/build.gradle +++ b/play-services-core/build.gradle @@ -20,7 +20,70 @@ apply plugin: 'kotlin-kapt' dependencies { implementation "com.squareup.wire:wire-runtime:$wireVersion" +<<<<<<< HEAD implementation "de.hdodenhof:circleimageview:$circleimageviewVersion" +======= + implementation "de.hdodenhof:circleimageview:1.3.0" + + implementation project(':fake-signature') + implementation project(':firebase-dynamic-links') + implementation project(':firebase-auth-core') + implementation project(':play-services-ads-core') + implementation project(':play-services-ads-identifier-core') + implementation project(':play-services-ads-lite-core') + implementation project(':play-services-appinvite-core') + implementation project(':play-services-auth-api-phone-core') + implementation project(':play-services-base-core') + implementation project(':play-services-cast-core') + implementation project(':play-services-cast-framework-core') + implementation project(':play-services-conscrypt-provider-core') + implementation project(':play-services-cronet-core') + implementation project(':play-services-droidguard-core') + implementation project(':play-services-fido-core') + implementation project(':play-services-gmscompliance-core') + implementation project(':play-services-location-core') + implementation project(':play-services-location-core-base') + implementation project(':play-services-oss-licenses-core') + implementation project(':play-services-panorama-core') + implementation project(':play-services-pay-core') + implementation project(':play-services-recaptcha-core') + implementation project(':play-services-safetynet-core') + implementation project(':play-services-tapandpay-core') + implementation project(':play-services-vision-core') + implementation project(':play-services-wearable-core') + + implementation project(':play-services-core-proto') + + implementation project(':play-services-core:microg-ui-tools') // deprecated + implementation project(':play-services-base-core-package') + implementation project(':play-services-api') + + implementation project(':play-services-appinvite') + implementation project(':play-services-auth-base') + implementation project(':play-services-auth') + implementation project(':play-services-clearcut') + implementation project(':play-services-drive') + implementation project(':play-services-games') + implementation project(':play-services-maps') + implementation project(':play-services-measurement-base') + implementation project(':play-services-places') + implementation project(':play-services-recaptcha') + implementation project(':play-services-safetynet') + implementation project(':play-services-tasks-ktx') + + mapboxRuntimeOnly project(':play-services-maps-core-mapbox') + vtmRuntimeOnly project(':play-services-maps-core-vtm') + defaultRuntimeOnly project(':play-services-location-core-provider') + + if (hasModule("nearby", true)) runtimeOnly project(':play-services-nearby-core-package') + if (hasModule("hms", false)) hmsRuntimeOnly project(':play-services-maps-core-hms') + + // AndroidX UI + implementation "androidx.multidex:multidex:$multidexVersion" + implementation "androidx.appcompat:appcompat:$appcompatVersion" + implementation "androidx.mediarouter:mediarouter:$mediarouterVersion" + implementation "androidx.preference:preference-ktx:$preferenceVersion" +>>>>>>> 1b34bbd3 (Add dummy service for PANORAMA(3) (#2173)) implementation "androidx.webkit:webkit:$webkitVersion" implementation project(':play-services-base-core') diff --git a/play-services-core/src/main/AndroidManifest.xml b/play-services-core/src/main/AndroidManifest.xml index cec102fb..80ceba01 100644 --- a/play-services-core/src/main/AndroidManifest.xml +++ b/play-services-core/src/main/AndroidManifest.xml @@ -597,12 +597,99 @@ >>>>>>> 048a6e59 (Add identity sign-in service (#2130)) +<<<<<<< HEAD +======= + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +>>>>>>> 1b34bbd3 (Add dummy service for PANORAMA(3) (#2173)) diff --git a/play-services-panorama/build.gradle b/play-services-panorama/build.gradle new file mode 100644 index 00000000..2c230a67 --- /dev/null +++ b/play-services-panorama/build.gradle @@ -0,0 +1,41 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +apply plugin: 'com.android.library' +apply plugin: 'maven-publish' +apply plugin: 'signing' + +android { + namespace "com.google.android.gms.panorama" + + compileSdkVersion androidCompileSdk + buildToolsVersion "$androidBuildVersionTools" + + buildFeatures { + aidl = true + } + + defaultConfig { + versionName version + minSdkVersion androidMinSdk + targetSdkVersion androidTargetSdk + } + + compileOptions { + sourceCompatibility = 1.8 + targetCompatibility = 1.8 + } +} + +apply from: '../gradle/publish-android.gradle' + +description = 'microG implementation of play-services-panorama' + +dependencies { + // Dependencies from play-services-panorama:17.1.0 + api project(":play-services-base") + api project(":play-services-basement") + api project(":play-services-tasks") +} diff --git a/play-services-panorama/core/build.gradle b/play-services-panorama/core/build.gradle new file mode 100644 index 00000000..81ce810d --- /dev/null +++ b/play-services-panorama/core/build.gradle @@ -0,0 +1,56 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'maven-publish' +apply plugin: 'signing' + +dependencies { + api project(':play-services-panorama') + + implementation project(':play-services-base-core') + + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutineVersion" +} + +android { + namespace "org.microg.gms.panorama.core" + + compileSdkVersion androidCompileSdk + buildToolsVersion "$androidBuildVersionTools" + + defaultConfig { + versionName version + minSdkVersion androidMinSdk + targetSdkVersion androidTargetSdk + } + + sourceSets { + main { + java.srcDirs = ['src/main/kotlin'] + } + } + + lintOptions { + disable 'MissingTranslation' + } + + compileOptions { + sourceCompatibility = 1.8 + targetCompatibility = 1.8 + } + + kotlinOptions { + jvmTarget = 1.8 + } +} + +// Nothing to publish yet +//apply from: '../gradle/publish-android.gradle' + +description = 'microG service implementation for play-services-panorama' diff --git a/play-services-panorama/core/src/main/AndroidManifest.xml b/play-services-panorama/core/src/main/AndroidManifest.xml new file mode 100644 index 00000000..fbe8d244 --- /dev/null +++ b/play-services-panorama/core/src/main/AndroidManifest.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/play-services-panorama/core/src/main/kotlin/org/microg/gms/panorama/PanoramaService.kt b/play-services-panorama/core/src/main/kotlin/org/microg/gms/panorama/PanoramaService.kt new file mode 100644 index 00000000..382a698c --- /dev/null +++ b/play-services-panorama/core/src/main/kotlin/org/microg/gms/panorama/PanoramaService.kt @@ -0,0 +1,31 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ +package org.microg.gms.panorama + +import android.net.Uri +import android.os.Bundle +import android.util.Log +import com.google.android.gms.common.api.CommonStatusCodes +import com.google.android.gms.common.internal.GetServiceRequest +import com.google.android.gms.common.internal.IGmsCallbacks +import com.google.android.gms.panorama.internal.IPanoramaCallbacks +import com.google.android.gms.panorama.internal.IPanoramaService +import org.microg.gms.BaseService +import org.microg.gms.common.GmsService + +const val TAG = "PanoramaService" + +class PanoramaService : BaseService(TAG, GmsService.PANORAMA) { + override fun handleServiceRequest(callback: IGmsCallbacks, request: GetServiceRequest, service: GmsService) { + callback.onPostInitComplete(CommonStatusCodes.SUCCESS, PanoramaServiceImpl().asBinder(), null) + } +} + +class PanoramaServiceImpl : IPanoramaService.Stub() { + override fun loadPanoramaInfo(callback: IPanoramaCallbacks?, uri: Uri, bundle: Bundle, needGrantReadUriPermissions: Boolean) { + Log.d(TAG, "Not implemented! $uri bundle:$bundle") + runCatching { callback?.onPanoramaResult(CommonStatusCodes.SUCCESS, null, 0, null) } + } +} diff --git a/play-services-panorama/src/AndroidManifest.xml b/play-services-panorama/src/AndroidManifest.xml new file mode 100644 index 00000000..210a05c1 --- /dev/null +++ b/play-services-panorama/src/AndroidManifest.xml @@ -0,0 +1,6 @@ + + + diff --git a/play-services-panorama/src/main/aidl/com/google/android/gms/panorama/Panorama.java b/play-services-panorama/src/main/aidl/com/google/android/gms/panorama/Panorama.java new file mode 100644 index 00000000..005b0f76 --- /dev/null +++ b/play-services-panorama/src/main/aidl/com/google/android/gms/panorama/Panorama.java @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. + */ + +package com.google.android.gms.panorama; + +import com.google.android.gms.common.api.Api; +import com.google.android.gms.common.api.GoogleApiClient; + +/** + * The main entry point for panorama integration. + */ +@Deprecated +public class Panorama { + /** + * Token to pass to {@link GoogleApiClient.Builder#addApi(Api)} to enable the Panorama features. + */ + @Deprecated + public static final Api API = null; + + /** + * The entry point for interacting with the Panorama API. + */ + @Deprecated + public static final PanoramaApi PanoramaApi = null; +} diff --git a/play-services-panorama/src/main/aidl/com/google/android/gms/panorama/PanoramaApi.java b/play-services-panorama/src/main/aidl/com/google/android/gms/panorama/PanoramaApi.java new file mode 100644 index 00000000..9c255f22 --- /dev/null +++ b/play-services-panorama/src/main/aidl/com/google/android/gms/panorama/PanoramaApi.java @@ -0,0 +1,50 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. + */ + +package com.google.android.gms.panorama; + +import android.content.Intent; +import android.net.Uri; +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.PendingResult; +import com.google.android.gms.common.api.Result; + +/** + * The main entry point for interacting with Panorama viewer. This class provides methods for obtaining an Intent to view a Panorama. + */ +@Deprecated +public interface PanoramaApi { + /** + * Loads information about a panorama. + * + * @param uri the URI of the panorama to load info about. May be a file:, content:, or android_resource: scheme. + */ + @Deprecated + PendingResult loadPanoramaInfo(GoogleApiClient client, Uri uri); + + /** + * Loads information about a panorama from a content provider. This method will also explicitly grant and revoke access to + * the URI while the load is happening so images in content providers may be inspected without giving permission to an + * entire content provider. The returned viewer intent will also have the {@link Intent#FLAG_GRANT_READ_URI_PERMISSION} set so + * the viewer has access. + */ + @Deprecated + PendingResult loadPanoramaInfoAndGrantAccess(GoogleApiClient client, Uri uri); + + /** + * Result interface for loading panorama info. + */ + @Deprecated + interface PanoramaResult extends Result { + /** + * Returns if the image is a panorama this is not null and will launch a viewer when started. If the image is not a panorama this will be null. + */ + @Deprecated + Intent getViewerIntent(); + } +} diff --git a/play-services-panorama/src/main/aidl/com/google/android/gms/panorama/internal/IPanoramaCallbacks.aidl b/play-services-panorama/src/main/aidl/com/google/android/gms/panorama/internal/IPanoramaCallbacks.aidl new file mode 100644 index 00000000..58b92427 --- /dev/null +++ b/play-services-panorama/src/main/aidl/com/google/android/gms/panorama/internal/IPanoramaCallbacks.aidl @@ -0,0 +1,11 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ +package com.google.android.gms.panorama.internal; + +import android.content.Intent; + +interface IPanoramaCallbacks { + void onPanoramaResult(int statusCode, in Bundle statusExtras, int unknown, in Intent viewerIntent); +} \ No newline at end of file diff --git a/play-services-panorama/src/main/aidl/com/google/android/gms/panorama/internal/IPanoramaService.aidl b/play-services-panorama/src/main/aidl/com/google/android/gms/panorama/internal/IPanoramaService.aidl new file mode 100644 index 00000000..3bada8be --- /dev/null +++ b/play-services-panorama/src/main/aidl/com/google/android/gms/panorama/internal/IPanoramaService.aidl @@ -0,0 +1,13 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ +package com.google.android.gms.panorama.internal; + +import com.google.android.gms.panorama.internal.IPanoramaCallbacks; +import android.os.Bundle; +import android.net.Uri; + +interface IPanoramaService { + void loadPanoramaInfo(IPanoramaCallbacks callback, in Uri uri, in Bundle bundle, boolean needGrantReadUriPermissions) = 0; +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 7725a9a8..7b51c22b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,5 +1,32 @@ include ':play-services-basement' +<<<<<<< HEAD +======= +include ':play-services-cast' +include ':play-services-cast-framework' +include ':play-services-clearcut' +include ':play-services-drive' +include ':play-services-droidguard' +include ':play-services-fido' +include ':play-services-games' +include ':play-services-gcm' +include ':play-services-gmscompliance' +include ':play-services-iid' +include ':play-services-location' +include ':play-services-maps' +include ':play-services-measurement-base' +sublude ':play-services-mlkit:barcode-scanning' +include ':play-services-nearby' +include ':play-services-oss-licenses' +include ':play-services-panorama' +include ':play-services-pay' +include ':play-services-phenotype' +include ':play-services-places' +include ':play-services-places-placereport' +include ':play-services-recaptcha' +include ':play-services-safetynet' +include ':play-services-tapandpay' +>>>>>>> 1b34bbd3 (Add dummy service for PANORAMA(3) (#2173)) include ':play-services-tasks' include ':play-services-base-api' @@ -9,6 +36,48 @@ include ':play-services-api' include ':play-services-core-proto' +<<<<<<< HEAD +======= +sublude ':play-services-basement:ktx' +sublude ':play-services-tasks:ktx' + +sublude ':play-services-ads:core' +sublude ':play-services-ads-identifier:core' +sublude ':play-services-ads-lite:core' +sublude ':play-services-appinvite:core' +sublude ':play-services-auth-api-phone:core' +sublude ':play-services-base:core' +sublude ':play-services-base:core:package' +sublude ':play-services-cast:core' +sublude ':play-services-cast-framework:core' +include ':play-services-chimera-core' +include ':play-services-conscrypt-provider-core' +sublude ':play-services-cronet:core' +sublude ':play-services-droidguard:core' +sublude ':play-services-fido:core' +sublude ':play-services-gmscompliance:core' +sublude ':play-services-location:core' +sublude ':play-services-location:core:base' +sublude ':play-services-location:core:provider' +sublude ':play-services-location:core:system-api' +if (hasModule("hms", false)) sublude ':play-services-maps:core:hms' +sublude ':play-services-maps:core:mapbox' +sublude ':play-services-maps:core:vtm' +sublude ':play-services-maps:core:vtm:microg-theme' +if (hasModule("nearby", true)) sublude ':play-services-nearby:core' +if (hasModule("nearby", true)) sublude ':play-services-nearby:core:package' +sublude ':play-services-oss-licenses:core' +sublude ':play-services-panorama:core' +sublude ':play-services-pay:core' +sublude ':play-services-safetynet:core' +sublude ':play-services-recaptcha:core' +sublude ':play-services-tapandpay:core' +sublude ':play-services-vision:core' +sublude ':play-services-wearable:core' + +sublude ':firebase-auth:core' + +>>>>>>> 1b34bbd3 (Add dummy service for PANORAMA(3) (#2173)) include ':play-services-core:microg-ui-tools' // Legacy include ':play-services-core' From 1476713d51ba2dae3c301096c1d528505f58e454 Mon Sep 17 00:00:00 2001 From: DaVinci9196 <150454414+DaVinci9196@users.noreply.github.com> Date: Sat, 17 Feb 2024 04:43:35 +0800 Subject: [PATCH 046/209] fixed & app map layer display issues (#2161) --- .../gms/maps/hms/utils/typeConverter.kt | 229 ++++++++++++++++++ 1 file changed, 229 insertions(+) create mode 100644 play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/utils/typeConverter.kt diff --git a/play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/utils/typeConverter.kt b/play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/utils/typeConverter.kt new file mode 100644 index 00000000..d7c542b8 --- /dev/null +++ b/play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/utils/typeConverter.kt @@ -0,0 +1,229 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.maps.hms.utils + +import android.os.Bundle +import android.util.Log +import com.google.android.gms.dynamic.unwrap +import com.google.android.gms.maps.GoogleMapOptions +import com.google.android.gms.maps.internal.ICancelableCallback +import com.huawei.hms.maps.HuaweiMap +import com.huawei.hms.maps.HuaweiMapOptions +import com.huawei.hms.maps.model.* +import com.google.android.gms.maps.model.CameraPosition as GmsCameraPosition +import com.google.android.gms.maps.model.CircleOptions as GmsCircleOptions +import com.google.android.gms.maps.model.Dash as GmsDash +import com.google.android.gms.maps.model.Dot as GmsDot +import com.google.android.gms.maps.model.Gap as GmsGap +import com.google.android.gms.maps.model.GroundOverlayOptions as GmsGroundOverlayOptions +import com.google.android.gms.maps.model.LatLng as GmsLatLng +import com.google.android.gms.maps.model.LatLngBounds as GmsLatLngBounds +import com.google.android.gms.maps.model.MarkerOptions as GmsMarkerOptions +import com.google.android.gms.maps.model.PatternItem as GmsPatternItem +import com.google.android.gms.maps.model.PolygonOptions as GmsPolygonOptions +import com.google.android.gms.maps.model.PolylineOptions as GmsPolylineOptions +import com.google.android.gms.maps.model.Tile as GmsTile +import com.google.android.gms.maps.model.TileOverlayOptions as GmsTileOverlayOptions +import com.google.android.gms.maps.model.VisibleRegion as GmsVisibleRegion + +fun GmsCameraPosition.toHms(): CameraPosition { + return CameraPosition.Builder().target(target.toHms()).zoom(toHmsZoom(zoom)).tilt(tilt) + .bearing(toHmsBearing(bearing)).build() +} + +fun GmsCircleOptions.toHms(): CircleOptions = + CircleOptions().center(center.toHms()).clickable(isClickable).fillColor(fillColor) + .radius(radius).strokeColor(strokeColor).strokeWidth(strokeWidth).visible(isVisible) + .zIndex(zIndex) + +fun GmsPatternItem.toHms(): PatternItem { + return when (this) { + is GmsDash -> Dash(length) + is GmsDot -> Dot() + is GmsGap -> Gap(length) + else -> PatternItem(0,0f) + } +} + +fun GoogleMapOptions.toHms(): HuaweiMapOptions { + val huaweiMapOptions = HuaweiMapOptions() + camera?.let { huaweiMapOptions.camera(camera?.toHms()) } + if (maxZoomPreference != 0f) { + huaweiMapOptions.maxZoomPreference(toHmsZoom(maxZoomPreference)) + } + if (minZoomPreference != 0f) { + huaweiMapOptions.minZoomPreference(toHmsZoom(minZoomPreference)) + } + latLngBoundsForCameraTarget?.let { + huaweiMapOptions.latLngBoundsForCameraTarget( + latLngBoundsForCameraTarget?.toHms() + ) + } + + return huaweiMapOptions + .compassEnabled(isCompassEnabled) + .liteMode(liteMode) +// .mapType(mapType) + .rotateGesturesEnabled(rotateGesturesEnabled == true) + .scrollGesturesEnabled(scrollGesturesEnabled == true) + .tiltGesturesEnabled(tiltGesturesEnabled == true) + .useViewLifecycleInFragment(useViewLifecycleInFragment == true) + .zOrderOnTop(zOrderOnTop == true) + .zoomControlsEnabled(zoomControlsEnabled == true) + .zoomGesturesEnabled(zoomGesturesEnabled == true) +} + +fun GmsLatLng.toHms(): LatLng = + LatLng(latitude, longitude) + +fun GmsLatLngBounds.toHms(): LatLngBounds = + LatLngBounds( + LatLng(if(southwest.latitude.isNaN()) 0.0 else southwest.latitude, if(southwest.longitude.isNaN()) 0.0 else southwest.longitude), + LatLng(if(northeast.latitude.isNaN()) 0.0 else northeast.latitude, if(northeast.longitude.isNaN()) 0.0 else northeast.longitude) + ) + +fun ICancelableCallback.toHms(): HuaweiMap.CancelableCallback = + object : HuaweiMap.CancelableCallback { + override fun onFinish() = this@toHms.onFinish() + override fun onCancel() = this@toHms.onCancel() + } + +fun GmsMarkerOptions.toHms(): MarkerOptions { + val markerOptions = MarkerOptions() + icon?.let { markerOptions.icon(it.remoteObject.unwrap()) } + return markerOptions.alpha(alpha).anchorMarker(anchorU, anchorV).draggable(isDraggable) + .flat(isFlat).infoWindowAnchor(infoWindowAnchorU, infoWindowAnchorV) + .position(position.toHms()).rotation(rotation).snippet(snippet).title(title) + .visible(isVisible).zIndex(zIndex) +} + +fun GmsGroundOverlayOptions.toHms(): GroundOverlayOptions { + val groundOverlayOptions = GroundOverlayOptions() + groundOverlayOptions.anchor(anchorU, anchorV).bearing(bearing) + .clickable(isClickable) + .image(image.remoteObject.unwrap()) + .visible(isVisible) + .zIndex(zIndex) + location?.let { + if (height > 0) { + groundOverlayOptions.position(it.toHms(), width, height) + } else { + groundOverlayOptions.position(it.toHms(), width) + } + } + bounds?.let { groundOverlayOptions.positionFromBounds(it.toHms()) } + return groundOverlayOptions +} + +fun GmsTileOverlayOptions.toHms(): TileOverlayOptions { + return TileOverlayOptions().tileProvider(tileProvider?.let { TileProvider { x, y, zoom -> it.getTile(x, y, zoom)?.toHms() } }) + .fadeIn(fadeIn) + .visible(isVisible) + .transparency(transparency) + .zIndex(zIndex) +} + +fun GmsTile.toHms(): Tile = Tile(width, height, data) + +fun GmsPolygonOptions.toHms(): PolygonOptions { + val polygonOptions = PolygonOptions() + holes?.map { + val hole = it?.map { it?.toHms() } + polygonOptions.addHole(hole) + } + return polygonOptions.addAll(points.map { it.toHms() }) + .clickable(isClickable) + .fillColor(fillColor) + .geodesic(isGeodesic) + .strokeColor(strokeColor).strokeJointType(strokeJointType).strokeWidth(strokeWidth) + .visible(isVisible) + .zIndex(zIndex) +} + +fun GmsPolylineOptions.toHms(): PolylineOptions { + val polylineOptions = PolylineOptions() + polylineOptions.addAll(points.map { it.toHms() }) + return polylineOptions.clickable(isClickable).color(color).geodesic(isGeodesic) + .jointType(jointType).visible(isVisible).width(toHmsPolylineWidth(width)).zIndex(zIndex) +} + +fun toHmsPolylineWidth(gmsWidth: Float): Float = gmsWidth / 3 + +fun toHmsZoom(gmsZoom: Float?): Float { + if (gmsZoom == null) { + return 3f + } + if (gmsZoom < 3) { + return 3f + } else if (gmsZoom > 18) { + return 18f + } + return gmsZoom +} + +fun toHmsBearing(gmsBearing: Float): Float { + return 360 - gmsBearing +} + +fun Bundle.toHms(): Bundle { + val newBundle = Bundle(this) + val oldLoader = newBundle.classLoader + newBundle.classLoader = GmsLatLng::class.java.classLoader + for (key in newBundle.keySet()) { + when (val value = newBundle.get(key)) { + is GmsCameraPosition -> newBundle.putParcelable(key, value.toHms()) + is GmsLatLng -> newBundle.putParcelable(key, value.toHms()) + is GmsLatLngBounds -> newBundle.putParcelable(key, value.toHms()) + is Bundle -> newBundle.putBundle(key, value.toHms()) + } + } + newBundle.classLoader = oldLoader + return newBundle +} + +fun CameraPosition.toGms(): GmsCameraPosition = + GmsCameraPosition(target.toGms(), zoom, tilt, bearing) + +fun PatternItem.toGms(): GmsPatternItem = when (this) { + is Dot -> GmsDot() + is Dash -> GmsDash(length) + is Gap -> GmsGap(length) + else -> GmsGap(0f) +} + +fun LatLng.toGms(): GmsLatLng = GmsLatLng(latitude, longitude) + +fun LatLngBounds.toGms(): GmsLatLngBounds = GmsLatLngBounds( + GmsLatLng(southwest.latitude, southwest.longitude), + GmsLatLng(northeast.latitude, northeast.longitude) +) + +fun VisibleRegion.toGms(): GmsVisibleRegion = + GmsVisibleRegion( + nearLeft.toGms(), + nearRight.toGms(), + farLeft.toGms(), + farRight.toGms(), + latLngBounds.toGms() + ) + +fun toGmsPolylineWidth(hmsWidth: Float): Float = hmsWidth * 3 + +fun Bundle.toGms(): Bundle { + val newBundle = Bundle(this) + val oldLoader = newBundle.classLoader + newBundle.classLoader = LatLng::class.java.classLoader + for (key in newBundle.keySet()) { + when (val value = newBundle.get(key)) { + is CameraPosition -> newBundle.putParcelable(key, value.toGms()) + is LatLng -> newBundle.putParcelable(key, value.toGms()) + is LatLngBounds -> newBundle.putParcelable(key, value.toGms()) + is Bundle -> newBundle.putBundle(key, value.toGms()) + } + } + newBundle.classLoader = oldLoader + return newBundle +} From e288ba255452609efa0e606a10c1df491e7db0ef Mon Sep 17 00:00:00 2001 From: DaVinci9196 <150454414+DaVinci9196@users.noreply.github.com> Date: Sat, 17 Feb 2024 22:23:56 +0800 Subject: [PATCH 047/209] Add dummy service for SEMANTIC_LOCATION(173) (#2174) Co-authored-by: Marvin W --- .../SemanticLocationEventRequest.aidl | 7 ++ .../internal/ISemanticLocationService.aidl | 18 +++ .../internal/SemanticLocationParameters.aidl | 7 ++ .../SemanticLocationEventRequest.java | 32 +++++ .../internal/SemanticLocationParameters.java | 38 ++++++ .../org/microg/gms/common/GmsService.java | 63 ++++++++++ .../src/main/AndroidManifest.xml | 113 +++++++++++++++++- .../SemanticLocationService.kt | 56 +++++++++ 8 files changed, 333 insertions(+), 1 deletion(-) create mode 100644 play-services-api/src/main/aidl/com/google/android/gms/semanticlocation/SemanticLocationEventRequest.aidl create mode 100644 play-services-api/src/main/aidl/com/google/android/gms/semanticlocation/internal/ISemanticLocationService.aidl create mode 100644 play-services-api/src/main/aidl/com/google/android/gms/semanticlocation/internal/SemanticLocationParameters.aidl create mode 100644 play-services-api/src/main/java/com/google/android/gms/semanticlocation/SemanticLocationEventRequest.java create mode 100644 play-services-api/src/main/java/com/google/android/gms/semanticlocation/internal/SemanticLocationParameters.java create mode 100644 play-services-core/src/main/java/com/google/android/gms/semanticlocation/SemanticLocationService.kt diff --git a/play-services-api/src/main/aidl/com/google/android/gms/semanticlocation/SemanticLocationEventRequest.aidl b/play-services-api/src/main/aidl/com/google/android/gms/semanticlocation/SemanticLocationEventRequest.aidl new file mode 100644 index 00000000..715a75c0 --- /dev/null +++ b/play-services-api/src/main/aidl/com/google/android/gms/semanticlocation/SemanticLocationEventRequest.aidl @@ -0,0 +1,7 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ +package com.google.android.gms.semanticlocation; + +parcelable SemanticLocationEventRequest; diff --git a/play-services-api/src/main/aidl/com/google/android/gms/semanticlocation/internal/ISemanticLocationService.aidl b/play-services-api/src/main/aidl/com/google/android/gms/semanticlocation/internal/ISemanticLocationService.aidl new file mode 100644 index 00000000..de75deff --- /dev/null +++ b/play-services-api/src/main/aidl/com/google/android/gms/semanticlocation/internal/ISemanticLocationService.aidl @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ +package com.google.android.gms.semanticlocation.internal; + +import android.app.PendingIntent; +import android.os.IInterface; +import com.google.android.gms.semanticlocation.SemanticLocationEventRequest; +import com.google.android.gms.semanticlocation.internal.SemanticLocationParameters; +import com.google.android.gms.common.api.internal.IStatusCallback; + +interface ISemanticLocationService { + void registerSemanticLocationEvents(in SemanticLocationParameters params, IStatusCallback callback, in SemanticLocationEventRequest request, in PendingIntent pendingIntent) = 0; + void unregisterSemanticLocationEvents(in SemanticLocationParameters params, IStatusCallback callback, in PendingIntent pendingIntent) = 1; + + void setIncognitoMode(in SemanticLocationParameters params, IStatusCallback callback, boolean mode) = 4; +} \ No newline at end of file diff --git a/play-services-api/src/main/aidl/com/google/android/gms/semanticlocation/internal/SemanticLocationParameters.aidl b/play-services-api/src/main/aidl/com/google/android/gms/semanticlocation/internal/SemanticLocationParameters.aidl new file mode 100644 index 00000000..1ac959eb --- /dev/null +++ b/play-services-api/src/main/aidl/com/google/android/gms/semanticlocation/internal/SemanticLocationParameters.aidl @@ -0,0 +1,7 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ +package com.google.android.gms.semanticlocation.internal; + +parcelable SemanticLocationParameters; diff --git a/play-services-api/src/main/java/com/google/android/gms/semanticlocation/SemanticLocationEventRequest.java b/play-services-api/src/main/java/com/google/android/gms/semanticlocation/SemanticLocationEventRequest.java new file mode 100644 index 00000000..65ee1379 --- /dev/null +++ b/play-services-api/src/main/java/com/google/android/gms/semanticlocation/SemanticLocationEventRequest.java @@ -0,0 +1,32 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ +package com.google.android.gms.semanticlocation; + + +import android.os.Parcel; + +import androidx.annotation.NonNull; + +import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; + +@SafeParcelable.Class +public class SemanticLocationEventRequest extends AbstractSafeParcelable { + @Field(1) + public final float position; + + @Constructor + public SemanticLocationEventRequest(@Param(1) float position) { + this.position = position; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } + + public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(SemanticLocationEventRequest.class); +} diff --git a/play-services-api/src/main/java/com/google/android/gms/semanticlocation/internal/SemanticLocationParameters.java b/play-services-api/src/main/java/com/google/android/gms/semanticlocation/internal/SemanticLocationParameters.java new file mode 100644 index 00000000..3dc753eb --- /dev/null +++ b/play-services-api/src/main/java/com/google/android/gms/semanticlocation/internal/SemanticLocationParameters.java @@ -0,0 +1,38 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ +package com.google.android.gms.semanticlocation.internal; + +import android.accounts.Account; +import android.os.Parcel; + +import androidx.annotation.NonNull; + +import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; + +@SafeParcelable.Class +public class SemanticLocationParameters extends AbstractSafeParcelable { + @Field(1) + public final Account account; + @Field(2) + public final String clientIdentifier; + @Field(3) + public final String packageName; + + @Constructor + public SemanticLocationParameters(@Param(1) Account account, @Param(2) String clientIdentifier, @Param(3) String packageName) { + this.account = account; + this.clientIdentifier = clientIdentifier; + this.packageName = packageName; + } + + public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(SemanticLocationParameters.class); + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } +} diff --git a/play-services-basement/src/main/java/org/microg/gms/common/GmsService.java b/play-services-basement/src/main/java/org/microg/gms/common/GmsService.java index 739bc259..2802e9eb 100644 --- a/play-services-basement/src/main/java/org/microg/gms/common/GmsService.java +++ b/play-services-basement/src/main/java/org/microg/gms/common/GmsService.java @@ -27,6 +27,69 @@ public enum GmsService { COMMON(39, "com.google.android.gms.common.service.START"), SIGN_IN(44, "com.google.android.gms.signin.service.START"), CREDENTIALS(68, "com.google.android.gms.auth.api.credentials.service.START"), +<<<<<<< HEAD +======= + NEARBY_BOOTSTRAP(69, "com.google.android.gms.nearby.bootstrap.service.NearbyBootstrapService.START"), + PLUS_INTERNAL(70), + SOURCE_DEVICE(75, "com.google.android.gms.smartdevice.d2d.SourceDeviceService.START"), + TARGET_DEVICE(76, "com.google.android.gms.smartdevice.d2d.TargetDeviceService.START"), + APP_INVITE(77, "com.google.android.gms.appinvite.service.START"), + TAP_AND_PAY(79, "com.google.android.gms.tapandpay.service.BIND"), + CHROME_SYNC(80, "com.google.android.gms.chromesync.service.START"), + ACCOUNTS(81, "com.google.android.gms.smartdevice.setup.accounts.AccountsService.START"), + CAST_REMOTE_DISPLAY(83, "com.google.android.gms.cast.remote_display.service.START"), + TRUST_AGENT(85, "com.google.android.gms.trustagent.StateApi.START"), + AUTH_SIGN_IN(91, "com.google.android.gms.auth.api.signin.service.START"), + MEASUREMENT(93, "com.google.android.gms.measurement.START"), + FREIGHTER(98, "com.google.android.gms.freighter.service.START"), + GUNS(110, "com.google.android.gms.notifications.service.START"), + BLE(111, "com.google.android.gms.beacon.internal.IBleService.START"), + FIREBASE_AUTH(112, "com.google.firebase.auth.api.gms.service.START"), + APP_INDEXING(113), + GASS(116, "com.google.android.gms.gass.START"), + WORK_ACCOUNT(120), + INSTANT_APPS(121, "com.google.android.gms.instantapps.START"), + CAST_FIRSTPATY(122, "com.google.android.gms.cast.firstparty.START"), + AD_CACHE(123, "com.google.android.gms.ads.service.CACHE"), + SMS_RETRIEVER(126, "com.google.android.gms.auth.api.phone.service.SmsRetrieverApiService.START"), + CRYPT_AUTH(129, "com.google.android.gms.auth.cryptauth.cryptauthservice.START"), + DYNAMIC_LINKS(131, "com.google.firebase.dynamiclinks.service.START"), + FONTS(132, "com.google.android.gms.fonts.service.START"), + ROMANESCO(135, "com.google.android.gms.romanesco.service.START"), + TRAINER(139, "com.google.android.gms.learning.trainer.START"), + FIDO2_REGULAR(148, "com.google.android.gms.fido.fido2.regular.START"), + FIDO2_PRIVILEGED(149, "com.google.android.gms.fido.fido2.privileged.START"), + DATA_DOWNLOAD(152, "com.google.android.mdd.service.START"), + ACCOUNT_DATA(153, "com.google.android.gms.auth.account.data.service.START"), + CONSTELLATION(155, "com.google.android.gms.constellation.service.START"), + AUDIT(154, "com.google.android.gms.audit.service.START"), + SYSTEM_UPDATE(157, "com.google.android.gms.update.START_API_SERVICE"), + MOBSTORE(160, "com.google.android.mobstore.service.START"), + USER_LOCATION(163, "com.google.android.gms.userlocation.service.START"), + AD_HTTP(166, "com.google.android.gms.ads.service.HTTP"), + LANGUAGE_PROFILE(167, "com.google.android.gms.languageprofile.service.START"), + MDNS(168, "com.google.android.gms.mdns.service.START"), + SEMANTIC_LOCATION(173, "com.google.android.gms.semanticlocation.service.START_ODLH"), + FIDO2_ZEROPARTY(180, "com.google.android.gms.fido.fido2.zeroparty.START"), + G1_RESTORE(181, "com.google.android.gms.backup.G1_RESTORE"), + G1_BACKUP(182, "com.google.android.gms.backup.G1_BACKUP"), + OSS_LICENSES(185, "com.google.android.gms.oss.licenses.service.START"), + PAYSE(188, "com.google.android.gms.payse.service.BIND"), + RCS(189, "com.google.android.gms.rcs.START"), + CARRIER_AUTH(191, "com.google.android.gms.carrierauth.service.START"), + SYSTEM_UPDATE_SINGLE_UESR(192, "com.google.android.gms.update.START_SINGLE_USER_API_SERVICE"), + APP_USAGE(193, "com.google.android.gms.appusage.service.START"), + NEARBY_SHARING_2(194, "com.google.android.gms.nearby.sharing.START_SERVICE"), + AD_CONSENT_LOOKUP(195, "com.google.android.gms.ads.service.CONSENT_LOOKUP"), + CREDENTIAL_MANAGER(196, "com.google.android.gms.credential.manager.service.firstparty.START"), + PHONE_INTERNAL(197, "com.google.android.gms.auth.api.phone.service.InternalService.START"), + PAY(198, "com.google.android.gms.pay.service.BIND"), + ASTERISM(199, "com.google.android.gms.asterism.service.START"), + MODULE_RESTORE(201, "com.google.android.gms.backup.GMS_MODULE_RESTORE"), + FACS_CACHE(202, "com.google.android.gms.facs.cache.service.START"), + RECAPTCHA(205, "com.google.android.gms.recaptcha.service.START"), + CONTACT_SYNC(208, "com.google.android.gms.people.contactssync.service.START"), +>>>>>>> 37dd2b59 (Add dummy service for SEMANTIC_LOCATION(173) (#2174)) IDENTITY_SIGN_IN(212, "com.google.android.gms.auth.api.identity.service.signin.START"), ; diff --git a/play-services-core/src/main/AndroidManifest.xml b/play-services-core/src/main/AndroidManifest.xml index 80ceba01..47282490 100644 --- a/play-services-core/src/main/AndroidManifest.xml +++ b/play-services-core/src/main/AndroidManifest.xml @@ -528,6 +528,118 @@ +<<<<<<< HEAD +======= + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +>>>>>>> 37dd2b59 (Add dummy service for SEMANTIC_LOCATION(173) (#2174)) <<<<<<< HEAD ======= @@ -639,7 +751,6 @@ - diff --git a/play-services-core/src/main/java/com/google/android/gms/semanticlocation/SemanticLocationService.kt b/play-services-core/src/main/java/com/google/android/gms/semanticlocation/SemanticLocationService.kt new file mode 100644 index 00000000..70b36538 --- /dev/null +++ b/play-services-core/src/main/java/com/google/android/gms/semanticlocation/SemanticLocationService.kt @@ -0,0 +1,56 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ +package com.google.android.gms.semanticlocation + +import android.app.PendingIntent +import android.util.Log +import com.google.android.gms.common.ConnectionResult +import com.google.android.gms.common.Feature +import com.google.android.gms.common.api.internal.IStatusCallback +import com.google.android.gms.common.internal.ConnectionInfo +import com.google.android.gms.common.internal.GetServiceRequest +import com.google.android.gms.common.internal.IGmsCallbacks +import com.google.android.gms.semanticlocation.internal.ISemanticLocationService +import com.google.android.gms.semanticlocation.internal.SemanticLocationParameters +import org.microg.gms.BaseService +import org.microg.gms.common.GmsService + +private const val TAG = "SemanticLocationService" + +private val FEATURES = arrayOf( + Feature("semanticlocation_events", 1L), +) + +class SemanticLocationService : BaseService(TAG, GmsService.SEMANTIC_LOCATION) { + override fun handleServiceRequest(callback: IGmsCallbacks, request: GetServiceRequest, service: GmsService) { + val connectionInfo = ConnectionInfo().apply { + features = FEATURES + } + callback.onPostInitCompleteWithConnectionInfo( + ConnectionResult.SUCCESS, + SemanticLocationServiceImpl().asBinder(), + connectionInfo + ) + } +} + +class SemanticLocationServiceImpl : ISemanticLocationService.Stub() { + override fun registerSemanticLocationEvents( + params: SemanticLocationParameters, + callback: IStatusCallback, + request: SemanticLocationEventRequest, + pendingIntent: PendingIntent + ) { + Log.d(TAG, "registerSemanticLocationEvents: $params") + } + + override fun setIncognitoMode(params: SemanticLocationParameters, callback: IStatusCallback, mode: Boolean) { + Log.d(TAG, "setIncognitoMode: $params") + } + + override fun unregisterSemanticLocationEvents(params: SemanticLocationParameters, callback: IStatusCallback, pendingIntent: PendingIntent) { + Log.d(TAG, "unregisterSemanticLocationEvents: $params") + } +} From d3892ceeb245bf87686051681aec7ed37afcb395 Mon Sep 17 00:00:00 2001 From: DaVinci9196 <150454414+DaVinci9196@users.noreply.github.com> Date: Sun, 18 Feb 2024 03:21:54 +0800 Subject: [PATCH 048/209] Add dummy services for PSEUDONYMOUS_ID(52), GASS(116), ACCOUNT_DATA(153) and APP_SET(300) (#2175) Co-authored-by: Marvin W --- .../gms/gass/internal/GassRequestParcel.aidl | 8 ++ .../gms/gass/internal/GassResponseParcel.aidl | 8 ++ .../gms/gass/internal/IGassService.aidl | 16 ++++ .../gms/pseudonymous/PseudonymousIdToken.aidl | 8 ++ .../internal/IPseudonymousIdCallbacks.aidl | 15 ++++ .../internal/IPseudonymousIdService.aidl | 15 ++++ .../gms/gass/internal/GassRequestParcel.java | 32 ++++++++ .../gms/gass/internal/GassResponseParcel.java | 49 ++++++++++++ .../gms/pseudonymous/PseudonymousIdToken.java | 34 +++++++++ play-services-appset/build.gradle | 37 +++++++++ play-services-appset/core/build.gradle | 56 ++++++++++++++ .../core/src/main/AndroidManifest.xml | 16 ++++ .../org/microg/gms/appset/AppSetService.kt | 40 ++++++++++ .../src/main/AndroidManifest.xml | 4 + .../gms/appset/AppSetIdRequestParams.aidl | 8 ++ .../android/gms/appset/AppSetInfoParcel.aidl | 8 ++ .../appset/internal/IAppSetIdCallback.aidl | 13 ++++ .../gms/appset/internal/IAppSetService.aidl | 13 ++++ .../com/google/android/gms/appset/AppSet.java | 25 +++++++ .../android/gms/appset/AppSetIdClient.java | 25 +++++++ .../android/gms/appset/AppSetIdInfo.java | 75 +++++++++++++++++++ .../gms/appset/AppSetIdRequestParams.java | 45 +++++++++++ .../android/gms/appset/AppSetInfoParcel.java | 38 ++++++++++ .../android/gms/appset/package-info.java | 12 +++ .../DeviceManagementInfoResponse.aidl | 8 ++ .../DeviceManagementInfoResponse.java | 44 +++++++++++ .../account/data/IAccountDataService.aidl | 16 ++++ .../data/IDeviceManagementInfoCallback.aidl | 13 ++++ .../org/microg/gms/common/GmsService.java | 30 ++++++++ play-services-core/build.gradle | 1 + .../src/main/AndroidManifest.xml | 21 +++++- .../auth/account/data/AccountDataService.kt | 52 +++++++++++++ .../kotlin/org/microg/gms/gass/GassService.kt | 39 ++++++++++ .../gms/pseudonymous/PseudonymousIdService.kt | 52 +++++++++++++ settings.gradle | 36 +++++++++ 35 files changed, 909 insertions(+), 3 deletions(-) create mode 100644 play-services-api/src/main/aidl/com/google/android/gms/gass/internal/GassRequestParcel.aidl create mode 100644 play-services-api/src/main/aidl/com/google/android/gms/gass/internal/GassResponseParcel.aidl create mode 100644 play-services-api/src/main/aidl/com/google/android/gms/gass/internal/IGassService.aidl create mode 100644 play-services-api/src/main/aidl/com/google/android/gms/pseudonymous/PseudonymousIdToken.aidl create mode 100644 play-services-api/src/main/aidl/com/google/android/gms/pseudonymous/internal/IPseudonymousIdCallbacks.aidl create mode 100644 play-services-api/src/main/aidl/com/google/android/gms/pseudonymous/internal/IPseudonymousIdService.aidl create mode 100644 play-services-api/src/main/java/com/google/android/gms/gass/internal/GassRequestParcel.java create mode 100644 play-services-api/src/main/java/com/google/android/gms/gass/internal/GassResponseParcel.java create mode 100644 play-services-api/src/main/java/com/google/android/gms/pseudonymous/PseudonymousIdToken.java create mode 100644 play-services-appset/build.gradle create mode 100644 play-services-appset/core/build.gradle create mode 100644 play-services-appset/core/src/main/AndroidManifest.xml create mode 100644 play-services-appset/core/src/main/kotlin/org/microg/gms/appset/AppSetService.kt create mode 100644 play-services-appset/src/main/AndroidManifest.xml create mode 100644 play-services-appset/src/main/aidl/com/google/android/gms/appset/AppSetIdRequestParams.aidl create mode 100644 play-services-appset/src/main/aidl/com/google/android/gms/appset/AppSetInfoParcel.aidl create mode 100644 play-services-appset/src/main/aidl/com/google/android/gms/appset/internal/IAppSetIdCallback.aidl create mode 100644 play-services-appset/src/main/aidl/com/google/android/gms/appset/internal/IAppSetService.aidl create mode 100644 play-services-appset/src/main/java/com/google/android/gms/appset/AppSet.java create mode 100644 play-services-appset/src/main/java/com/google/android/gms/appset/AppSetIdClient.java create mode 100644 play-services-appset/src/main/java/com/google/android/gms/appset/AppSetIdInfo.java create mode 100644 play-services-appset/src/main/java/com/google/android/gms/appset/AppSetIdRequestParams.java create mode 100644 play-services-appset/src/main/java/com/google/android/gms/appset/AppSetInfoParcel.java create mode 100644 play-services-appset/src/main/java/com/google/android/gms/appset/package-info.java create mode 100644 play-services-auth-base/src/main/aidl/com/google/android/gms/auth/firstparty/dataservice/DeviceManagementInfoResponse.aidl create mode 100644 play-services-auth-base/src/main/java/com/google/android/gms/auth/firstparty/dataservice/DeviceManagementInfoResponse.java create mode 100644 play-services-auth/src/main/aidl/com/google/android/gms/auth/account/data/IAccountDataService.aidl create mode 100644 play-services-auth/src/main/aidl/com/google/android/gms/auth/account/data/IDeviceManagementInfoCallback.aidl create mode 100644 play-services-core/src/main/kotlin/org/microg/gms/auth/account/data/AccountDataService.kt create mode 100644 play-services-core/src/main/kotlin/org/microg/gms/gass/GassService.kt create mode 100644 play-services-core/src/main/kotlin/org/microg/gms/pseudonymous/PseudonymousIdService.kt diff --git a/play-services-api/src/main/aidl/com/google/android/gms/gass/internal/GassRequestParcel.aidl b/play-services-api/src/main/aidl/com/google/android/gms/gass/internal/GassRequestParcel.aidl new file mode 100644 index 00000000..ffe2ae73 --- /dev/null +++ b/play-services-api/src/main/aidl/com/google/android/gms/gass/internal/GassRequestParcel.aidl @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.gass.internal; + +parcelable GassRequestParcel; diff --git a/play-services-api/src/main/aidl/com/google/android/gms/gass/internal/GassResponseParcel.aidl b/play-services-api/src/main/aidl/com/google/android/gms/gass/internal/GassResponseParcel.aidl new file mode 100644 index 00000000..f8f40a73 --- /dev/null +++ b/play-services-api/src/main/aidl/com/google/android/gms/gass/internal/GassResponseParcel.aidl @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.gass.internal; + +parcelable GassResponseParcel; diff --git a/play-services-api/src/main/aidl/com/google/android/gms/gass/internal/IGassService.aidl b/play-services-api/src/main/aidl/com/google/android/gms/gass/internal/IGassService.aidl new file mode 100644 index 00000000..7fafe56a --- /dev/null +++ b/play-services-api/src/main/aidl/com/google/android/gms/gass/internal/IGassService.aidl @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.gass.internal; + +import android.os.Bundle; +import android.os.IInterface; +import com.google.android.gms.gass.internal.GassRequestParcel; +import com.google.android.gms.gass.internal.GassResponseParcel; + +interface IGassService { + GassResponseParcel getGassResponse(in GassRequestParcel gassRequestParcel) = 0; + Bundle getGassBundle(in Bundle bundle, int code) = 3; +} diff --git a/play-services-api/src/main/aidl/com/google/android/gms/pseudonymous/PseudonymousIdToken.aidl b/play-services-api/src/main/aidl/com/google/android/gms/pseudonymous/PseudonymousIdToken.aidl new file mode 100644 index 00000000..b8d88c14 --- /dev/null +++ b/play-services-api/src/main/aidl/com/google/android/gms/pseudonymous/PseudonymousIdToken.aidl @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.pseudonymous; + +parcelable PseudonymousIdToken; diff --git a/play-services-api/src/main/aidl/com/google/android/gms/pseudonymous/internal/IPseudonymousIdCallbacks.aidl b/play-services-api/src/main/aidl/com/google/android/gms/pseudonymous/internal/IPseudonymousIdCallbacks.aidl new file mode 100644 index 00000000..f6a47ded --- /dev/null +++ b/play-services-api/src/main/aidl/com/google/android/gms/pseudonymous/internal/IPseudonymousIdCallbacks.aidl @@ -0,0 +1,15 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.pseudonymous.internal; + +import com.google.android.gms.common.api.Status; +import com.google.android.gms.pseudonymous.PseudonymousIdToken; + +interface IPseudonymousIdCallbacks { + void onGetTokenResponse(in Status status, in PseudonymousIdToken token) = 0; + void onSetTokenResponse(in Status status) = 1; + void onGetLastResetWallTimeMsResponse(in Status status, long time) = 2; +} diff --git a/play-services-api/src/main/aidl/com/google/android/gms/pseudonymous/internal/IPseudonymousIdService.aidl b/play-services-api/src/main/aidl/com/google/android/gms/pseudonymous/internal/IPseudonymousIdService.aidl new file mode 100644 index 00000000..e99f7df6 --- /dev/null +++ b/play-services-api/src/main/aidl/com/google/android/gms/pseudonymous/internal/IPseudonymousIdService.aidl @@ -0,0 +1,15 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.pseudonymous.internal; + +import com.google.android.gms.pseudonymous.internal.IPseudonymousIdCallbacks; +import com.google.android.gms.pseudonymous.PseudonymousIdToken; + +interface IPseudonymousIdService { + void getToken(IPseudonymousIdCallbacks call) = 0; + void setToken(IPseudonymousIdCallbacks call, in PseudonymousIdToken token) = 1; + void getLastResetWallTimeMs(IPseudonymousIdCallbacks callbacks) = 2; +} diff --git a/play-services-api/src/main/java/com/google/android/gms/gass/internal/GassRequestParcel.java b/play-services-api/src/main/java/com/google/android/gms/gass/internal/GassRequestParcel.java new file mode 100644 index 00000000..66669683 --- /dev/null +++ b/play-services-api/src/main/java/com/google/android/gms/gass/internal/GassRequestParcel.java @@ -0,0 +1,32 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.gass.internal; + +import android.os.Parcel; + +import androidx.annotation.NonNull; + +import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; + +@SafeParcelable.Class +public class GassRequestParcel extends AbstractSafeParcelable { + + @Field(1) + public int versionCode; + @Field(2) + public String packageName; + @Field(3) + public String appVersionCode; + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } + + public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(GassRequestParcel.class); +} diff --git a/play-services-api/src/main/java/com/google/android/gms/gass/internal/GassResponseParcel.java b/play-services-api/src/main/java/com/google/android/gms/gass/internal/GassResponseParcel.java new file mode 100644 index 00000000..34e2c617 --- /dev/null +++ b/play-services-api/src/main/java/com/google/android/gms/gass/internal/GassResponseParcel.java @@ -0,0 +1,49 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.gass.internal; + +import android.os.Parcel; + +import androidx.annotation.NonNull; + +import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; +import com.google.android.gms.feedback.ErrorReport; + +@SafeParcelable.Class +public class GassResponseParcel extends AbstractSafeParcelable { + + @Field(1) + public int versionCode; + + @Field(2) + public byte[] data; + + public ErrorReport report; + + public GassResponseParcel() { + } + + public GassResponseParcel(int i, byte[] bArr) { + this.versionCode = i; + this.report = null; + this.data = bArr; + } + + public GassResponseParcel(ErrorReport report) { + this.versionCode = 1; + this.report = report; + this.data = null; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } + + public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(GassResponseParcel.class); +} diff --git a/play-services-api/src/main/java/com/google/android/gms/pseudonymous/PseudonymousIdToken.java b/play-services-api/src/main/java/com/google/android/gms/pseudonymous/PseudonymousIdToken.java new file mode 100644 index 00000000..18df15cb --- /dev/null +++ b/play-services-api/src/main/java/com/google/android/gms/pseudonymous/PseudonymousIdToken.java @@ -0,0 +1,34 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.pseudonymous; + +import android.os.Parcel; + +import androidx.annotation.NonNull; + +import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; + +@SafeParcelable.Class +public class PseudonymousIdToken extends AbstractSafeParcelable { + + @Field(2) + public String name; + + @Constructor + public PseudonymousIdToken(@Param(2) String name) { + this.name = name; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } + + public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(PseudonymousIdToken.class); + +} diff --git a/play-services-appset/build.gradle b/play-services-appset/build.gradle new file mode 100644 index 00000000..cb838e7e --- /dev/null +++ b/play-services-appset/build.gradle @@ -0,0 +1,37 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +apply plugin: 'com.android.library' + +android { + namespace "com.google.android.gms.appset" + + compileSdkVersion androidCompileSdk + buildToolsVersion "$androidBuildVersionTools" + + buildFeatures { + aidl = true + } + + defaultConfig { + versionName version + minSdkVersion androidMinSdk + targetSdkVersion androidTargetSdk + } + + compileOptions { + sourceCompatibility = 1.8 + targetCompatibility = 1.8 + } +} + +description = 'microG implementation of play-services-appset' + +dependencies { + api project(':play-services-base') + api project(':play-services-basement') + + annotationProcessor project(':safe-parcel-processor') +} diff --git a/play-services-appset/core/build.gradle b/play-services-appset/core/build.gradle new file mode 100644 index 00000000..c6017e1a --- /dev/null +++ b/play-services-appset/core/build.gradle @@ -0,0 +1,56 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'maven-publish' +apply plugin: 'signing' + +dependencies { + api project(':play-services-appset') + + implementation project(':play-services-base-core') + + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutineVersion" +} + +android { + namespace "org.microg.gms.appset.core" + + compileSdkVersion androidCompileSdk + buildToolsVersion "$androidBuildVersionTools" + + defaultConfig { + versionName version + minSdkVersion androidMinSdk + targetSdkVersion androidTargetSdk + } + + sourceSets { + main { + java.srcDirs = ['src/main/kotlin'] + } + } + + lintOptions { + disable 'MissingTranslation' + } + + compileOptions { + sourceCompatibility = 1.8 + targetCompatibility = 1.8 + } + + kotlinOptions { + jvmTarget = 1.8 + } +} + +// Nothing to publish yet +//apply from: '../gradle/publish-android.gradle' + +description = 'microG service implementation for play-services-appset' diff --git a/play-services-appset/core/src/main/AndroidManifest.xml b/play-services-appset/core/src/main/AndroidManifest.xml new file mode 100644 index 00000000..0df43222 --- /dev/null +++ b/play-services-appset/core/src/main/AndroidManifest.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + diff --git a/play-services-appset/core/src/main/kotlin/org/microg/gms/appset/AppSetService.kt b/play-services-appset/core/src/main/kotlin/org/microg/gms/appset/AppSetService.kt new file mode 100644 index 00000000..a024f498 --- /dev/null +++ b/play-services-appset/core/src/main/kotlin/org/microg/gms/appset/AppSetService.kt @@ -0,0 +1,40 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.appset + +import android.util.Log +import com.google.android.gms.appset.AppSetIdRequestParams +import com.google.android.gms.appset.internal.IAppSetIdCallback +import com.google.android.gms.appset.internal.IAppSetService +import com.google.android.gms.common.ConnectionResult +import com.google.android.gms.common.Feature +import com.google.android.gms.common.api.Status +import com.google.android.gms.common.internal.ConnectionInfo +import com.google.android.gms.common.internal.GetServiceRequest +import com.google.android.gms.common.internal.IGmsCallbacks +import org.microg.gms.BaseService +import org.microg.gms.common.GmsService + +private const val TAG = "AppSetService" +private val FEATURES = arrayOf(Feature("app_set_id", 1L)) + +class AppSetService : BaseService(TAG, GmsService.APP_SET) { + + override fun handleServiceRequest(callback: IGmsCallbacks?, request: GetServiceRequest?, service: GmsService?) { + callback?.onPostInitCompleteWithConnectionInfo( + ConnectionResult.SUCCESS, + AppSetServiceImpl().asBinder(), + ConnectionInfo().apply { features = FEATURES } + ) + } +} + +class AppSetServiceImpl : IAppSetService.Stub() { + override fun getAppSetIdInfo(params: AppSetIdRequestParams?, callback: IAppSetIdCallback?) { + Log.d(TAG, "AppSetServiceImp getAppSetIdInfo is called -> ${params?.toString()} ") + callback?.onAppSetInfo(Status.SUCCESS, null) + } +} diff --git a/play-services-appset/src/main/AndroidManifest.xml b/play-services-appset/src/main/AndroidManifest.xml new file mode 100644 index 00000000..8bdb7e14 --- /dev/null +++ b/play-services-appset/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/play-services-appset/src/main/aidl/com/google/android/gms/appset/AppSetIdRequestParams.aidl b/play-services-appset/src/main/aidl/com/google/android/gms/appset/AppSetIdRequestParams.aidl new file mode 100644 index 00000000..5beef708 --- /dev/null +++ b/play-services-appset/src/main/aidl/com/google/android/gms/appset/AppSetIdRequestParams.aidl @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.appset; + +parcelable AppSetIdRequestParams; diff --git a/play-services-appset/src/main/aidl/com/google/android/gms/appset/AppSetInfoParcel.aidl b/play-services-appset/src/main/aidl/com/google/android/gms/appset/AppSetInfoParcel.aidl new file mode 100644 index 00000000..c2d4818a --- /dev/null +++ b/play-services-appset/src/main/aidl/com/google/android/gms/appset/AppSetInfoParcel.aidl @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.appset; + +parcelable AppSetInfoParcel; diff --git a/play-services-appset/src/main/aidl/com/google/android/gms/appset/internal/IAppSetIdCallback.aidl b/play-services-appset/src/main/aidl/com/google/android/gms/appset/internal/IAppSetIdCallback.aidl new file mode 100644 index 00000000..1b453de3 --- /dev/null +++ b/play-services-appset/src/main/aidl/com/google/android/gms/appset/internal/IAppSetIdCallback.aidl @@ -0,0 +1,13 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.appset.internal; + +import com.google.android.gms.appset.AppSetInfoParcel; +import com.google.android.gms.common.api.Status; + +interface IAppSetIdCallback { + void onAppSetInfo(in Status status, in AppSetInfoParcel info) = 0; +} \ No newline at end of file diff --git a/play-services-appset/src/main/aidl/com/google/android/gms/appset/internal/IAppSetService.aidl b/play-services-appset/src/main/aidl/com/google/android/gms/appset/internal/IAppSetService.aidl new file mode 100644 index 00000000..59dbb6f9 --- /dev/null +++ b/play-services-appset/src/main/aidl/com/google/android/gms/appset/internal/IAppSetService.aidl @@ -0,0 +1,13 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.appset.internal; + +import com.google.android.gms.appset.AppSetIdRequestParams; +import com.google.android.gms.appset.internal.IAppSetIdCallback; + +interface IAppSetService { + void getAppSetIdInfo(in AppSetIdRequestParams params, in IAppSetIdCallback callback) = 0; +} diff --git a/play-services-appset/src/main/java/com/google/android/gms/appset/AppSet.java b/play-services-appset/src/main/java/com/google/android/gms/appset/AppSet.java new file mode 100644 index 00000000..f3616527 --- /dev/null +++ b/play-services-appset/src/main/java/com/google/android/gms/appset/AppSet.java @@ -0,0 +1,25 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. + */ + +package com.google.android.gms.appset; + +import android.content.Context; +import androidx.annotation.NonNull; + +/** + * Entry point of the app set APIs. + */ +public class AppSet { + /** + * Creates a new instance of {@link AppSetIdClient}. + */ + @NonNull + public static AppSetIdClient getClient (Context context) { + throw new UnsupportedOperationException(); + } +} diff --git a/play-services-appset/src/main/java/com/google/android/gms/appset/AppSetIdClient.java b/play-services-appset/src/main/java/com/google/android/gms/appset/AppSetIdClient.java new file mode 100644 index 00000000..91b6a671 --- /dev/null +++ b/play-services-appset/src/main/java/com/google/android/gms/appset/AppSetIdClient.java @@ -0,0 +1,25 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. + */ + +package com.google.android.gms.appset; + +import androidx.annotation.NonNull; +import com.google.android.gms.tasks.Task; + +/** + * A client for interacting with the {@link AppSetIdInfo} API. + */ +public interface AppSetIdClient { + /** + * Gets the AppSetIdInfo asynchronously. + * + * @return a {@link Task} of the returned {@link AppSetIdInfo}. + */ + @NonNull + Task getAppSetIdInfo(); +} diff --git a/play-services-appset/src/main/java/com/google/android/gms/appset/AppSetIdInfo.java b/play-services-appset/src/main/java/com/google/android/gms/appset/AppSetIdInfo.java new file mode 100644 index 00000000..29608f5f --- /dev/null +++ b/play-services-appset/src/main/java/com/google/android/gms/appset/AppSetIdInfo.java @@ -0,0 +1,75 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. + */ + +package com.google.android.gms.appset; + +import androidx.annotation.IntDef; +import org.microg.gms.common.Hide; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Contains information about app set ID. + */ +public class AppSetIdInfo { + /** + * The app set ID is scoped to the app. + */ + public static final int SCOPE_APP = 1; + /** + * The app set ID is scoped to a developer account on an app store. All apps from the same developer on a device will have + * the same developer scoped app set ID. + */ + public static final int SCOPE_DEVELOPER = 2; + + private final String id; + private final @Scope int scope; + + @Hide + public AppSetIdInfo(String id, @Scope int scope) { + this.id = id; + this.scope = scope; + } + + /** + * Gets the app set ID. + * + * @return the app set ID. + */ + public String getId() { + return id; + } + + /** + * Returns the {@link AppSetIdInfo.Scope} of the app set ID. Possible values include {@link #SCOPE_APP} and {@link #SCOPE_DEVELOPER}. + * + * @return the app set ID's {@link AppSetIdInfo.Scope}. + */ + public @Scope int getScope() { + return scope; + } + + /** + * Allowed constants for {@link AppSetIdInfo#getScope()}. + *

+ * Supported constants: + *

    + *
  • {@link #SCOPE_APP}
  • + *
  • {@link #SCOPE_DEVELOPER}
  • + *
+ */ + @Target({ElementType.TYPE_USE}) + @Retention(RetentionPolicy.SOURCE) + @IntDef({SCOPE_APP, SCOPE_DEVELOPER}) + public @interface Scope { + + } +} diff --git a/play-services-appset/src/main/java/com/google/android/gms/appset/AppSetIdRequestParams.java b/play-services-appset/src/main/java/com/google/android/gms/appset/AppSetIdRequestParams.java new file mode 100644 index 00000000..2bc9b0d0 --- /dev/null +++ b/play-services-appset/src/main/java/com/google/android/gms/appset/AppSetIdRequestParams.java @@ -0,0 +1,45 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.appset; + +import android.os.Parcel; + +import androidx.annotation.NonNull; + +import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; + +import org.microg.gms.common.Hide; +import org.microg.gms.utils.ToStringHelper; + +@SafeParcelable.Class +@Hide +public class AppSetIdRequestParams extends AbstractSafeParcelable { + @Field(1) + public final String version; + @Field(2) + public final String clientAppPackageName; + + @Constructor + public AppSetIdRequestParams(@Param(1) String version, @Param(2) String clientAppPackageName) { + this.version = version; + this.clientAppPackageName = clientAppPackageName; + } + + @NonNull + @Override + public String toString() { + return ToStringHelper.name("AppSetIdRequestParams").field("version", version).field("clientAppPackageName", clientAppPackageName).end(); + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } + + public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(AppSetIdRequestParams.class); +} diff --git a/play-services-appset/src/main/java/com/google/android/gms/appset/AppSetInfoParcel.java b/play-services-appset/src/main/java/com/google/android/gms/appset/AppSetInfoParcel.java new file mode 100644 index 00000000..dd47b947 --- /dev/null +++ b/play-services-appset/src/main/java/com/google/android/gms/appset/AppSetInfoParcel.java @@ -0,0 +1,38 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.appset; + +import android.os.Parcel; + +import androidx.annotation.NonNull; + +import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; +import org.microg.gms.common.Hide; + +@SafeParcelable.Class +@Hide +public class AppSetInfoParcel extends AbstractSafeParcelable { + @Field(1) + public final String id; + @Field(2) + public final int scope; + + @Constructor + public AppSetInfoParcel(@Param(1) String id, @Param(2) int scope) { + this.id = id; + this.scope = scope; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } + + public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(AppSetInfoParcel.class); + +} diff --git a/play-services-appset/src/main/java/com/google/android/gms/appset/package-info.java b/play-services-appset/src/main/java/com/google/android/gms/appset/package-info.java new file mode 100644 index 00000000..e9ab491d --- /dev/null +++ b/play-services-appset/src/main/java/com/google/android/gms/appset/package-info.java @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: 2022 microG Project Team + * SPDX-License-Identifier: CC-BY-4.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. + */ +/** + * For analytics or fraud prevention use cases, on a given device you may + need to correlate usage or actions across a set of apps owned by your organization. + */ +package com.google.android.gms.appset; diff --git a/play-services-auth-base/src/main/aidl/com/google/android/gms/auth/firstparty/dataservice/DeviceManagementInfoResponse.aidl b/play-services-auth-base/src/main/aidl/com/google/android/gms/auth/firstparty/dataservice/DeviceManagementInfoResponse.aidl new file mode 100644 index 00000000..950fc2fb --- /dev/null +++ b/play-services-auth-base/src/main/aidl/com/google/android/gms/auth/firstparty/dataservice/DeviceManagementInfoResponse.aidl @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.auth.firstparty.dataservice; + +parcelable DeviceManagementInfoResponse; diff --git a/play-services-auth-base/src/main/java/com/google/android/gms/auth/firstparty/dataservice/DeviceManagementInfoResponse.java b/play-services-auth-base/src/main/java/com/google/android/gms/auth/firstparty/dataservice/DeviceManagementInfoResponse.java new file mode 100644 index 00000000..bb93f63a --- /dev/null +++ b/play-services-auth-base/src/main/java/com/google/android/gms/auth/firstparty/dataservice/DeviceManagementInfoResponse.java @@ -0,0 +1,44 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.auth.firstparty.dataservice; + +import android.os.Parcel; + +import androidx.annotation.NonNull; + +import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; + +@SafeParcelable.Class +public class DeviceManagementInfoResponse extends AbstractSafeParcelable { + @Field(1) + public final int versionCode; + @Field(2) + public final String info; + @Field(3) + public final boolean status; + + + @Constructor + public DeviceManagementInfoResponse(@Param(1) int versionCode, @Param(2) String info, @Param(3) boolean status) { + this.versionCode = versionCode; + this.info = info; + this.status = status; + } + + public DeviceManagementInfoResponse(String info, boolean status) { + this(1, info, status); + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } + + public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(DeviceManagementInfoResponse.class); + +} diff --git a/play-services-auth/src/main/aidl/com/google/android/gms/auth/account/data/IAccountDataService.aidl b/play-services-auth/src/main/aidl/com/google/android/gms/auth/account/data/IAccountDataService.aidl new file mode 100644 index 00000000..2fd6c854 --- /dev/null +++ b/play-services-auth/src/main/aidl/com/google/android/gms/auth/account/data/IAccountDataService.aidl @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.auth.account.data; + +import com.google.android.gms.auth.account.data.IDeviceManagementInfoCallback; +import android.accounts.Account; +import com.google.android.gms.common.api.internal.IStatusCallback; + +interface IAccountDataService { + void requestDeviceManagementInfo(in IDeviceManagementInfoCallback callback, in Account account) = 0; + void requestAccountInfo(in IStatusCallback callback, in Account account, boolean isPrimary) = 1; + void requestProfileInfo(in IStatusCallback callback, String profile) = 2; +} diff --git a/play-services-auth/src/main/aidl/com/google/android/gms/auth/account/data/IDeviceManagementInfoCallback.aidl b/play-services-auth/src/main/aidl/com/google/android/gms/auth/account/data/IDeviceManagementInfoCallback.aidl new file mode 100644 index 00000000..801beac3 --- /dev/null +++ b/play-services-auth/src/main/aidl/com/google/android/gms/auth/account/data/IDeviceManagementInfoCallback.aidl @@ -0,0 +1,13 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.auth.account.data; + +import com.google.android.gms.auth.firstparty.dataservice.DeviceManagementInfoResponse; +import com.google.android.gms.common.api.Status; + +interface IDeviceManagementInfoCallback { + void onResult(in Status status, in DeviceManagementInfoResponse response); +} diff --git a/play-services-basement/src/main/java/org/microg/gms/common/GmsService.java b/play-services-basement/src/main/java/org/microg/gms/common/GmsService.java index 2802e9eb..a28d547b 100644 --- a/play-services-basement/src/main/java/org/microg/gms/common/GmsService.java +++ b/play-services-basement/src/main/java/org/microg/gms/common/GmsService.java @@ -91,6 +91,36 @@ public enum GmsService { CONTACT_SYNC(208, "com.google.android.gms.people.contactssync.service.START"), >>>>>>> 37dd2b59 (Add dummy service for SEMANTIC_LOCATION(173) (#2174)) IDENTITY_SIGN_IN(212, "com.google.android.gms.auth.api.identity.service.signin.START"), +<<<<<<< HEAD +======= + CREDENTIAL_STORE(214, "com.google.android.gms.fido.credentialstore.internal_service.START"), + MDI_SYNC(215, "com.google.android.gms.mdisync.service.START"), + EVENT_ATTESTATION(216, "com.google.android.gms.ads.identifier.service.EVENT_ATTESTATION"), + SCHEDULER(218, "com.google.android.gms.scheduler.ACTION_PROXY_SCHEDULE"), + AUTHORIZATION(219, "com.google.android.gms.auth.api.identity.service.authorization.START"), + FACS_SYNC(220, "com.google.android.gms.facs.internal.service.START"), + AUTH_CONFIG_SYNC(221, "com.google.android.gms.auth.config.service.START"), + CREDENTIAL_SAVING(223, "com.google.android.gms.auth.api.identity.service.credentialsaving.START"), + GOOGLE_AUTH(224, "com.google.android.gms.auth.account.authapi.START"), + ENTERPRISE_LOADER(225, "com.google.android.gms.enterprise.loader.service.START"), + THUNDERBIRD(226, "com.google.android.gms.thunderbird.service.START"), + NEARBY_EXPOSURE(236, "com.google.android.gms.nearby.exposurenotification.START"), + GMS_COMPLIANCE(257, "com.google.android.gms.gmscompliance.service.START"), + FIDO_SOURCE_DEVICE(262, "com.google.android.gms.fido.sourcedevice.service.START"), + FAST_PAIR(265, "com.google.android.gms.nearby.fastpair.START"), + MATCHSTICK_LIGHTER(268, "com.google.android.gms.matchstick.lighter.service.START"), + FIDO_TARGET_DEVICE_INTERNAL(269, "com.google.android.gms.fido.targetdevice.internal_service.START"), + TELEMETRY(270, "com.google.android.gms.common.telemetry.service.START"), + SECOND_DEVICE_AUTH(275, "com.google.android.gms.setup.auth.SecondDeviceAuth.START"), + LOCATION_SHARING_REPORTER(277, "com.google.android.gms.locationsharingreporter.service.START"), + OCR(279, "com.google.android.gms.ocr.service.START"), + POTOKENS(285, "com.google.android.gms.potokens.service.START"), + OCR_INTERNAL(281, "com.google.android.gms.ocr.service.internal.START"), + APP_SET(300, "com.google.android.gms.appset.service.START"), + MODULE_INSTALL(308, "com.google.android.gms.chimera.container.moduleinstall.ModuleInstallService.START"), + IN_APP_REACH(315, "com.google.android.gms.inappreach.service.START"), + APP_ERRORS(334, "com.google.android.gms.apperrors.service.START_APP_ERROR"), +>>>>>>> 9ee35e3c (Add dummy services for PSEUDONYMOUS_ID(52), GASS(116), ACCOUNT_DATA(153) and APP_SET(300) (#2175)) ; public final int SERVICE_ID; diff --git a/play-services-core/build.gradle b/play-services-core/build.gradle index 6ce62d2c..6c7ded13 100644 --- a/play-services-core/build.gradle +++ b/play-services-core/build.gradle @@ -32,6 +32,7 @@ dependencies { implementation project(':play-services-ads-identifier-core') implementation project(':play-services-ads-lite-core') implementation project(':play-services-appinvite-core') + implementation project(':play-services-appset-core') implementation project(':play-services-auth-api-phone-core') implementation project(':play-services-base-core') implementation project(':play-services-cast-core') diff --git a/play-services-core/src/main/AndroidManifest.xml b/play-services-core/src/main/AndroidManifest.xml index 47282490..20017ded 100644 --- a/play-services-core/src/main/AndroidManifest.xml +++ b/play-services-core/src/main/AndroidManifest.xml @@ -624,6 +624,24 @@ + + + + + + + + + + + + + + + + + + @@ -656,7 +674,6 @@ - @@ -703,7 +720,6 @@ - @@ -746,7 +762,6 @@ - diff --git a/play-services-core/src/main/kotlin/org/microg/gms/auth/account/data/AccountDataService.kt b/play-services-core/src/main/kotlin/org/microg/gms/auth/account/data/AccountDataService.kt new file mode 100644 index 00000000..e81ae066 --- /dev/null +++ b/play-services-core/src/main/kotlin/org/microg/gms/auth/account/data/AccountDataService.kt @@ -0,0 +1,52 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.auth.account.data + +import android.accounts.Account +import android.util.Log +import com.google.android.gms.auth.account.data.IAccountDataService +import com.google.android.gms.auth.account.data.IDeviceManagementInfoCallback +import com.google.android.gms.auth.firstparty.dataservice.DeviceManagementInfoResponse +import com.google.android.gms.common.ConnectionResult +import com.google.android.gms.common.Feature +import com.google.android.gms.common.api.Status +import com.google.android.gms.common.api.internal.IStatusCallback +import com.google.android.gms.common.internal.ConnectionInfo +import com.google.android.gms.common.internal.GetServiceRequest +import com.google.android.gms.common.internal.IGmsCallbacks +import org.microg.gms.BaseService +import org.microg.gms.common.GmsService + +private const val TAG = "AccountDataService" + +class AccountDataService : BaseService(TAG, GmsService.ACCOUNT_DATA) { + + override fun handleServiceRequest(callback: IGmsCallbacks, request: GetServiceRequest, service: GmsService) { + callback.onPostInitCompleteWithConnectionInfo( + ConnectionResult.SUCCESS, + AccountDataServiceImpl().asBinder(), + ConnectionInfo().apply { features = FEATURES } + ) + } + +} + +class AccountDataServiceImpl : IAccountDataService.Stub() { + override fun requestDeviceManagementInfo(callback: IDeviceManagementInfoCallback, account: Account?) { + Log.d(TAG, "requestDeviceManagementInfo is called ") + callback.onResult(Status.SUCCESS, DeviceManagementInfoResponse(null, false)) + } + + override fun requestAccountInfo(callback: IStatusCallback, account: Account?, isPrimary: Boolean) { + Log.d(TAG, "requestAccountInfo is called ") + callback.onResult(Status.SUCCESS) + } + + override fun requestProfileInfo(callback: IStatusCallback, profile: String?) { + Log.d(TAG, "requestProfileInfo is called ") + callback.onResult(Status.SUCCESS) + } +} diff --git a/play-services-core/src/main/kotlin/org/microg/gms/gass/GassService.kt b/play-services-core/src/main/kotlin/org/microg/gms/gass/GassService.kt new file mode 100644 index 00000000..afff107f --- /dev/null +++ b/play-services-core/src/main/kotlin/org/microg/gms/gass/GassService.kt @@ -0,0 +1,39 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ +package org.microg.gms.gass + +import android.os.Bundle +import android.util.Log +import com.google.android.gms.common.ConnectionResult +import com.google.android.gms.common.internal.GetServiceRequest +import com.google.android.gms.common.internal.IGmsCallbacks +import com.google.android.gms.gass.internal.GassRequestParcel +import com.google.android.gms.gass.internal.GassResponseParcel +import com.google.android.gms.gass.internal.IGassService +import org.microg.gms.BaseService +import org.microg.gms.common.GmsService + +private const val TAG = "GassService" + +class GassService : BaseService(TAG, GmsService.GASS) { + + override fun handleServiceRequest(callback: IGmsCallbacks?, request: GetServiceRequest?, service: GmsService?) { + callback?.onPostInitComplete(ConnectionResult.SUCCESS, GassServiceImpl().asBinder(), null) + } + +} + +class GassServiceImpl : IGassService.Stub() { + override fun getGassBundle(bundle: Bundle?, code: Int): Bundle? { + Log.d(TAG, "GassServiceImpl getGassBundle is Called") + return null + } + + override fun getGassResponse(gassRequestParcel: GassRequestParcel?): GassResponseParcel? { + Log.d(TAG, "GassServiceImpl getGassResponse is Called") + return null + } + +} diff --git a/play-services-core/src/main/kotlin/org/microg/gms/pseudonymous/PseudonymousIdService.kt b/play-services-core/src/main/kotlin/org/microg/gms/pseudonymous/PseudonymousIdService.kt new file mode 100644 index 00000000..7750ed3a --- /dev/null +++ b/play-services-core/src/main/kotlin/org/microg/gms/pseudonymous/PseudonymousIdService.kt @@ -0,0 +1,52 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.pseudonymous + +import android.util.Log +import com.google.android.gms.common.Feature +import com.google.android.gms.common.api.CommonStatusCodes +import com.google.android.gms.common.api.Status +import com.google.android.gms.common.internal.ConnectionInfo +import com.google.android.gms.common.internal.GetServiceRequest +import com.google.android.gms.common.internal.IGmsCallbacks +import com.google.android.gms.pseudonymous.PseudonymousIdToken +import com.google.android.gms.pseudonymous.internal.IPseudonymousIdCallbacks +import com.google.android.gms.pseudonymous.internal.IPseudonymousIdService +import org.microg.gms.BaseService +import org.microg.gms.common.GmsService + +private const val TAG = "PseudonymousIdService" +private val FEATURES = arrayOf(Feature("get_last_reset_time_api", 1L)) + +class PseudonymousIdService : BaseService(TAG, GmsService.PSEUDONYMOUS_ID) { + + override fun handleServiceRequest(callback: IGmsCallbacks, request: GetServiceRequest, service: GmsService) { + Log.e(TAG, " PseudonymousIdService handleServiceRequest is start") + callback.onPostInitCompleteWithConnectionInfo( + CommonStatusCodes.SUCCESS, + PseudonymousIdServiceImpl().asBinder(), + ConnectionInfo().apply { features = FEATURES } + ) + } +} + +class PseudonymousIdServiceImpl : IPseudonymousIdService.Stub() { + + override fun getToken(call: IPseudonymousIdCallbacks) { + Log.d(TAG, "PseudonymousIdServiceImpl onResponse is called") + call.onGetTokenResponse(Status.SUCCESS, PseudonymousIdToken(null)) + } + + override fun setToken(call: IPseudonymousIdCallbacks, token: PseudonymousIdToken?) { + Log.d(TAG, "PseudonymousIdServiceImpl onResponseToken is called") + call.onSetTokenResponse(Status.SUCCESS) + } + + override fun getLastResetWallTimeMs(callbacks: IPseudonymousIdCallbacks?) { + Log.d(TAG, "Not yet implemented: getLastResetWallTimeMs") + } + +} diff --git a/settings.gradle b/settings.gradle index 7b51c22b..4b13c94a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,3 +1,38 @@ +<<<<<<< HEAD +======= +def sublude(name) { + def projectName = ':' + name.substring(1).replace(':', '-') + include projectName + project(projectName).projectDir = file(name.substring(1).replace(':', '/')) +} + +def localProperties = new Properties() +try { + var stream = new File(rootDir, 'local.properties').newDataInputStream() + localProperties.load(stream) + stream.close() +} catch (ignored) { + // Ignore +} +def hasModule = (String name, boolean enabledByDefault) -> { + return localProperties.getProperty("modules." + name, enabledByDefault.toString()).toBoolean() +} + +include ':fake-signature' +include ':safe-parcel-processor' +include ':vending-app' + +include ':play-services-ads' +include ':play-services-ads-base' +include ':play-services-ads-identifier' +include ':play-services-ads-lite' +include ':play-services-appinvite' +include ':play-services-appset' +include ':play-services-auth' +include ':play-services-auth-api-phone' +include ':play-services-auth-base' +include ':play-services-base' +>>>>>>> 9ee35e3c (Add dummy services for PSEUDONYMOUS_ID(52), GASS(116), ACCOUNT_DATA(153) and APP_SET(300) (#2175)) include ':play-services-basement' <<<<<<< HEAD @@ -45,6 +80,7 @@ sublude ':play-services-ads:core' sublude ':play-services-ads-identifier:core' sublude ':play-services-ads-lite:core' sublude ':play-services-appinvite:core' +sublude ':play-services-appset:core' sublude ':play-services-auth-api-phone:core' sublude ':play-services-base:core' sublude ':play-services-base:core:package' From 00f39bdb5047a817fea8b2f6426baad26f753c0b Mon Sep 17 00:00:00 2001 From: Marvin W Date: Thu, 4 Jan 2024 20:21:52 +0100 Subject: [PATCH 049/209] Add API docs and details for Auth Identity API --- .../auth/api/identity/BeginSignInRequest.java | 581 +++++++++++++++++- .../auth/api/identity/BeginSignInResult.java | 21 +- .../api/identity/CredentialSavingClient.java | 56 ++ .../api/identity/CredentialSavingOptions.java | 23 + .../GetPhoneNumberHintIntentRequest.java | 46 +- .../api/identity/GetSignInIntentRequest.java | 186 +++++- .../identity/GoogleIdTokenRequestOptions.java | 41 -- .../gms/auth/api/identity/Identity.java | 70 +++ .../api/identity/PasswordRequestOptions.java | 27 - .../SaveAccountLinkingTokenRequest.java | 221 +++++++ .../SaveAccountLinkingTokenResult.java | 73 +++ .../api/identity/SavePasswordRequest.java | 108 ++++ .../auth/api/identity/SavePasswordResult.java | 62 ++ .../gms/auth/api/identity/SignInClient.java | 82 +++ .../auth/api/identity/SignInCredential.java | 132 +++- .../gms/auth/api/identity/SignInOptions.java | 23 + .../gms/auth/api/identity/SignInPassword.java | 70 +++ .../safeparcel/AbstractSafeParcelable.java | 30 + .../identity/IdentitySignInService.kt | 2 +- .../gms/auth/signin/AuthSignInActivity.kt | 251 ++++++++ .../microg/safeparcel/SafeParcelProcessor.kt | 428 +++++++++++++ 21 files changed, 2422 insertions(+), 111 deletions(-) create mode 100644 play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/CredentialSavingClient.java create mode 100644 play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/CredentialSavingOptions.java delete mode 100644 play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/GoogleIdTokenRequestOptions.java create mode 100644 play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/Identity.java delete mode 100644 play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/PasswordRequestOptions.java create mode 100644 play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/SaveAccountLinkingTokenRequest.java create mode 100644 play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/SaveAccountLinkingTokenResult.java create mode 100644 play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/SavePasswordRequest.java create mode 100644 play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/SavePasswordResult.java create mode 100644 play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/SignInClient.java create mode 100644 play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/SignInOptions.java create mode 100644 play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/SignInPassword.java create mode 100644 play-services-basement/src/main/java/com/google/android/gms/common/internal/safeparcel/AbstractSafeParcelable.java create mode 100644 play-services-core/src/main/kotlin/org/microg/gms/auth/signin/AuthSignInActivity.kt create mode 100644 safe-parcel-processor/src/main/kotlin/org/microg/safeparcel/SafeParcelProcessor.kt diff --git a/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/BeginSignInRequest.java b/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/BeginSignInRequest.java index 22d74516..f844822c 100644 --- a/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/BeginSignInRequest.java +++ b/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/BeginSignInRequest.java @@ -1,6 +1,9 @@ /* * SPDX-FileCopyrightText: 2023 microG Project Team * SPDX-License-Identifier: Apache-2.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. */ package com.google.android.gms.auth.api.identity; @@ -9,22 +12,579 @@ import android.os.Parcel; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; import com.google.android.gms.common.internal.safeparcel.SafeParcelable; import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; +import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialRequestOptions; +import org.microg.gms.common.Hide; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +/** + * Configurations that can be used to filter acceptable types of credentials returned from a sign-in attempt. + */ @SafeParcelable.Class public class BeginSignInRequest extends AbstractSafeParcelable { - @Field(1) - public PasswordRequestOptions passwordRequestOptions; - @Field(2) - public GoogleIdTokenRequestOptions googleIdTokenRequestOptions; - @Field(3) - public String score; - @Field(4) - public boolean isPrimary; - @Field(5) - public int code; + @Field(value = 1, getterName = "getPasswordRequestOptions") + private final PasswordRequestOptions passwordRequestOptions; + @Field(value = 2, getterName = "getGoogleIdTokenRequestOptions") + private final GoogleIdTokenRequestOptions googleIdTokenRequestOptions; + @Field(value = 3, getterName = "getSessionId") + private final String sessionId; + @Field(value = 4, getterName = "isAutoSelectEnabled") + private final boolean autoSelectEnabled; + @Field(value = 5, getterName = "getTheme") + private final int theme; + @Field(value = 6, getterName = "getPasskeysRequestOptions") + private final PasskeysRequestOptions passkeysRequestOptions; + @Field(value = 7, getterName = "getPasskeyJsonRequestOptions") + private final PasskeyJsonRequestOptions passkeyJsonRequestOptions; + + @Constructor + BeginSignInRequest(@Param(1) PasswordRequestOptions passwordRequestOptions, @Param(2) GoogleIdTokenRequestOptions googleIdTokenRequestOptions, @Param(3) String sessionId, @Param(4) boolean autoSelectEnabled, @Param(5) int theme, @Param(6) PasskeysRequestOptions passkeysRequestOptions, @Param(7) PasskeyJsonRequestOptions passkeyJsonRequestOptions) { + this.passwordRequestOptions = passwordRequestOptions; + this.googleIdTokenRequestOptions = googleIdTokenRequestOptions; + this.sessionId = sessionId; + this.autoSelectEnabled = autoSelectEnabled; + this.theme = theme; + this.passkeysRequestOptions = passkeysRequestOptions; + this.passkeyJsonRequestOptions = passkeyJsonRequestOptions; + } + + @NonNull + public static Builder builder() { + return new Builder(); + } + + public GoogleIdTokenRequestOptions getGoogleIdTokenRequestOptions() { + return googleIdTokenRequestOptions; + } + + public PasskeyJsonRequestOptions getPasskeyJsonRequestOptions() { + return passkeyJsonRequestOptions; + } + + public PasskeysRequestOptions getPasskeysRequestOptions() { + return passkeysRequestOptions; + } + + public PasswordRequestOptions getPasswordRequestOptions() { + return passwordRequestOptions; + } + + @Hide + public String getSessionId() { + return sessionId; + } + + @Hide + public int getTheme() { + return theme; + } + + /** + * Returns {@code true} if auto-select is requested, {@code false} otherwise. + */ + public boolean isAutoSelectEnabled() { + return autoSelectEnabled; + } + + public static class Builder { + + } + + /** + * Options for requesting Google ID token-backed credentials during sign-in. + */ + @Class + public static class GoogleIdTokenRequestOptions extends AbstractSafeParcelable { + @Field(value = 1, getterName = "isSupported") + private final boolean supported; + @Field(value = 2, getterName = "getServerClientId") + private final String serverClientId; + @Field(value = 3, getterName = "getNonce") + private final String nonce; + @Field(value = 4, getterName = "filterByAuthorizedAccounts") + private final boolean filterByAuthorizedAccounts; + @Field(value = 5, getterName = "getLinkedServiceId") + private final String linkedServiceId; + @Field(value = 6, getterName = "getIdTokenDepositionScopes") + private final List idTokenDepositionScopes; + @Field(value = 7, getterName = "requestVerifiedPhoneNumber") + private final boolean requestVerifiedPhoneNumber; + + @Hide + @Constructor + public GoogleIdTokenRequestOptions(@Param(1) boolean supported, @Param(2) String serverClientId, @Param(3) String nonce, @Param(4) boolean filterByAuthorizedAccounts, @Param(5) String linkedServiceId, @Param(6) List idTokenDepositionScopes, @Param(7) boolean requestVerifiedPhoneNumber) { + this.supported = supported; + this.serverClientId = serverClientId; + this.nonce = nonce; + this.filterByAuthorizedAccounts = filterByAuthorizedAccounts; + this.linkedServiceId = linkedServiceId; + this.idTokenDepositionScopes = idTokenDepositionScopes; + this.requestVerifiedPhoneNumber = requestVerifiedPhoneNumber; + } + + @NonNull + public static Builder builder() { + return new Builder(); + } + + public boolean filterByAuthorizedAccounts() { + return filterByAuthorizedAccounts; + } + + public List getIdTokenDepositionScopes() { + return idTokenDepositionScopes; + } + + public String getLinkedServiceId() { + return linkedServiceId; + } + + public String getNonce() { + return nonce; + } + + public String getServerClientId() { + return serverClientId; + } + + public boolean isSupported() { + return supported; + } + + /** + * @deprecated No replacement. + */ + @Deprecated + public boolean requestVerifiedPhoneNumber() { + return requestVerifiedPhoneNumber; + } + + /** + * Builder for {@link BeginSignInRequest.GoogleIdTokenRequestOptions}. + */ + public static class Builder { + private boolean supported; + @Nullable + private String serverClientId; + @Nullable + private String nonce; + private boolean filterByAuthorizedAccounts = true; + @Nullable + private String linkedServiceId; + @Nullable + private List idTokenDepositionScopes; + private boolean requestVerifiedPhoneNumber; + + /** + * Sets whether to support sign-in using Google accounts that are linked to your users' accounts. + *

+ * When such a credential is selected, a Google ID token for the Google account that the selected account is linked to, will + * first be deposited to your server and then returned to you. Similar to the regular sign-in, your backend could use the ID + * token to sign the user in. Note that, the ID token deposition will only happen the first time signing in, using this credential. + * Subsequent sign-ins will not require a deposition, since the ID token will already have been associated with your user's + * account. + * + * @param linkedServiceId service ID used when linking accounts to a Google account. + * @param idTokenDepositionScopes Oauth scopes that Google will need to deposit a Google ID token to your application's backend server. This + * is optional and can be set to null. Only required if Google requires particular scopes to be able to deposit + * the ID token. + */ + public Builder associateLinkedAccounts(@NonNull String linkedServiceId, @Nullable List idTokenDepositionScopes) { + this.linkedServiceId = linkedServiceId; + this.idTokenDepositionScopes = idTokenDepositionScopes; + return this; + } + + /** + * Returns the built {@link BeginSignInRequest.GoogleIdTokenRequestOptions}. + */ + @NonNull + public GoogleIdTokenRequestOptions build() { + return new GoogleIdTokenRequestOptions(supported, serverClientId, nonce, filterByAuthorizedAccounts, linkedServiceId, idTokenDepositionScopes, requestVerifiedPhoneNumber); + } + + /** + * Sets whether to only allow the user to select from Google accounts that are already authorized to sign in to your + * application. The default value is true. + *

+ * If {@code true}, the user will not be able to select any Google account that would otherwise require explicit authorization to + * share basic profile/email data with your application. This may reduce some friction in the sign-in user journey, and + * guarantees that the returned credential is for a "returning user", but limits the user's freedom to choose among all the + * Google accounts on the device. + * + * @param filterByAuthorizedAccounts whether to only allow the user to select from Google accounts that are already authorized to sign in to + * your application + */ + @NonNull + public Builder setFilterByAuthorizedAccounts(boolean filterByAuthorizedAccounts) { + this.filterByAuthorizedAccounts = filterByAuthorizedAccounts; + return this; + } + + /** + * Sets the nonce to use when generating a Google ID token. + * + * @param nonce the nonce to use during ID token generation + */ + @NonNull + public Builder setNonce(@Nullable String nonce) { + this.nonce = nonce; + return this; + } + + /** + * Sets whether to request for a verified phone number during sign-ups. + *

+ * In order to use this feature, the + * {@link BeginSignInRequest.GoogleIdTokenRequestOptions#filterByAuthorizedAccounts()} field must be explicitly set + * to false, because this feature is only available during sign-ups. + * + * @deprecated No replacement. + */ + @Deprecated + @NonNull + public Builder setRequestVerifiedPhoneNumber(boolean requestVerifiedPhoneNumber) { + this.requestVerifiedPhoneNumber = requestVerifiedPhoneNumber; + return this; + } + + /** + * Sets the server's client ID to use as the audience for Google ID tokens generated during the sign-in. + * + * @param serverClientId the client ID of the server to which the ID token will be issued + */ + @NonNull + public Builder setServerClientId(@NonNull String serverClientId) { + this.serverClientId = serverClientId; + return this; + } + + /** + * Sets whether Google ID token-backed credentials should be returned by the API. + * + * @param supported whether Google ID token-backed credentials should be returned + */ + @NonNull + public Builder setSupported(boolean supported) { + this.supported = supported; + return this; + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof GoogleIdTokenRequestOptions)) return false; + + GoogleIdTokenRequestOptions that = (GoogleIdTokenRequestOptions) o; + + if (supported != that.supported) return false; + if (filterByAuthorizedAccounts != that.filterByAuthorizedAccounts) return false; + if (requestVerifiedPhoneNumber != that.requestVerifiedPhoneNumber) return false; + if (!Objects.equals(serverClientId, that.serverClientId)) return false; + if (!Objects.equals(nonce, that.nonce)) return false; + if (!Objects.equals(linkedServiceId, that.linkedServiceId)) return false; + return Objects.equals(idTokenDepositionScopes, that.idTokenDepositionScopes); + } + + @Override + public int hashCode() { + return Arrays.hashCode(new Object[]{supported, serverClientId, nonce, filterByAuthorizedAccounts, linkedServiceId, idTokenDepositionScopes, requestVerifiedPhoneNumber}); + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } + + public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(GoogleIdTokenRequestOptions.class); + } + + /** + * Options for requesting passkeys during sign-in. + */ + @Class + public static class PasskeyJsonRequestOptions extends AbstractSafeParcelable { + @Field(value = 1, getterName = "isSupported") + private final boolean supported; + @Field(value = 2, getterName = "getRequestJson") + private final String requestJson; + + @Constructor + @Hide + public PasskeyJsonRequestOptions(@Param(1) boolean supported, @Param(2) String requestJson) { + this.supported = supported; + this.requestJson = requestJson; + } + + public static Builder builder() { + return new Builder(); + } + + public String getRequestJson() { + return requestJson; + } + + public boolean isSupported() { + return supported; + } + + /** + * Builder for {@link BeginSignInRequest.PasskeyJsonRequestOptions}. + */ + public static class Builder { + private boolean supported; + private String requestJson; + + @NonNull + public PasskeyJsonRequestOptions build() { + return new PasskeyJsonRequestOptions(supported, requestJson); + } + + /** + * Sets the {@link PublicKeyCredentialRequestOptions} in JSON format. + * + * @param requestJson the JSON formatted representation of the WebAuthn request. + */ + @NonNull + public Builder setRequestJson(@NonNull String requestJson) { + this.requestJson = requestJson; + return this; + } + + /** + * Sets whether passkey credentials should be returned by this API. + * + * @param supported whether passkey credentials should be returned + */ + @NonNull + public Builder setSupported(boolean supported) { + this.supported = supported; + return this; + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof PasskeyJsonRequestOptions)) return false; + + PasskeyJsonRequestOptions that = (PasskeyJsonRequestOptions) o; + + if (supported != that.supported) return false; + return Objects.equals(requestJson, that.requestJson); + } + + @Override + public int hashCode() { + return Arrays.hashCode(new Object[]{supported, requestJson}); + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } + + public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(PasskeyJsonRequestOptions.class); + } + + /** + * Options for requesting passkeys during sign-in. + * + * @deprecated Use {@link BeginSignInRequest.PasskeyJsonRequestOptions} instead. + */ + @Deprecated + @Class + public static class PasskeysRequestOptions extends AbstractSafeParcelable { + @Field(value = 1, getterName = "isSupported") + private final boolean supported; + @Field(value = 2, getterName = "getChallenge") + private final byte[] challenge; + @Field(value = 3, getterName = "getRpId") + private final String rpId; + + @Constructor + @Hide + public PasskeysRequestOptions(@Param(1) boolean supported, @Param(2) byte[] challenge, @Param(3) String rpId) { + this.supported = supported; + this.challenge = challenge; + this.rpId = rpId; + } + + public static Builder builder() { + return new Builder(); + } + + public byte[] getChallenge() { + return challenge; + } + + public String getRpId() { + return rpId; + } + + public boolean isSupported() { + return supported; + } + + /** + * Builder for {@link BeginSignInRequest.PasskeysRequestOptions}. + */ + public static class Builder { + private boolean supported; + private byte[] challenge; + private String rpId; + + public PasskeysRequestOptions build() { + return new PasskeysRequestOptions(supported, challenge, rpId); + } + + /** + * Sets the nonce value that the authenticator should sign using a private key corresponding to a public key credential that + * is acceptable for this authentication session. + * + * @param challenge the challenge + */ + @NonNull + public Builder setChallenge(@NonNull byte[] challenge) { + this.challenge = challenge; + return this; + } + + /** + * Sets identifier for a relying party, on whose behalf a given authentication operation is being performed. A public key + * credential can only be used for authentication with the same replying party it was registered with. + *

+ * Note: the RpId should be an effective domain (without scheme or port). + * + * @param rpId identifier for a relying party + */ + @NonNull + public Builder setRpId(@NonNull String rpId) { + this.rpId = rpId; + return this; + } + + /** + * Sets whether passkey credentials should be returned by this API. + * + * @param supported whether passkey credentials should be returned + */ + @NonNull + public Builder setSupported(boolean supported) { + this.supported = supported; + return this; + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof PasskeysRequestOptions)) return false; + + PasskeysRequestOptions that = (PasskeysRequestOptions) o; + + if (supported != that.supported) return false; + if (!Arrays.equals(challenge, that.challenge)) return false; + return Objects.equals(rpId, that.rpId); + } + + @Override + public int hashCode() { + return Arrays.hashCode(new Object[]{supported, challenge, rpId}); + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } + + public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(PasskeysRequestOptions.class); + } + + /** + * Options for requesting password-backed credentials during sign-in. + */ + @Class + public static class PasswordRequestOptions extends AbstractSafeParcelable { + @Field(value = 1, getterName = "isSupported") + public final boolean supported; + + @Constructor + @Hide + public PasswordRequestOptions(@Param(1) boolean supported) { + this.supported = supported; + } + + public static Builder builder() { + return new Builder(); + } + + public boolean isSupported() { + return supported; + } + + /** + * Builder for {@link BeginSignInRequest.PasswordRequestOptions}. + */ + public static class Builder { + private boolean supported = false; + + /** + * Returns the built {@link BeginSignInRequest.PasswordRequestOptions}. + */ + @NonNull + public PasswordRequestOptions build() { + return new PasswordRequestOptions(supported); + } + + /** + * Sets whether password-backed credentials should be returned by the API. + * + * @param supported whether password-backed credentials should be returned + */ + @NonNull + public Builder setSupported(boolean supported) { + this.supported = supported; + return this; + } + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } + + public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(PasswordRequestOptions.class); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof BeginSignInRequest)) return false; + + BeginSignInRequest that = (BeginSignInRequest) o; + + if (autoSelectEnabled != that.autoSelectEnabled) return false; + if (theme != that.theme) return false; + if (!Objects.equals(passwordRequestOptions, that.passwordRequestOptions)) return false; + if (!Objects.equals(googleIdTokenRequestOptions, that.googleIdTokenRequestOptions)) + return false; + if (!Objects.equals(sessionId, that.sessionId)) return false; + if (!Objects.equals(passkeysRequestOptions, that.passkeysRequestOptions)) return false; + return Objects.equals(passkeyJsonRequestOptions, that.passkeyJsonRequestOptions); + } + + @Override + public int hashCode() { + return Arrays.hashCode(new Object[]{passwordRequestOptions, googleIdTokenRequestOptions, sessionId, autoSelectEnabled, theme, passkeysRequestOptions, passkeyJsonRequestOptions}); + } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { @@ -32,4 +592,5 @@ public class BeginSignInRequest extends AbstractSafeParcelable { } public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(BeginSignInRequest.class); + } diff --git a/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/BeginSignInResult.java b/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/BeginSignInResult.java index 7c9b887d..ab875a06 100644 --- a/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/BeginSignInResult.java +++ b/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/BeginSignInResult.java @@ -1,6 +1,9 @@ /* * SPDX-FileCopyrightText: 2023 microG Project Team * SPDX-License-Identifier: Apache-2.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. */ package com.google.android.gms.auth.api.identity; @@ -13,11 +16,25 @@ import androidx.annotation.NonNull; import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; import com.google.android.gms.common.internal.safeparcel.SafeParcelable; import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; +import org.microg.gms.common.Hide; +/** + * Result returned from sign-in initiation that includes a {@link PendingIntent} that can be used to continue the sign-in flow. + */ @SafeParcelable.Class public class BeginSignInResult extends AbstractSafeParcelable { - @Field(1) - public PendingIntent pendingIntent; + @Field(value = 1, getterName = "getPendingIntent") + private final PendingIntent pendingIntent; + + @Constructor + @Hide + public BeginSignInResult(@Param(1) PendingIntent pendingIntent) { + this.pendingIntent = pendingIntent; + } + + public PendingIntent getPendingIntent() { + return pendingIntent; + } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { diff --git a/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/CredentialSavingClient.java b/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/CredentialSavingClient.java new file mode 100644 index 00000000..1761d85e --- /dev/null +++ b/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/CredentialSavingClient.java @@ -0,0 +1,56 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. + */ + +package com.google.android.gms.auth.api.identity; + +import android.app.PendingIntent; +import android.content.Intent; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.gms.common.api.HasApiKey; +import com.google.android.gms.common.api.Status; +import com.google.android.gms.tasks.Task; + +/** + * A client for the Credential Saving API. + */ +public interface CredentialSavingClient extends HasApiKey { + /** + * Extracts the {@link Status} from the {@link Intent} object in activity results. + */ + @NonNull + Status getStatusFromIntent(@Nullable Intent intent); + + /** + * Attempts to save a token for account linking. + *

+ * Calling this method will provide a {@link PendingIntent} in the response that can be used to launch the flow to complete the + * saving of the account linking token. As part of the request, you need to provide a {@link PendingIntent} for your consent page + * that Google Play services will launch in the middle of the flow. The result must then be sent back to the caller following a + * certain contract described in {@link SaveAccountLinkingTokenRequest.Builder#setConsentPendingIntent(PendingIntent)}. + * + * @param saveAccountLinkingTokenRequest the request that contains the parameters to successfully return a response that can be used to + * launch the appropriate flow. + * @return {@link Task} which may contain the {@link PendingIntent} required to launch the flow. To find out if the response can be used + * to start the flow, first call {@link SaveAccountLinkingTokenResult#hasResolution()}. + */ + @NonNull + Task saveAccountLinkingToken(@NonNull SaveAccountLinkingTokenRequest saveAccountLinkingTokenRequest); + + /** + * Initiates the storage of a password-backed credential that can later be used to sign a user in. + *

+ * If the request cannot be honored, an exception will be set on the returned {@link Task}. In all other cases, a + * {@link SavePasswordResult} will be returned. + * + * @param savePasswordRequest container for the {@link SignInPassword} for the password-saving flow + * @return {@link Task} which eventually contains the result of the initialization + */ + @NonNull + Task savePassword(@NonNull SavePasswordRequest savePasswordRequest); +} diff --git a/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/CredentialSavingOptions.java b/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/CredentialSavingOptions.java new file mode 100644 index 00000000..bfdd7320 --- /dev/null +++ b/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/CredentialSavingOptions.java @@ -0,0 +1,23 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.auth.api.identity; + +import androidx.annotation.Nullable; +import com.google.android.gms.common.api.Api; + +import java.util.Arrays; + +public class CredentialSavingOptions implements Api.ApiOptions.Optional { + @Override + public int hashCode() { + return Arrays.hashCode(new Object[]{CredentialSavingOptions.class}); + } + + @Override + public boolean equals(@Nullable Object obj) { + return obj instanceof CredentialSavingOptions; + } +} diff --git a/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/GetPhoneNumberHintIntentRequest.java b/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/GetPhoneNumberHintIntentRequest.java index 11343886..89063405 100644 --- a/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/GetPhoneNumberHintIntentRequest.java +++ b/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/GetPhoneNumberHintIntentRequest.java @@ -1,6 +1,9 @@ /* * SPDX-FileCopyrightText: 2023 microG Project Team * SPDX-License-Identifier: Apache-2.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. */ package com.google.android.gms.auth.api.identity; @@ -12,11 +15,50 @@ import androidx.annotation.NonNull; import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; import com.google.android.gms.common.internal.safeparcel.SafeParcelable; import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; +import org.microg.gms.common.Hide; +/** + * Request object used to get an Intent to start the Phone Number Hint flow. + */ @SafeParcelable.Class public class GetPhoneNumberHintIntentRequest extends AbstractSafeParcelable { - @Field(1) - public int code; + @Field(value = 1, getterName = "getTheme") + private final int theme; + + @Constructor + GetPhoneNumberHintIntentRequest(@Param(1) int theme) { + this.theme = theme; + } + + @NonNull + public static Builder builder() { + return new Builder(); + } + + @Hide + public int getTheme() { + return theme; + } + + /** + * Builder for {@link GetPhoneNumberHintIntentRequest} + */ + public static class Builder { + private int theme; + + @NonNull + public GetPhoneNumberHintIntentRequest build() { + return new GetPhoneNumberHintIntentRequest(theme); + } + + @Hide + @NonNull + public Builder setTheme(int theme) { + this.theme = theme; + return this; + } + } + @Override public void writeToParcel(@NonNull Parcel dest, int flags) { diff --git a/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/GetSignInIntentRequest.java b/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/GetSignInIntentRequest.java index bbb02c78..c656b329 100644 --- a/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/GetSignInIntentRequest.java +++ b/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/GetSignInIntentRequest.java @@ -1,6 +1,9 @@ /* * SPDX-FileCopyrightText: 2023 microG Project Team * SPDX-License-Identifier: Apache-2.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. */ package com.google.android.gms.auth.api.identity; @@ -9,24 +12,183 @@ import android.os.Parcel; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; import com.google.android.gms.common.internal.safeparcel.SafeParcelable; import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; +import org.microg.gms.common.Hide; +import java.util.Arrays; +import java.util.Objects; + +/** + * Request to get a Google sign-in intent. + */ @SafeParcelable.Class public class GetSignInIntentRequest extends AbstractSafeParcelable { - @Field(1) - public String clientId; - @Field(2) - public String scope; - @Field(3) - public String requestTag; - @Field(4) - public String requestToken; - @Field(5) - public boolean isPrimary; - @Field(6) - public int code; + @Field(value = 1, getterName = "getServerClientId") + private final String serverClientId; + @Field(value = 2, getterName = "getHostedDomainFilter") + private final String hostedDomainFilter; + @Field(value = 3, getterName = "getSessionId") + private final String sessionId; + @Field(value = 4, getterName = "getNonce") + private final String nonce; + @Field(value = 5, getterName = "requestVerifiedPhoneNumber") + private final boolean requestVerifiedPhoneNumber; + @Field(value = 6, getterName = "getTheme") + private final int theme; + + @Constructor + GetSignInIntentRequest(@Param(1) String serverClientId, @Param(2) String hostedDomainFilter, @Param(3) String sessionId, @Param(4) String nonce, @Param(5) boolean requestVerifiedPhoneNumber, @Param(6) int theme) { + this.serverClientId = serverClientId; + this.hostedDomainFilter = hostedDomainFilter; + this.sessionId = sessionId; + this.nonce = nonce; + this.requestVerifiedPhoneNumber = requestVerifiedPhoneNumber; + this.theme = theme; + } + + @NonNull + public static Builder builder() { + return new Builder(); + } + + /** + * Returns the {@code hostedDomainFilter} if it was set in the request, or {@code null} otherwise. + */ + public String getHostedDomainFilter() { + return hostedDomainFilter; + } + + /** + * Returns the nonce that was set in the request. + */ + public String getNonce() { + return nonce; + } + + /** + * Returns the {@code serverClientId} that was set in the request. + */ + public String getServerClientId() { + return serverClientId; + } + + @Hide + public String getSessionId() { + return sessionId; + } + + @Hide + public int getTheme() { + return theme; + } + + /** + * Returns whether a verified phone number is requested. + * + * @deprecated No replacement. + */ + @Deprecated + public boolean requestVerifiedPhoneNumber() { + return requestVerifiedPhoneNumber; + } + + /** + * Builder class for {@link GetSignInIntentRequest}. + */ + public static class Builder { + private String serverClientId; + private String hostedDomainFilter; + private String sessionId; + private String nonce; + private boolean requestVerifiedPhoneNumber; + private int theme; + + @NonNull + public GetSignInIntentRequest build() { + return new GetSignInIntentRequest(serverClientId, hostedDomainFilter, sessionId, nonce, requestVerifiedPhoneNumber, theme); + } + + /** + * Sets the hosted domain filter (e.g. myuniveristy.edu). Default is no filter. + */ + @NonNull + public Builder filterByHostedDomain(@Nullable String hostedDomainFilter) { + this.hostedDomainFilter = hostedDomainFilter; + return this; + } + + /** + * Sets the nonce to use when generating a Google ID token. The values for nonce can be any random string and is used to + * prevent replay-attack. Default is no nonce. + * + * @param nonce the nonce to use during ID token generation + */ + @NonNull + public Builder setNonce(@Nullable String nonce) { + this.nonce = nonce; + return this; + } + + /** + * Sets whether to prompt the users to share a verified phone number associated with their Google accounts. + *

+ * Note that if the user selects a Google account which has previously been used to sign up to your app, they won't be + * prompted with the phone number selection. + * + * @deprecated No replacement. + */ + @Deprecated + @NonNull + public Builder setRequestVerifiedPhoneNumber(boolean requestsVerifiedPhoneNumber) { + this.requestVerifiedPhoneNumber = requestsVerifiedPhoneNumber; + return this; + } + + /** + * Sets the client ID of the server that will verify the integrity of the token. Calling this method to set the {@code serverClientId} is + * required. + */ + @NonNull + public Builder setServerClientId(@NonNull String serverClientId) { + this.serverClientId = serverClientId; + return this; + } + + @NonNull + public Builder setSessionId(@Nullable String sessionId) { + this.sessionId = sessionId; + return this; + } + + @NonNull + public Builder setTheme(int theme) { + this.theme = theme; + return this; + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof GetSignInIntentRequest)) return false; + + GetSignInIntentRequest that = (GetSignInIntentRequest) o; + + if (requestVerifiedPhoneNumber != that.requestVerifiedPhoneNumber) return false; + if (theme != that.theme) return false; + if (!Objects.equals(serverClientId, that.serverClientId)) return false; + if (!Objects.equals(hostedDomainFilter, that.hostedDomainFilter)) return false; + if (!Objects.equals(sessionId, that.sessionId)) return false; + return Objects.equals(nonce, that.nonce); + } + + @Override + public int hashCode() { + return Arrays.hashCode(new Object[]{serverClientId, hostedDomainFilter, serverClientId, nonce, requestVerifiedPhoneNumber, theme}); + } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { diff --git a/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/GoogleIdTokenRequestOptions.java b/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/GoogleIdTokenRequestOptions.java deleted file mode 100644 index ffdf5c5e..00000000 --- a/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/GoogleIdTokenRequestOptions.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2023 microG Project Team - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.google.android.gms.auth.api.identity; - -import android.os.Parcel; - -import androidx.annotation.NonNull; - -import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; -import com.google.android.gms.common.internal.safeparcel.SafeParcelable; -import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; - -import java.util.ArrayList; - -@SafeParcelable.Class -public class GoogleIdTokenRequestOptions extends AbstractSafeParcelable { - @Field(1) - public boolean idTokenRequested; - @Field(2) - public String clientId; - @Field(3) - public String requestToken; - @Field(4) - public boolean serverAuthCodeRequested; - @Field(5) - public String serverClientId; - @Field(6) - public ArrayList scopes; - @Field(7) - public boolean forceCodeForRefreshToken; - - @Override - public void writeToParcel(@NonNull Parcel dest, int flags) { - CREATOR.writeToParcel(this, dest, flags); - } - - public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(GoogleIdTokenRequestOptions.class); -} diff --git a/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/Identity.java b/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/Identity.java new file mode 100644 index 00000000..19647beb --- /dev/null +++ b/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/Identity.java @@ -0,0 +1,70 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. + */ + +package com.google.android.gms.auth.api.identity; + +import android.app.Activity; +import android.content.Context; +import androidx.annotation.NonNull; + +/** + * The entry point to the Sign-In APIs. + */ +public final class Identity { + +// /** +// * Returns a new instance of {@link AuthorizationClient}. +// */ +// @NonNull +// public static AuthorizationClient getAuthorizationClient(@NonNull Context context) { +// throw new UnsupportedOperationException(); +// } +// +// /** +// * Returns a new instance of {@link AuthorizationClient}. +// */ +// @NonNull +// public static AuthorizationClient getAuthorizationClient(@NonNull Activity activity) { +// throw new UnsupportedOperationException(); +// } + + /** + * Returns a new instance of {@link CredentialSavingClient}. + */ + @NonNull + public static CredentialSavingClient getCredentialSavingClient(@NonNull Activity activity) { + throw new UnsupportedOperationException(); + } + + /** + * Returns a new instance of {@link CredentialSavingClient}. + */ + @NonNull + public static CredentialSavingClient getCredentialSavingClient(@NonNull Context context) { + throw new UnsupportedOperationException(); + } + + /** + * Returns a new instance of {@link SignInClient}. + */ + @NonNull + public static SignInClient getSignInClient(@NonNull Activity activity) { + throw new UnsupportedOperationException(); + } + + /** + * Returns a new instance of {@link SignInClient}. + */ + @NonNull + public static SignInClient getSignInClient(@NonNull Context context) { + throw new UnsupportedOperationException(); + } + + private Identity() { + } +} diff --git a/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/PasswordRequestOptions.java b/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/PasswordRequestOptions.java deleted file mode 100644 index dfca1e06..00000000 --- a/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/PasswordRequestOptions.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2023 microG Project Team - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.google.android.gms.auth.api.identity; - -import android.os.Parcel; - -import androidx.annotation.NonNull; - -import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; -import com.google.android.gms.common.internal.safeparcel.SafeParcelable; -import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; - -@SafeParcelable.Class -public class PasswordRequestOptions extends AbstractSafeParcelable { - @Field(1) - public boolean primary; - - @Override - public void writeToParcel(@NonNull Parcel dest, int flags) { - CREATOR.writeToParcel(this, dest, flags); - } - - public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(PasswordRequestOptions.class); -} diff --git a/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/SaveAccountLinkingTokenRequest.java b/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/SaveAccountLinkingTokenRequest.java new file mode 100644 index 00000000..ada66da8 --- /dev/null +++ b/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/SaveAccountLinkingTokenRequest.java @@ -0,0 +1,221 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. + */ + +package com.google.android.gms.auth.api.identity; + +import android.app.Activity; +import android.app.PendingIntent; +import android.os.Parcel; +import androidx.annotation.NonNull; +import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; +import org.microg.gms.common.Hide; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +/** + * Configurations that allow saving a token to Google for the purpose of account linking. + */ +public class SaveAccountLinkingTokenRequest extends AbstractSafeParcelable { + public static final String EXTRA_TOKEN = "extra_token"; + /** + * A constant to indicate that the type of token that will be saved is an Authorization Code. + */ + public static final String TOKEN_TYPE_AUTH_CODE = "auth_code"; + + @Field(value = 1, getterName = "getConsentPendingIntent") + private final PendingIntent consentPendingIntent; + @Field(value = 2, getterName = "getTokenType") + private final String tokenType; + @Field(value = 3, getterName = "getServiceId") + private final String serviceId; + @Field(value = 4, getterName = "getScopes") + private final List scopes; + @Field(value = 5, getterName = "getSessionId") + private final String sessionId; + @Field(value = 6, getterName = "getTheme") + private final int theme; + + @Constructor + SaveAccountLinkingTokenRequest(@Param(1) PendingIntent consentPendingIntent, @Param(2) String tokenType, @Param(3) String serviceId, @Param(4) List scopes, @Param(5) String sessionId, @Param(6) int theme) { + this.consentPendingIntent = consentPendingIntent; + this.tokenType = tokenType; + this.serviceId = serviceId; + this.scopes = scopes; + this.sessionId = sessionId; + this.theme = theme; + } + + /** + * Returns an instance of the {@link SaveAccountLinkingTokenRequest.Builder} that can be used to build an instance of {@link SaveAccountLinkingTokenRequest}. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Returns the {@link PendingIntent} that is set in the request and will be used to launch the consent page by Google Play Services during the flow. + */ + public PendingIntent getConsentPendingIntent() { + return consentPendingIntent; + } + + /** + * Returns the scopes that were set in the request. These are the requested scopes for the token that will be issued by your application. + */ + public List getScopes() { + return scopes; + } + + /** + * Returns the service-id that was set in the request. This service-id can be found in your cloud project. + */ + public String getServiceId() { + return serviceId; + } + + @Hide + public String getSessionId() { + return sessionId; + } + + @Hide + public int getTheme() { + return theme; + } + + /** + * Returns the type of token that is requested. + */ + public String getTokenType() { + return tokenType; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof SaveAccountLinkingTokenRequest)) return false; + + SaveAccountLinkingTokenRequest that = (SaveAccountLinkingTokenRequest) o; + + if (theme != that.theme) return false; + if (!Objects.equals(consentPendingIntent, that.consentPendingIntent)) return false; + if (!Objects.equals(tokenType, that.tokenType)) return false; + if (!Objects.equals(serviceId, that.serviceId)) return false; + if (!Objects.equals(scopes, that.scopes)) return false; + return Objects.equals(sessionId, that.sessionId); + } + + @Override + public int hashCode() { + return Arrays.hashCode(new Object[]{consentPendingIntent, tokenType, serviceId, scopes, serviceId, theme}); + } + + /** + * Builder for {@link SaveAccountLinkingTokenRequest}. + */ + public static class Builder { + private PendingIntent consentPendingIntent; + private String tokenType; + private String serviceId; + private List scopes; + private String sessionId; + private int theme; + + /** + * Builds an immutable instance of the {@link SaveAccountLinkingTokenRequest}. + */ + @NonNull + public SaveAccountLinkingTokenRequest build() { + return new SaveAccountLinkingTokenRequest(consentPendingIntent, tokenType, serviceId, scopes, sessionId, theme); + } + + /** + * Sets the (mandatory) {@link PendingIntent} that can be launched by Google Play services to show the consent page during + * the flow. When the launched Activity is finished, it has to provide the appropriate data in the result that it returns to the + * caller, based on the following contract: + *

    + *
  • + * If the user has accepted the consent, the launched Activity must call {@code setResult} with {@link Activity#RESULT_OK}, along with a + * token as a (string) intent extra in the result, with the key {@link SaveAccountLinkingTokenRequest#EXTRA_TOKEN}, + * similar to the following snippet: + *
    +         *  Intent intent = new Intent();
    +         *  intent.putExtra(SaveAccountLinkingTokenRequest.EXTRA_TOKEN, token);
    +         *  setResult(Activity.RESULT_OK, intent);
    +         *  finish();
    +         *         
    + *
  • + *
  • + * If, however, the user has rejected the consent, the Activity has to call {@code setResult} with {@link Activity#RESULT_CANCELED}. + *
  • + *
+ */ + @NonNull + public Builder setConsentPendingIntent(PendingIntent consentPendingIntent) { + this.consentPendingIntent = consentPendingIntent; + return this; + } + + /** + * Sets the list of scopes that are associated with the token that will be saved to Google. Calling this method with the + * correct scope(s) is required. + */ + @NonNull + public Builder setScopes(List scopes) { + this.scopes = scopes; + return this; + } + + /** + * Sets the service-id that can be obtained from your Google Cloud project. Calling this method to set {@code serviceId} + * is required. + */ + @NonNull + public Builder setServiceId(String serviceId) { + this.serviceId = serviceId; + return this; + } + + @Hide + @NonNull + public Builder setSessionId(String sessionId) { + this.sessionId = sessionId; + return this; + } + + @Hide + @NonNull + public Builder setTheme(int theme) { + this.theme = theme; + return this; + } + + /** + * Sets the type of token that will be saved to Google. Valid options are: + *
    + *
  • {@link SaveAccountLinkingTokenRequest#TOKEN_TYPE_AUTH_CODE}
  • + *
+ * Calling this method with a valid token type is required. + */ + @NonNull + public Builder setTokenType(String tokenType) { + this.tokenType = tokenType; + return this; + } + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } + + public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(SaveAccountLinkingTokenRequest.class); +} diff --git a/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/SaveAccountLinkingTokenResult.java b/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/SaveAccountLinkingTokenResult.java new file mode 100644 index 00000000..11448995 --- /dev/null +++ b/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/SaveAccountLinkingTokenResult.java @@ -0,0 +1,73 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. + */ + +package com.google.android.gms.auth.api.identity; + +import android.app.PendingIntent; +import android.os.Parcel; +import androidx.annotation.NonNull; +import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; +import org.microg.gms.common.Hide; + +import java.util.Objects; + +/** + * Result returned from the initial call to save an account linking token that includes a {@link PendingIntent} that can be used to + * continue the flow. + */ +@SafeParcelable.Class +public class SaveAccountLinkingTokenResult extends AbstractSafeParcelable { + @Field(value = 1, getterName = "getPendingIntent") + private final PendingIntent pendingIntent; + + @Constructor + @Hide + public SaveAccountLinkingTokenResult(@Param(1) PendingIntent pendingIntent) { + this.pendingIntent = pendingIntent; + } + + /** + * Returns the {@link PendingIntent} that can be used to launch the flow. Note that this method can return a {@code null} value if such + * flow cannot be started. It is expected that the caller first calls {@link #hasResolution()} to make sure the flow can be started, + * instead of examining the nullness of the result returned by this method. + */ + public PendingIntent getPendingIntent() { + return pendingIntent; + } + + /** + * Returns {@code true} if and only if this result contains a resolution that needs to be launched. + */ + public boolean hasResolution() { + return pendingIntent != null; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof SaveAccountLinkingTokenResult)) return false; + + SaveAccountLinkingTokenResult that = (SaveAccountLinkingTokenResult) o; + + return Objects.equals(pendingIntent, that.pendingIntent); + } + + @Override + public int hashCode() { + return pendingIntent != null ? pendingIntent.hashCode() : 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } + + public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(SaveAccountLinkingTokenResult.class); +} diff --git a/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/SavePasswordRequest.java b/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/SavePasswordRequest.java new file mode 100644 index 00000000..cd998a20 --- /dev/null +++ b/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/SavePasswordRequest.java @@ -0,0 +1,108 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. + */ + +package com.google.android.gms.auth.api.identity; + +import android.os.Parcel; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; +import org.microg.gms.common.Hide; + +/** + * Configurations that allow saving a {@link SignInPassword} to Google. + */ +@SafeParcelable.Class +public class SavePasswordRequest extends AbstractSafeParcelable { + @Field(value = 1, getterName = "getSignInPassword") + private final SignInPassword signInPassword; + @Field(value = 2, getterName = "getSessionId") + @Nullable + private final String sessionId; + @Field(value = 3, getterName = "getTheme") + private final int theme; + + @Constructor + SavePasswordRequest(@Param(1) SignInPassword signInPassword, @Param(2) @Nullable String sessionId, @Param(3) int theme) { + this.signInPassword = signInPassword; + this.sessionId = sessionId; + this.theme = theme; + } + + /** + * Returns a new instance of the {@link SavePasswordRequest.Builder}. + */ + public static Builder builder() { + return new Builder(); + } + + @Hide + @Nullable + public String getSessionId() { + return sessionId; + } + + public SignInPassword getSignInPassword() { + return signInPassword; + } + + @Hide + public int getTheme() { + return theme; + } + + /** + * Builder for {@link SavePasswordRequest}. + */ + public static class Builder { + private SignInPassword signInPassword; + @Nullable + private String sessionId; + private int theme; + + /** + * Sets the {@link SignInPassword} + */ + @NonNull + public Builder setSignInPassword(@NonNull SignInPassword signInPassword) { + this.signInPassword = signInPassword; + return this; + } + + @Hide + @NonNull + public Builder setSessionId(@Nullable String sessionId) { + this.sessionId = sessionId; + return this; + } + + @Hide + @NonNull + public Builder setTheme(int theme) { + this.theme = theme; + return this; + } + + /** + * Builds and returns a new instance of {@link SavePasswordRequest}. + */ + @NonNull + public SavePasswordRequest build() { + return new SavePasswordRequest(signInPassword, sessionId, theme); + } + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } + + public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(SavePasswordRequest.class); +} diff --git a/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/SavePasswordResult.java b/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/SavePasswordResult.java new file mode 100644 index 00000000..9360a1c3 --- /dev/null +++ b/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/SavePasswordResult.java @@ -0,0 +1,62 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. + */ + +package com.google.android.gms.auth.api.identity; + +import android.app.PendingIntent; +import android.os.Parcel; +import androidx.annotation.NonNull; +import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; + +import java.util.Objects; + +/** + * The result returned from calling {@link CredentialSavingClient#savePassword(SavePasswordRequest)} that includes a + * {@link PendingIntent} that can be used to launch the password saving flow. + */ +@SafeParcelable.Class +public class SavePasswordResult extends AbstractSafeParcelable { + @Field(value = 1, getterName = "getPendingIntent") + private final PendingIntent pendingIntent; + + @Constructor + public SavePasswordResult(@Param(1) PendingIntent pendingIntent) { + this.pendingIntent = pendingIntent; + } + + /** + * Returns the {@link PendingIntent} that should be launched to start the UI flow for saving the password. + */ + public PendingIntent getPendingIntent() { + return pendingIntent; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof SavePasswordResult)) return false; + + SavePasswordResult that = (SavePasswordResult) o; + + return Objects.equals(pendingIntent, that.pendingIntent); + } + + @Override + public int hashCode() { + return pendingIntent != null ? pendingIntent.hashCode() : 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } + + public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(SavePasswordResult.class); +} diff --git a/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/SignInClient.java b/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/SignInClient.java new file mode 100644 index 00000000..0268ef85 --- /dev/null +++ b/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/SignInClient.java @@ -0,0 +1,82 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. + */ + +package com.google.android.gms.auth.api.identity; + +import android.app.PendingIntent; +import android.content.Intent; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.gms.common.api.ApiException; +import com.google.android.gms.common.api.HasApiKey; +import com.google.android.gms.tasks.Task; + +public interface SignInClient extends HasApiKey { + /** + * Initiates the retrieval of a credential that can assist the caller in signing a user in to their application. + *

+ * If the request cannot be honored, an exception will be set on the returned {@link Task}. In all other cases, a + * {@link BeginSignInResult} will be returned. + * + * @param signInRequest configuration for the sign-in operation + * @return {@link Task} which eventually contains the result of the initialization + */ + @NonNull + Task beginSignIn(@NonNull BeginSignInRequest signInRequest); + + /** + * Retrieves the Phone Number from the {@link Intent} returned upon a successful Phone Number Hint request, throwing an + * {@link ApiException} if no phone number is available or the input {@link Intent} is null. + * + * @throws ApiException + */ + @NonNull + String getPhoneNumberFromIntent(@Nullable Intent data) throws ApiException; + + /** + * Gets the {@link PendingIntent} that initiates the Phone Number Hint flow. + *

+ * If there is no phone number on the device, an exception will be set on the returned {@link Task}. In all other cases, a + * {@link PendingIntent} will be returned. + * + * @return {@link Task} which can be used to start the Phone Number Hint flow. + */ + @NonNull + Task getPhoneNumberHintIntent(@NonNull GetPhoneNumberHintIntentRequest getPhoneNumberHintIntentRequest); + + /** + * Retrieves the {@link SignInCredential} from the {@link Intent} returned upon successful sign-in, throwing an {@link ApiException} if no + * credential is present. + * + * @throws ApiException + */ + @NonNull + SignInCredential getSignInCredentialFromIntent(@Nullable Intent data) throws ApiException; + + /** + * Gets the {@link PendingIntent} that initiates the Google Sign-in flow. + *

+ * If the request cannot be honored, an exception will be set on the returned {@link Task}. In all other cases, a {@link PendingIntent} + * will be returned. + * + * @param getSignInIntentRequest configuration for Google Sign-in flow + * @return {@link Task} which eventually contains the {@link PendingIntent} to start the Google Sign-in flow. + */ + @NonNull + Task getSignInIntent(@NonNull GetSignInIntentRequest getSignInIntentRequest); + + /** + * Resets internal state related to sign-in. + *

+ * This method should be invoked when a user signs out of your app. + * + * @return {@link Task} which eventually terminates in success or failure + */ + @NonNull + Task signOut(); +} diff --git a/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/SignInCredential.java b/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/SignInCredential.java index 5d4c8d38..85ef8e92 100644 --- a/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/SignInCredential.java +++ b/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/SignInCredential.java @@ -1,10 +1,14 @@ /* * SPDX-FileCopyrightText: 2023 microG Project Team * SPDX-License-Identifier: Apache-2.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. */ package com.google.android.gms.auth.api.identity; +import android.net.Uri; import android.os.Parcel; import androidx.annotation.NonNull; @@ -12,25 +16,121 @@ import androidx.annotation.NonNull; import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; import com.google.android.gms.common.internal.safeparcel.SafeParcelable; import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; +import com.google.android.gms.fido.fido2.api.common.PublicKeyCredential; +import org.microg.gms.common.Hide; +import java.util.Arrays; +import java.util.Objects; + +/** + * The credential returned as a result of a successful sign-in. Data returned within this object depends on the type of + * credential that user has selected; for example a password is returned only when a password-backed credential was + * selected. + */ @SafeParcelable.Class public class SignInCredential extends AbstractSafeParcelable { - @Field(1) - public String email; - @Field(2) - public String displayName; - @Field(3) - public String familyName; - @Field(4) - public String givenName; - @Field(5) - public String avatar; - @Field(6) - public String serverAuthCode; - @Field(7) - public String idToken; - @Field(8) - public String obfuscatedIdentifier; + @Field(value = 1, getterName = "getId") + private final String id; + @Field(value = 2, getterName = "getDisplayName") + private final String displayName; + @Field(value = 3, getterName = "getGivenName") + private final String givenName; + @Field(value = 4, getterName = "getFamilyName") + private final String familyName; + @Field(value = 5, getterName = "getProfilePictureUri") + private final Uri profilePictureUri; + @Field(value = 6, getterName = "getPassword") + private final String password; + @Field(value = 7, getterName = "getGoogleIdToken") + private final String googleIdToken; + @Field(value = 8, getterName = "getPhoneNumber") + private final String phoneNumber; + @Field(value = 9, getterName = "getPublicKeyCredential") + private final PublicKeyCredential publicKeyCredential; + + @Hide + @Constructor + public SignInCredential(@Param(1) String id, @Param(2) String displayName, @Param(3) String givenName, @Param(4) String familyName, @Param(5) Uri profilePictureUri, @Param(6) String password, @Param(7) String googleIdToken, @Param(8) String phoneNumber, @Param(9) PublicKeyCredential publicKeyCredential) { + this.id = id; + this.displayName = displayName; + this.givenName = givenName; + this.familyName = familyName; + this.profilePictureUri = profilePictureUri; + this.password = password; + this.googleIdToken = googleIdToken; + this.phoneNumber = phoneNumber; + this.publicKeyCredential = publicKeyCredential; + } + + public String getDisplayName() { + return displayName; + } + + public String getFamilyName() { + return familyName; + } + + public String getGivenName() { + return givenName; + } + + public String getGoogleIdToken() { + return googleIdToken; + } + + public String getPassword() { + return password; + } + + /** + * Returns the identifier of the credential. For an ID token credential, this returns the email address of the user's account + * and for a password-backed credential, it returns the username for that password. + */ + public String getId() { + return id; + } + + /** + * @deprecated No replacement. + */ + @Deprecated + public String getPhoneNumber() { + return phoneNumber; + } + + public Uri getProfilePictureUri() { + return profilePictureUri; + } + + /** + * Returns {@code publicKeyCredential}. + */ + public PublicKeyCredential getPublicKeyCredential() { + return publicKeyCredential; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof SignInCredential)) return false; + + SignInCredential that = (SignInCredential) o; + + if (!Objects.equals(id, that.id)) return false; + if (!Objects.equals(displayName, that.displayName)) return false; + if (!Objects.equals(givenName, that.givenName)) return false; + if (!Objects.equals(familyName, that.familyName)) return false; + if (!Objects.equals(profilePictureUri, that.profilePictureUri)) return false; + if (!Objects.equals(password, that.password)) return false; + if (!Objects.equals(googleIdToken, that.googleIdToken)) return false; + if (!Objects.equals(phoneNumber, that.phoneNumber)) return false; + return Objects.equals(publicKeyCredential, that.publicKeyCredential); + } + + @Override + public int hashCode() { + return Arrays.hashCode(new Object[]{id, displayName, givenName, familyName, profilePictureUri, password, googleIdToken, phoneNumber, publicKeyCredential}); + } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { diff --git a/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/SignInOptions.java b/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/SignInOptions.java new file mode 100644 index 00000000..c1078df4 --- /dev/null +++ b/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/SignInOptions.java @@ -0,0 +1,23 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.auth.api.identity; + +import androidx.annotation.Nullable; +import com.google.android.gms.common.api.Api; + +import java.util.Arrays; + +public class SignInOptions implements Api.ApiOptions.Optional { + @Override + public int hashCode() { + return Arrays.hashCode(new Object[]{SignInOptions.class}); + } + + @Override + public boolean equals(@Nullable Object obj) { + return obj instanceof SignInOptions; + } +} diff --git a/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/SignInPassword.java b/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/SignInPassword.java new file mode 100644 index 00000000..a2889522 --- /dev/null +++ b/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/SignInPassword.java @@ -0,0 +1,70 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. + */ + +package com.google.android.gms.auth.api.identity; + +import android.os.Parcel; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; + +import java.util.Arrays; + +/** + * An account ID such as a username or an email, and a password that can be used to sign a user in. + */ +@SafeParcelable.Class +public class SignInPassword extends AbstractSafeParcelable { + @Field(value = 1, getterName = "getId") + @NonNull + private final String id; + @Field(value = 2, getterName = "getPassword") + @NonNull + private final String password; + + @Constructor + public SignInPassword(@Param(1) @NonNull String id, @Param(2) @NonNull String password) { + this.id = id; + this.password = password; + } + + @NonNull + public String getId() { + return id; + } + + @NonNull + public String getPassword() { + return password; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof SignInPassword)) return false; + + SignInPassword that = (SignInPassword) o; + + if (!id.equals(that.id)) return false; + return password.equals(that.password); + } + + @Override + public int hashCode() { + return Arrays.hashCode(new String[]{id, password}); + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } + + public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(SignInPassword.class); +} diff --git a/play-services-basement/src/main/java/com/google/android/gms/common/internal/safeparcel/AbstractSafeParcelable.java b/play-services-basement/src/main/java/com/google/android/gms/common/internal/safeparcel/AbstractSafeParcelable.java new file mode 100644 index 00000000..671c3c4a --- /dev/null +++ b/play-services-basement/src/main/java/com/google/android/gms/common/internal/safeparcel/AbstractSafeParcelable.java @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.common.internal.safeparcel; + +public abstract class AbstractSafeParcelable implements SafeParcelable { + + @SuppressWarnings("unchecked") + public static SafeParcelableCreatorAndWriter findCreator(java.lang.Class tClass) { + java.lang.Class upmostClass = tClass; + while (upmostClass.getEnclosingClass() != null) upmostClass = upmostClass.getEnclosingClass(); + String upmostClassName = upmostClass.getName(); + int idx = upmostClassName.lastIndexOf('.'); + String packagePrefix = idx > 0 ? upmostClassName.substring(0, idx + 1) : ""; + String creatorClassName = packagePrefix + tClass.getSimpleName() + "$000Creator"; + try { + return (SafeParcelableCreatorAndWriter) java.lang.Class.forName(creatorClassName).newInstance(); + } catch (Exception e) { + throw new RuntimeException("No Creator found for " + tClass.getName(), e); + } + } + + @Override + public int describeContents() { + return 0; + } + +} diff --git a/play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/identity/IdentitySignInService.kt b/play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/identity/IdentitySignInService.kt index bf09ec58..b92f1cde 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/identity/IdentitySignInService.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/identity/IdentitySignInService.kt @@ -63,7 +63,7 @@ class IdentitySignInServiceImpl(private val mContext: Context, private val clien ) { val signInConfiguration = SignInConfiguration().apply { options = GoogleSignInOptions.Builder() - .requestIdToken(request.clientId) + .requestIdToken(request.serverClientId) .requestId() .requestEmail() .requestProfile() diff --git a/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/AuthSignInActivity.kt b/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/AuthSignInActivity.kt new file mode 100644 index 00000000..a0a0475e --- /dev/null +++ b/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/AuthSignInActivity.kt @@ -0,0 +1,251 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.auth.signin + +import android.accounts.Account +import android.accounts.AccountManager +import android.content.Intent +import android.graphics.Bitmap +import android.os.Bundle +import android.util.Log +import android.view.View +import android.view.ViewGroup +import android.widget.ArrayAdapter +import android.widget.ImageView +import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.getSystemService +import androidx.lifecycle.lifecycleScope +import com.google.android.gms.R +import com.google.android.gms.auth.api.identity.SignInCredential +import com.google.android.gms.auth.api.signin.GoogleSignInAccount +import com.google.android.gms.auth.api.signin.SignInAccount +import com.google.android.gms.auth.api.signin.internal.SignInConfiguration +import com.google.android.gms.common.Scopes +import com.google.android.gms.common.api.CommonStatusCodes +import com.google.android.gms.common.api.Status +import com.google.android.gms.common.internal.safeparcel.SafeParcelableSerializer +import com.google.android.gms.databinding.SigninConfirmBinding +import com.google.android.gms.databinding.SigninPickerBinding +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.microg.gms.auth.AuthConstants +import org.microg.gms.auth.AuthConstants.DEFAULT_ACCOUNT +import org.microg.gms.auth.AuthConstants.DEFAULT_ACCOUNT_TYPE +import org.microg.gms.auth.login.LoginActivity +import org.microg.gms.people.DatabaseHelper +import org.microg.gms.people.PeopleManager +import org.microg.gms.utils.getApplicationLabel + +private const val TAG = "AuthSignInActivity" +private const val REQUEST_CODE_ADD_ACCOUNT = 100 + +private val ACCEPTABLE_SCOPES = setOf(Scopes.OPENID, Scopes.EMAIL, Scopes.PROFILE, Scopes.USERINFO_EMAIL, Scopes.USERINFO_PROFILE, Scopes.GAMES_LITE) + +/** + * TODO: Get privacy policy / terms of service links via + * https://clientauthconfig.googleapis.com/google.identity.clientauthconfig.v1.ClientAuthConfig/GetDisplayBrand + */ +class AuthSignInActivity : AppCompatActivity() { + private val config: SignInConfiguration? + get() = runCatching { + intent?.extras?.also { it.classLoader = SignInConfiguration::class.java.classLoader }?.getParcelable("config") + }.getOrNull() + + private val Int.px: Int get() = (this * resources.displayMetrics.density).toInt() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setResult(CommonStatusCodes.CANCELED) + + Log.d(TAG, "Request: $config") + + val packageName = config?.packageName + if (packageName == null || (packageName != callingActivity?.packageName && callingActivity?.packageName != packageName)) + return finishResult(CommonStatusCodes.DEVELOPER_ERROR, "package name mismatch") + val accountManager = getSystemService() ?: return finishResult(CommonStatusCodes.INTERNAL_ERROR, "No account manager") + + val unacceptableScopes = config?.options?.scopes.orEmpty().filter { it.scopeUri !in ACCEPTABLE_SCOPES } + if (unacceptableScopes.isNotEmpty()) return finishResult(CommonStatusCodes.DEVELOPER_ERROR, "Unacceptable scopes: $unacceptableScopes") + + val accounts = accountManager.getAccountsByType(DEFAULT_ACCOUNT_TYPE) + if (accounts.isNotEmpty()) { + val account = config?.options?.account + if (account != null) { + if (account in accounts) { + showSignInConfirm(packageName, account) + } else { + finishResult(CommonStatusCodes.INVALID_ACCOUNT) + } + } else { + openAccountPicker(packageName) + } + } else { + openAddAccount() + } + } + + private fun openAddAccount() { + startActivityForResult(Intent(this, LoginActivity::class.java), REQUEST_CODE_ADD_ACCOUNT) + } + + private fun getDisplayName(account: Account): String? { + val databaseHelper = DatabaseHelper(this) + val cursor = databaseHelper.getOwner(account.name) + return try { + if (cursor.moveToNext()) { + cursor.getColumnIndex("display_name").takeIf { it >= 0 }?.let { cursor.getString(it) }.takeIf { !it.isNullOrBlank() } + } else null + } finally { + cursor.close() + databaseHelper.close() + } + } + + private fun bindAccountRow(root: View, account: Account, updateAction: (ImageView, Bitmap) -> Unit) { + val photoView = root.findViewById(R.id.account_photo) + val displayNameView = root.findViewById(R.id.account_display_name) + val emailView = root.findViewById(R.id.account_email) + if (account.name != DEFAULT_ACCOUNT) { + val photo = PeopleManager.getOwnerAvatarBitmap(this@AuthSignInActivity, account.name, false) + if (photo == null) { + lifecycleScope.launchWhenStarted { + withContext(Dispatchers.IO) { + PeopleManager.getOwnerAvatarBitmap(this@AuthSignInActivity, account.name, true) + }?.let { + updateAction(photoView, it) + } + } + } + val displayName = getDisplayName(account) + photoView.setImageBitmap(photo) + if (displayName != null) { + displayNameView.text = displayName + emailView.text = account.name + emailView.visibility = View.VISIBLE + } else { + displayNameView.text = account.name + emailView.visibility = View.GONE + } + } else { + photoView.setImageResource(R.drawable.ic_add_account_alt) + displayNameView.setText(R.string.signin_picker_add_account_label) + emailView.visibility = View.GONE + } + } + + private fun openAccountPicker(packageName: String) { + val binding = SigninPickerBinding.inflate(layoutInflater) + binding.appName = packageManager.getApplicationLabel(packageName).toString() + binding.appIcon = packageManager.getApplicationIcon(packageName) + val accounts = getSystemService()!!.getAccountsByType(DEFAULT_ACCOUNT_TYPE) + Account(DEFAULT_ACCOUNT, DEFAULT_ACCOUNT_TYPE) + binding.pickerList.adapter = object : ArrayAdapter(this, 0, accounts) { + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + val v = convertView ?: layoutInflater.inflate(R.layout.signin_account_row, parent, false) + getItem(position)?.let { bindAccountRow(v, it) { _, _ -> notifyDataSetChanged() } } + return v + } + } + binding.pickerList.setOnItemClickListener { parent, view, position, id -> + binding.listProgressSpinner = true + if (accounts[position].name == DEFAULT_ACCOUNT) { + openAddAccount() + } else { + lifecycleScope.launchWhenStarted { + try { + signIn(accounts[position]) + } catch (e: Exception) { + Log.w(TAG, e) + finishResult(CommonStatusCodes.INTERNAL_ERROR) + } + } + } + } + setContentView(binding.root) + } + + private fun showSignInConfirm(packageName: String, account: Account) { + val binding = SigninConfirmBinding.inflate(layoutInflater) + binding.appName = packageManager.getApplicationLabel(packageName).toString() + binding.appIcon = packageManager.getApplicationIcon(packageName) + bindAccountRow(binding.root, account) { view, bitmap -> view.setImageBitmap(bitmap) } + binding.button2.setOnClickListener { + finishResult(CommonStatusCodes.CANCELED) + } + binding.button1.setOnClickListener { + binding.button1.isEnabled = false + binding.button2.isEnabled = false + lifecycleScope.launchWhenStarted { + try { + signIn(account) + } catch (e: Exception) { + Log.w(TAG, e) + finishResult(CommonStatusCodes.INTERNAL_ERROR) + } + } + } + setContentView(binding.root) + } + + private suspend fun signIn(account: Account) { + val googleSignInAccount = performSignIn(this, config?.packageName!!, config?.options, account, true) + if (googleSignInAccount != null) { + finishResult(CommonStatusCodes.SUCCESS, account = account, googleSignInAccount = googleSignInAccount) + } else { + finishResult(CommonStatusCodes.INTERNAL_ERROR, "Sign in failed") + } + } + + private fun finishResult(statusCode: Int, message: String? = null, account: Account? = null, googleSignInAccount: GoogleSignInAccount? = null) { + val data = Intent() + if (statusCode != CommonStatusCodes.SUCCESS) data.putExtra(AuthConstants.ERROR_CODE, statusCode) + data.putExtra(AuthConstants.GOOGLE_SIGN_IN_STATUS, Status(statusCode, message)) + data.putExtra(AuthConstants.GOOGLE_SIGN_IN_ACCOUNT, googleSignInAccount) + val bundle = Bundle() + if (googleSignInAccount != null) { + val signInAccount = SignInAccount().apply { + email = googleSignInAccount.email ?: account?.name + this.googleSignInAccount = googleSignInAccount + userId = googleSignInAccount.id ?: getSystemService()?.getUserData( + account, + AuthConstants.GOOGLE_USER_ID + ) + } + data.putExtra(AuthConstants.SIGN_IN_ACCOUNT, signInAccount) + val credential = SignInCredential( + googleSignInAccount.email, + googleSignInAccount.displayName, + googleSignInAccount.familyName, + googleSignInAccount.givenName, + null, null, + googleSignInAccount.idToken, + null, null + ) + val credentialToBytes = SafeParcelableSerializer.serializeToBytes(credential) + bundle.putByteArray(AuthConstants.SIGN_IN_CREDENTIAL, credentialToBytes) + bundle.putByteArray(AuthConstants.STATUS, SafeParcelableSerializer.serializeToBytes(Status.SUCCESS)) + } else { + bundle.putByteArray(AuthConstants.STATUS, SafeParcelableSerializer.serializeToBytes(Status.CANCELED)) + } + data.putExtras(bundle) + Log.d(TAG, "Result: ${data.extras?.also { it.keySet() }}") + setResult(RESULT_OK, data) + finish() + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == REQUEST_CODE_ADD_ACCOUNT) { + val accountManager = getSystemService() ?: return finish() + val accounts = accountManager.getAccountsByType(DEFAULT_ACCOUNT_TYPE) + if (accounts.isNotEmpty()) { + openAccountPicker(config?.packageName!!) + } else { + finishResult(CommonStatusCodes.CANCELED, "No account and creation cancelled") + } + } + } +} \ No newline at end of file diff --git a/safe-parcel-processor/src/main/kotlin/org/microg/safeparcel/SafeParcelProcessor.kt b/safe-parcel-processor/src/main/kotlin/org/microg/safeparcel/SafeParcelProcessor.kt new file mode 100644 index 00000000..ed787062 --- /dev/null +++ b/safe-parcel-processor/src/main/kotlin/org/microg/safeparcel/SafeParcelProcessor.kt @@ -0,0 +1,428 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ +package org.microg.safeparcel + +import javax.annotation.processing.AbstractProcessor +import javax.annotation.processing.ProcessingEnvironment +import javax.annotation.processing.RoundEnvironment +import javax.annotation.processing.SupportedAnnotationTypes +import javax.annotation.processing.SupportedSourceVersion +import javax.lang.model.SourceVersion +import javax.lang.model.element.Element +import javax.lang.model.element.ElementKind +import javax.lang.model.element.ExecutableElement +import javax.lang.model.element.Modifier +import javax.lang.model.element.TypeElement +import javax.lang.model.element.VariableElement +import javax.tools.Diagnostic + +const val SafeParcelable = "com.google.android.gms.common.internal.safeparcel.SafeParcelable" +const val SafeParcelReader = "com.google.android.gms.common.internal.safeparcel.SafeParcelReader" +const val SafeParcelWriter = "com.google.android.gms.common.internal.safeparcel.SafeParcelWriter" +const val SafeParcelableCreatorAndWriter = "com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter" + +const val Field = "java.lang.reflect.Field" +const val Constructor = "java.lang.reflect.Constructor" + +const val Log = "android.util.Log" +const val Parcel = "android.os.Parcel" +const val Parcelable = "android.os.Parcelable" +const val IInterface = "android.os.IInterface" + +val NATIVE_SUPPORTED_TYPES = setOf( + "int", "byte", "short", "boolean", "long", "float", "double", + "java.lang.Boolean", "java.lang.Byte", "java.lang.Char", "java.lang.Short", "java.lang.Integer", "java.lang.Long", "java.lang.Float", "java.lang.Double", + "java.lang.String", "android.os.Bundle", "android.os.IBinder", + "int[]", "byte[]", "byte[][]", "float[]", "java.lang.String[]", + "java.util.List", "java.util.ArrayList", + +// "java.util.List", "java.util.List", +// "java.util.ArrayList", "java.util.ArrayList", +// "java.util.List", "java.util.List", "java.util.List", +// "java.util.ArrayList", "java.util.ArrayList", "java.util.ArrayList", +) + +@SupportedSourceVersion(SourceVersion.RELEASE_8) +@SupportedAnnotationTypes("$SafeParcelable.Class") +class SafeParcelProcessor : AbstractProcessor() { + override fun process(set: Set, roundEnvironment: RoundEnvironment): Boolean { + val safeParcelableClassTypeElement = set.firstOrNull() ?: return false + + classes@ for (classElement in roundEnvironment.getElementsAnnotatedWith(safeParcelableClassTypeElement)) { + val clazz = ClassInfo(classElement) + if (clazz.check(processingEnv)) { + processingEnv.filer.createSourceFile(clazz.fullCreatorName, clazz.classElement).openWriter().use { it.write(clazz.generateCreator()) } + } + } + return false + } +} + +class ClassInfo(val classElement: Element) { + val name = classElement.simpleName.toString() + val fullName = classElement.toString() + val packageName = let { + var upmostClassElement = classElement + while (upmostClassElement.enclosingElement != null) upmostClassElement = upmostClassElement.enclosingElement + upmostClassElement.toString() + } + + val creatorName = "$name\$000Creator" + val fullCreatorName = "$packageName.$creatorName" + + val fields = classElement.enclosedElements + .filter { it.kind == ElementKind.FIELD && !it.modifiers.contains(Modifier.STATIC) } + .filterIsInstance() + .filter { it.annotationMirrors.any { it.annotationType.toString() == "$SafeParcelable.Field" } } + .map { FieldInfo(this, it) } + val constructor = classElement.enclosedElements + .filter { it.kind == ElementKind.CONSTRUCTOR } + .filterIsInstance() + .filter { it.annotationMirrors.any { it.annotationType.toString() == "$SafeParcelable.Constructor" } || it.parameters.isEmpty() } + .let { if (it.size == 2) it.first { it.parameters.isNotEmpty() } else it.firstOrNull() } + ?.let { ConstructorInfo(this, it) } + + fun check(processingEnv: ProcessingEnvironment): Boolean { + fun note(message: String) = processingEnv.messager.printMessage(Diagnostic.Kind.NOTE, message) + fun error(message: String) = processingEnv.messager.printMessage(Diagnostic.Kind.ERROR, message) + if (constructor == null) { + error("No suitable constructor found for $fullName") + return false + } + if (constructor.parameters.any { it.annotationMirrors.none { it.annotationType.toString() == "$SafeParcelable.Param" } }) { + error("Tagged constructor for $fullName has parameters without @Param.") + return false + } + if (constructor.fieldIds.any { id -> fields.none { it.id == id } }) { + error("Constructor for $fullName has parameters with @Param value without matching @Field.") + return false + } + if (constructor.isPrivate) { + note("Using reflection to construct $fullName from parcel. Consider providing a suitable package-visible constructor for improved performance.") + } + for (field in fields) { + if (field.type !in NATIVE_SUPPORTED_TYPES) { + val typeName = field.listItemType ?: field.type + val type = runCatching { processingEnv.typeUtils.getDeclaredType(processingEnv.elementUtils.getTypeElement(typeName)) }.getOrNull() + val parcelable = processingEnv.typeUtils.getDeclaredType(processingEnv.elementUtils.getTypeElement(Parcelable)) + val iinterface = processingEnv.typeUtils.getDeclaredType(processingEnv.elementUtils.getTypeElement(IInterface)) + if (type != null && processingEnv.typeUtils.isAssignable(type, parcelable)) { + field.isParcelable = true + } else if (type != null && processingEnv.typeUtils.isAssignable(type, iinterface)) { + field.isIInterface = true + } else { + error("Field ${field.name} in $fullName has unsupported type ${if (typeName != field.type) "$typeName if ${field.type}" else field.type}.") + return false + } + } + val readReflect = field.isPrivate && field.getter == null + val writeReflect = field.isPrivate && !constructor.fieldIds.contains(field.id) + when { + readReflect && writeReflect -> note("Using reflection when accessing ${field.name} in $fullName. Consider adding it to the @Constructor and a getter to the annotation for improved performance.") + writeReflect -> note("Using reflection when writing ${field.name} in $fullName. Consider adding it to the @Constructor for improved performance.") + readReflect -> note("Using reflection when reading ${field.name} in $fullName. Consider adding a getter to the annotation for improved performance.") + } + } + return true + } + + fun generateCreator(): String { + if (constructor == null) throw IllegalStateException("Can't create Creator for class without constructor") + fun List.linesToString(prefix: String = "") = joinToString("\n $prefix") + val variableDeclarations = fields.map { it.variableDeclaration }.linesToString() + val setVariablesDefault = fields.map { it.setVariableDefault }.linesToString() + val readVariablesFromParcel = fields.map { it.readVariableFromParcelCase }.linesToString(" ") + val writeVariableToParcel = fields.map { it.writeVariableToParcel }.linesToString() + val setFieldsFromVariables = fields.filter { it.id !in constructor.fieldIds }.flatMap { it.setFieldFromVariable }.linesToString() + val invokeConstructor = constructor.invocation.linesToString() + val setVariablesFromFields = fields.flatMap { it.setVariableFromField }.linesToString() + val file = """ + package $packageName; + + //@javax.annotation.processing.Generated // Not supported by Android + @androidx.annotation.Keep + @org.microg.gms.common.Hide + public class $creatorName implements $SafeParcelableCreatorAndWriter<$fullName> { + @Override + public $fullName createFromParcel($Parcel parcel) { + int end = $SafeParcelReader.readObjectHeader(parcel); + $fullName object; + try { + $variableDeclarations + $setVariablesDefault + while (parcel.dataPosition() < end) { + int header = $SafeParcelReader.readHeader(parcel); + int fieldId = $SafeParcelReader.getFieldId(header); + switch (fieldId) { + $readVariablesFromParcel + default: + $Log.d("SafeParcel", String.format("Unknown field id %d in %s, skipping.", fieldId, "$fullName")); + $SafeParcelReader.skip(parcel, header); + } + } + $invokeConstructor + $setFieldsFromVariables + } catch (Exception e) { + throw new RuntimeException(String.format("Error reading %s", "$fullName"), e); + } + if (parcel.dataPosition() > end) { + throw new RuntimeException(String.format("Overread allowed size end=%d", end)); + } + return object; + } + + @Override + public void writeToParcel($fullName object, $Parcel parcel, int flags) { + int start = $SafeParcelWriter.writeObjectHeader(parcel); + try { + $variableDeclarations + $setVariablesFromFields + $writeVariableToParcel + } catch (Exception e) { + throw new RuntimeException(String.format("Error writing %s", "$fullName"), e); + } + $SafeParcelWriter.finishObjectHeader(parcel, start); + } + + @Override + public $fullName[] newArray(int size) { + return new $fullName[size]; + } + } + """.trimIndent() + return file + } +} + +class ConstructorInfo(val clazz: ClassInfo, val constructorElement: ExecutableElement) { + val isPrivate by lazy { constructorElement.modifiers.contains(Modifier.PRIVATE) } + val parameters by lazy { constructorElement.parameters } + val fieldIds by lazy { + parameters.map { + it.annotationMirrors + .first { it.annotationType.toString() == "$SafeParcelable.Param" } + .elementValues + .filter { it.key.simpleName.toString() == "value" } + .firstNotNullOfOrNull { it.value } + .toString() + } + } + val argTypes by lazy { fieldIds.map { id -> clazz.fields.first { it.id == id }.type } } + val args by lazy { fieldIds.map { id -> clazz.fields.first { it.id == id }.variableName } } + val invocation by lazy { + if (isPrivate) { + listOf( + "$Constructor<${clazz.fullName}> constructor = ${clazz.fullName}.class.getConstructor(${argTypes.map { "$it.class" }.joinToString(", ")});", + "constructor.setAccessible(true);", + "object = constructor.newInstance(${args.joinToString(", ")});" + ) + } else { + listOf("object = new ${clazz.fullName}(${args.joinToString(", ")});") + } + } +} + +class FieldInfo(val clazz: ClassInfo, val fieldElement: VariableElement) { + val name by lazy { fieldElement.simpleName.toString() } + val type by lazy { + fieldElement.annotationMirrors + .first { it.annotationType.toString() == "$SafeParcelable.Field" } + .elementValues + .filter { it.key.simpleName.toString() == "type" } + .firstNotNullOfOrNull { it.value.value } + ?.toString()?.takeIf { it.isNotEmpty() } + ?: fieldElement.asType().toString() + } + var isParcelable: Boolean = false + var isIInterface: Boolean = false + val isPrivate by lazy { fieldElement.modifiers.contains(Modifier.PRIVATE) } + + val id by lazy { + fieldElement.annotationMirrors + .first { it.annotationType.toString() == "$SafeParcelable.Field" } + .elementValues + .filter { it.key.simpleName.toString() == "value" } + .firstNotNullOfOrNull { it.value.value } + .toString() + } + val mayNull by lazy { + fieldElement.annotationMirrors + .first { it.annotationType.toString() == "$SafeParcelable.Field" } + .elementValues + .filter { it.key.simpleName.toString() == "mayNull" } + .firstNotNullOfOrNull { it.value.value } + ?.toString() + ?.toBoolean() == true + } + val useValueParcel by lazy { + fieldElement.annotationMirrors + .first { it.annotationType.toString() == "$SafeParcelable.Field" } + .elementValues + .filter { it.key.simpleName.toString() == "useValueParcel" } + .firstNotNullOfOrNull { it.value.value } + ?.toString() + ?.toBoolean() == true + } + val getter by lazy { + fieldElement.annotationMirrors + .first { it.annotationType.toString() == "$SafeParcelable.Field" } + .elementValues + .filter { it.key.simpleName.toString() == "getter" } + .firstNotNullOfOrNull { it.value.value } + ?.toString()?.takeIf { it.isNotEmpty() } + ?.replace("\$object", "object") + ?.replace("\$type", type) + ?: fieldElement.annotationMirrors + .first { it.annotationType.toString() == "$SafeParcelable.Field" } + .elementValues + .filter { it.key.simpleName.toString() == "getterName" } + .firstNotNullOfOrNull { it.value.value } + ?.toString()?.takeIf { it.isNotEmpty() } + ?.let { "object.$it()" } + } + val subType by lazy { + fieldElement.annotationMirrors + .first { it.annotationType.toString() == "$SafeParcelable.Field" } + .elementValues + .filter { it.key.simpleName.toString() == "subClass" } + .firstNotNullOfOrNull { it.value.value } + ?.toString()?.takeIf { it.isNotEmpty() } + } + val isList by lazy { type.startsWith("java.util.List<") || type.startsWith("java.util.ArrayList<") } + val isArray by lazy { type.endsWith("[]") } + val listItemType by lazy { + subType ?: when { + type.startsWith("java.util.List<") -> type.substring(15, type.length - 1) + type.startsWith("java.util.ArrayList<") -> type.substring(20, type.length - 1) + type.endsWith("[]") -> type.substring(0, type.length - 2) + else -> null + } + } + + val variableName by lazy { "_$name\$000" } + val variableDeclaration by lazy { "$type $variableName;" } + val defaultValue by lazy { + fieldElement.annotationMirrors + .first { it.annotationType.toString() == "$SafeParcelable.Field" } + .elementValues + .filter { it.key.simpleName.toString() == "defaultValue" } + .firstNotNullOfOrNull { it.value.value } + ?.toString()?.takeIf { it.isNotEmpty() } + ?: when (type) { + "boolean" -> "false" + "byte", "char", "short", "int", "long", "float", "double" -> "0" + else -> "null" + } + } + val setVariableDefault by lazy { "$variableName = $defaultValue;" } + + val readVariableFromParcel by lazy { + when (type) { + "int", "java.lang.Integer" -> "$variableName = $SafeParcelReader.readInt(parcel, header)" + "byte", "java.lang.Byte" -> "$variableName = $SafeParcelReader.readByte(parcel, header)" + "short", "java.lang.Short" -> "$variableName = $SafeParcelReader.readShort(parcel, header)" + "boolean", "java.lang.Boolean" -> "$variableName = $SafeParcelReader.readBool(parcel, header)" + "long", "java.lang.Long" -> "$variableName = $SafeParcelReader.readLong(parcel, header)" + "float", "java.lang.Float" -> "$variableName = $SafeParcelReader.readFloat(parcel, header)" + "double", "java.lang.Double" -> "$variableName = $SafeParcelReader.readDouble(parcel, header)" + "java.lang.String" -> "$variableName = $SafeParcelReader.readString(parcel, header)" + "android.os.Bundle" -> "$variableName = $SafeParcelReader.readBundle(parcel, header, ${clazz.fullName}.class.getClassLoader())" + "android.os.IBinder" -> "$variableName = $SafeParcelReader.readBinder(parcel, header)" + "java.lang.String[]" -> "$variableName = $SafeParcelReader.readStringArray(parcel, header)" + "byte[]" -> "$variableName = $SafeParcelReader.readByteArray(parcel, header)" + "byte[][]" -> "$variableName = $SafeParcelReader.readByteArrayArray(parcel, header)" + "float[]" -> "$variableName = $SafeParcelReader.readFloatArray(parcel, header)" + "int[]" -> "$variableName = $SafeParcelReader.readIntArray(parcel, header)" + "java.util.List", "java.util.ArrayList" -> when { + !useValueParcel -> "$variableName = $SafeParcelReader.readStringList(parcel, header)" + else -> "$variableName = $SafeParcelReader.readList(parcel, header, String.class.getClassloader())" + } +// "java.util.List", "java.util.ArrayList" -> "$variableName = $SafeParcelReader.readIntegerList(parcel, header)" +// "java.util.List", "java.util.ArrayList" -> "$variableName = $SafeParcelReader.readBooleanList(parcel, header)" +// "java.util.List", "java.util.ArrayList" -> "$variableName = $SafeParcelReader.readLongList(parcel, header)" +// "java.util.List", "java.util.ArrayList" -> "$variableName = $SafeParcelReader.readFloatList(parcel, header)" +// "java.util.List", "java.util.ArrayList" -> "$variableName = $SafeParcelReader.readDoubleList(parcel, header)" + else -> when { + isList && isParcelable && !useValueParcel -> "$variableName = $SafeParcelReader.readParcelableList(parcel, header, $listItemType.CREATOR)" + isArray && isParcelable -> "$variableName = $SafeParcelReader.readParcelableArray(parcel, header, $listItemType.CREATOR)" + isList -> "$variableName = $SafeParcelReader.readList(parcel, header, $listItemType.class.getClassloader())" + isParcelable -> "$variableName = $SafeParcelReader.readParcelable(parcel, header, $type.CREATOR)" + !isList && isIInterface -> "$variableName = $type.Stub.asInterface($SafeParcelReader.readBinder(parcel, header))" + else -> throw UnsupportedOperationException("Field $name in ${clazz.fullName} has unsupported type $type.") + } + } + } + val readVariableFromParcelCase by lazy { "case $id: $readVariableFromParcel; break;" } + val writeVariableToParcel by lazy { + when (type) { + "boolean", "byte", "char", "short", "int", "long", "float", "double", + "java.lang.Boolean", "java.lang.Byte", "java.lang.Char", "java.lang.Short", "java.lang.Integer", "java.lang.Long", "java.lang.Float", "java.lang.Double" -> + "$SafeParcelWriter.write(parcel, $id, $variableName);" + + "java.util.List", "java.util.ArrayList" -> when { + !useValueParcel -> "$SafeParcelWriter.writeStringList(parcel, $id, $variableName, $mayNull);" + else -> "$SafeParcelWriter.write(parcel, $id, $variableName, $mayNull);" + } + + else -> when { + isParcelable -> "$SafeParcelWriter.write(parcel, $id, $variableName, flags, $mayNull);" + isIInterface -> "$SafeParcelWriter.write(parcel, $id, $variableName.asBinder(), $mayNull);" + else -> "$SafeParcelWriter.write(parcel, $id, $variableName, $mayNull);" + } + } + } + + val reflectionFieldName by lazy { "_${name}\$field" } + val reflectionFieldGetter by lazy { + when (type) { + "boolean" -> "$reflectionFieldName.getBoolean" + "byte" -> "$reflectionFieldName.getByte" + "char" -> "$reflectionFieldName.getChar" + "short" -> "$reflectionFieldName.getShort" + "int" -> "$reflectionFieldName.getInt" + "long" -> "$reflectionFieldName.getLong" + "float" -> "$reflectionFieldName.getFloat" + "double" -> "$reflectionFieldName.getDouble" + else -> "(${type}) $reflectionFieldName.get" + } + } + val reflectionFieldSetter by lazy { + when (type) { + "boolean" -> "$reflectionFieldName.setBoolean" + "byte" -> "$reflectionFieldName.setByte" + "char" -> "$reflectionFieldName.setChar" + "short" -> "$reflectionFieldName.setShort" + "int" -> "$reflectionFieldName.setInt" + "long" -> "$reflectionFieldName.setLong" + "float" -> "$reflectionFieldName.setFloat" + "double" -> "$reflectionFieldName.setDouble" + else -> "$reflectionFieldName.set" + } + } + + val setVariableFromField by lazy { + when { + getter != null -> listOf("$variableName = $getter;") + isPrivate -> listOf( + "$Field $reflectionFieldName = ${clazz.fullName}.class.getDeclaredField(\"$name\");", + "$reflectionFieldName.setAccessible(true);", + "$variableName = $reflectionFieldGetter(object);" + ) + + else -> listOf("$variableName = object.$name;") + } + } + val setFieldFromVariable by lazy { + if (isPrivate) { + listOf( + "$Field $reflectionFieldName = ${clazz.fullName}.class.getDeclaredField(\"$name\");", + "$reflectionFieldName.setAccessible(true);", + "$reflectionFieldSetter(object, $variableName);" + ) + } else { + listOf("object.$name = $variableName;") + } + } +} \ No newline at end of file From f0b742d8da60e1608d0e15aeeded810d4a550c5d Mon Sep 17 00:00:00 2001 From: Marvin W Date: Thu, 4 Jan 2024 22:02:34 +0100 Subject: [PATCH 050/209] Location: Add support for Hotsplots API in agilis trains --- .../main/res/xml/network_security_config.xml | 9 + .../location/network/wifi/MovingWifiHelper.kt | 342 ++++++++++++++++++ 2 files changed, 351 insertions(+) create mode 100644 play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/MovingWifiHelper.kt diff --git a/play-services-core/src/main/res/xml/network_security_config.xml b/play-services-core/src/main/res/xml/network_security_config.xml index 4dac2bac..76adbb44 100644 --- a/play-services-core/src/main/res/xml/network_security_config.xml +++ b/play-services-core/src/main/res/xml/network_security_config.xml @@ -1,5 +1,6 @@ +<<<<<<< HEAD @@ -11,4 +12,12 @@ +======= + + + portal.mav.hu + cdwifi.cz + hsp.hotsplots.net + +>>>>>>> ac9387ca (Location: Add support for Hotsplots API in agilis trains) \ No newline at end of file diff --git a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/MovingWifiHelper.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/MovingWifiHelper.kt new file mode 100644 index 00000000..5d5397f8 --- /dev/null +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/MovingWifiHelper.kt @@ -0,0 +1,342 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.network.wifi + +import android.content.Context +import android.location.Location +import android.net.ConnectivityManager +import android.net.ConnectivityManager.TYPE_WIFI +import android.os.Build.VERSION.SDK_INT +import android.util.Log +import androidx.core.content.getSystemService +import androidx.core.location.LocationCompat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.json.JSONArray +import org.json.JSONObject +import java.net.HttpURLConnection +import java.net.URL +import java.text.SimpleDateFormat +import java.util.* + +private val MOVING_WIFI_HOTSPOTS = setOf( + // Austria + "OEBB", + "Austrian Flynet", + // Belgium + "THALYSNET", + // Canada + "Air Canada", + "ACWiFi", + // Czech Republic + "CDWiFi", + // France + "_SNCF_WIFI_INOUI", + "_SNCF_WIFI_INTERCITES", + "_WIFI_LYRIA", + "OUIFI", + "NormandieTrainConnecte", + // Germany + "WIFIonICE", + "WIFI@DB", + "WiFi@DB", + "RRX Hotspot", + "FlixBux", + "FlixBus Wi-Fi", + "FlixTrain Wi-Fi", + "FlyNet", + "Telekom_FlyNet", + "Vestische WLAN", + "agilis-Wifi", + // Greece + "AegeanWiFi", + // Hong Kong + "Cathay Pacific", + // Hungary + "MAVSTART-WIFI", + // Netherlands + "KEOLIS Nederland", + // New Zealand + "AirNZ_InflightWiFi", + "Bluebridge WiFi", + // Singapore + "KrisWorld", + // Sweden + "SJ", + // Switzerland + "SBB-Free", + "SWISS Connect", + "Edelweiss Entertainment", + // United Kingdom + "CrossCountryWiFi", + "GWR WiFi", + // United States + "Amtrak_WiFi", +) + +private val PHONE_HOTSPOT_KEYWORDS = setOf( + "iPhone", + "Galaxy", + "AndroidAP" +) + +/** + * A Wi-Fi hotspot that changes its location dynamically and thus is unsuitable for use with location services that assume stable locations. + * + * Some moving Wi-Fi hotspots allow to determine their location when connected or through a public network API. + */ +val WifiDetails.isMoving: Boolean + get() { + if (open && MOVING_WIFI_HOTSPOTS.contains(ssid)) { + return true + } + if (PHONE_HOTSPOT_KEYWORDS.any { ssid?.contains(it) == true }) { + return true + } + return false + } + +const val FEET_TO_METERS = 0.3048 +const val KNOTS_TO_METERS_PER_SECOND = 0.5144 +const val MILES_PER_HOUR_TO_METERS_PER_SECOND = 0.447 + +class MovingWifiHelper(private val context: Context) { + suspend fun retrieveMovingLocation(current: WifiDetails): Location { + if (!isLocallyRetrievable(current)) throw IllegalArgumentException() + val connectivityManager = context.getSystemService() ?: throw IllegalStateException() + val url = URL(MOVING_WIFI_HOTSPOTS_LOCALLY_RETRIEVABLE[current.ssid]) + return withContext(Dispatchers.IO) { + val network = if (isLocallyRetrievable(current) && SDK_INT >= 23) { + @Suppress("DEPRECATION") + (connectivityManager.allNetworks.singleOrNull { + val networkInfo = connectivityManager.getNetworkInfo(it) + Log.d(org.microg.gms.location.network.TAG, "Network info: $networkInfo") + networkInfo?.type == TYPE_WIFI && networkInfo.isConnected + }) + } else { + null + } + val connection = (if (SDK_INT >= 21) { + network?.openConnection(url) + } else { + null + } ?: url.openConnection()) as HttpURLConnection + try { + connection.doInput = true + if (connection.responseCode != 200) throw RuntimeException("Got error") + parseInput(current.ssid!!, connection.inputStream.readBytes()) + } finally { + connection.inputStream.close() + connection.disconnect() + } + } + } + + private fun parseWifiOnIce(location: Location, data: ByteArray): Location { + val json = JSONObject(data.decodeToString()) + if (json.getString("gpsStatus") != "VALID") throw RuntimeException("GPS not valid") + location.accuracy = 100f + location.time = json.getLong("serverTime") - 15000L + location.latitude = json.getDouble("latitude") + location.longitude = json.getDouble("longitude") + json.optDouble("speed").takeIf { !it.isNaN() }?.let { + location.speed = (it / 3.6).toFloat() + LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f) + } + return location + } + + private fun parseOebb(location: Location, data: ByteArray): Location { + val json = JSONObject(data.decodeToString()).getJSONObject("latestStatus") + location.accuracy = 100f + runCatching { SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US).parse(json.getString("dateTime"))?.time }.getOrNull()?.let { location.time = it } + location.latitude = json.getJSONObject("gpsPosition").getDouble("latitude") + location.longitude = json.getJSONObject("gpsPosition").getDouble("longitude") + json.getJSONObject("gpsPosition").optDouble("orientation").takeIf { !it.isNaN() }?.let { + location.bearing = it.toFloat() + LocationCompat.setBearingAccuracyDegrees(location, 90f) + } + json.optDouble("speed").takeIf { !it.isNaN() }?.let { + location.speed = (it / 3.6).toFloat() + LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f) + } + return location + } + + private fun parseFlixbus(location: Location, data: ByteArray): Location { + val json = JSONObject(data.decodeToString()) + location.accuracy = 100f + location.latitude = json.getDouble("latitude") + location.longitude = json.getDouble("longitude") + json.optDouble("speed").takeIf { !it.isNaN() }?.let { + location.speed = it.toFloat() + LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f) + } + return location + } + + private fun parsePassengera(location: Location, data: ByteArray): Location { + val json = JSONObject(data.decodeToString()) + location.accuracy = 100f + location.latitude = json.getDouble("gpsLat") + location.longitude = json.getDouble("gpsLng") + json.optDouble("speed").takeIf { !it.isNaN() }?.let { + location.speed = (it / 3.6).toFloat() + LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f) + } + json.optDouble("altitude").takeIf { !it.isNaN() }?.let { location.altitude = it } + return location + } + + private fun parseDisplayUgo(location: Location, data: ByteArray): Location { + val json = JSONArray(data.decodeToString()).getJSONObject(0) + location.accuracy = 100f + location.latitude = json.getDouble("latitude") + location.longitude = json.getDouble("longitude") + runCatching { SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US).parse(json.getString("created_at"))?.time }.getOrNull()?.let { location.time = it } + json.optDouble("speed_kilometers_per_hour").takeIf { !it.isNaN() }?.let { + location.speed = (it / 3.6).toFloat() + LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f) + } + json.optDouble("altitude_meters").takeIf { !it.isNaN() }?.let { location.altitude = it } + json.optDouble("bearing_in_degree").takeIf { !it.isNaN() }?.let { + location.bearing = it.toFloat() + LocationCompat.setBearingAccuracyDegrees(location, 90f) + } + return location + } + + private fun parsePanasonic(location: Location, data: ByteArray): Location { + val json = JSONObject(data.decodeToString()) + location.accuracy = 100f + location.latitude = json.getJSONObject("current_coordinates").getDouble("latitude") + location.longitude = json.getJSONObject("current_coordinates").getDouble("longitude") + json.optDouble("ground_speed_knots").takeIf { !it.isNaN() }?.let { + location.speed = (it * KNOTS_TO_METERS_PER_SECOND).toFloat() + LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f) + } + json.optDouble("altitude_feet").takeIf { !it.isNaN() }?.let { location.altitude = it * FEET_TO_METERS } + json.optDouble("true_heading_degree").takeIf { !it.isNaN() }?.let { + location.bearing = it.toFloat() + LocationCompat.setBearingAccuracyDegrees(location, 90f) + } + return location + } + + private fun parseBoardConnect(location: Location, data: ByteArray): Location { + val json = JSONObject(data.decodeToString()) + location.accuracy = 100f + location.latitude = json.getDouble("lat") + location.longitude = json.getDouble("lon") + runCatching { SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US).parse(json.getString("utc"))?.time }.getOrNull()?.let { location.time = it } + json.optDouble("groundSpeed").takeIf { !it.isNaN() }?.let { + location.speed = (it * KNOTS_TO_METERS_PER_SECOND).toFloat() + LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f) + } + json.optDouble("altitude").takeIf { !it.isNaN() }?.let { location.altitude = it * FEET_TO_METERS } + json.optDouble("heading").takeIf { !it.isNaN() }?.let { + location.bearing = it.toFloat() + LocationCompat.setBearingAccuracyDegrees(location, 90f) + } + return location + } + + private fun parseSncf(location: Location, data: ByteArray): Location { + val json = JSONObject(data.decodeToString()) + if(json.getInt("fix") == -1) throw RuntimeException("GPS not valid") + location.accuracy = 100f + location.latitude = json.getDouble("latitude") + location.longitude = json.getDouble("longitude") + json.optDouble("speed").takeIf { !it.isNaN() }?.let { + location.speed = it.toFloat() + LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f) + } + location.time = json.getLong("timestamp") + json.optDouble("heading").takeIf { !it.isNaN() }?.let { + location.bearing = it.toFloat() + LocationCompat.setBearingAccuracyDegrees(location, 90f) + } + return location + } + + private fun parseAirCanada(location: Location, data: ByteArray): Location { + val json = JSONObject(data.decodeToString()).getJSONObject("gpsData") + location.accuracy = 100f + location.latitude = json.getDouble("latitude") + location.longitude = json.getDouble("longitude") + json.optLong("utcTime").takeIf { it != 0L }?.let { location.time = it } + json.optDouble("altitude").takeIf { !it.isNaN() }?.let { location.altitude = it * FEET_TO_METERS } + json.optDouble("horizontalVelocity").takeIf { !it.isNaN() }?.let { + location.speed = (it * MILES_PER_HOUR_TO_METERS_PER_SECOND).toFloat() + LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f) + } + return location + } + + private fun parseHotsplots(location: Location, data: ByteArray): Location { + val json = JSONObject(data.decodeToString()) + location.accuracy = 100f + location.latitude = json.getDouble("lat") + location.longitude = json.getDouble("lng") + json.optLong("ts").takeIf { it != 0L }?.let { location.time = it * 1000 } + json.optDouble("speed").takeIf { !it.isNaN() }?.let { + location.speed = it.toFloat() + LocationCompat.setSpeedAccuracyMetersPerSecond(location, location.speed * 0.1f) + } + return location + } + + private fun parseInput(ssid: String, data: ByteArray): Location { + val location = Location(ssid) + return when (ssid) { + "WIFIonICE" -> parseWifiOnIce(location, data) + "OEBB" -> parseOebb(location, data) + "FlixBus", "FlixBus Wi-Fi", "FlixTrain Wi-Fi" -> parseFlixbus(location, data) + "CDWiFi", "MAVSTART-WIFI" -> parsePassengera(location, data) + "AegeanWiFi" -> parseDisplayUgo(location, data) + "Cathay Pacific", "Telekom_FlyNet", "KrisWorld", "SWISS Connect", "Edelweiss Entertainment" -> parsePanasonic(location, data) + "FlyNet" -> parseBoardConnect(location, data) + "ACWiFi" -> parseAirCanada(location, data) + "OUIFI" -> parseSncf(location, data) + "_SNCF_WIFI_INOUI" -> parseSncf(location, data) + "_SNCF_WIFI_INTERCITES" -> parseSncf(location, data) + "_WIFI_LYRIA" -> parseSncf(location, data) + "NormandieTrainConnecte" -> parseSncf(location, data) + "agilis-Wifi" -> parseHotsplots(location, data) + else -> throw UnsupportedOperationException() + } + } + + fun isLocallyRetrievable(wifi: WifiDetails): Boolean = + MOVING_WIFI_HOTSPOTS_LOCALLY_RETRIEVABLE.containsKey(wifi.ssid) + + companion object { + private val MOVING_WIFI_HOTSPOTS_LOCALLY_RETRIEVABLE = mapOf( + "WIFIonICE" to "https://iceportal.de/api1/rs/status", + "OEBB" to "https://railnet.oebb.at/assets/modules/fis/combined.json", + "FlixBus" to "https://media.flixbus.com/services/pis/v1/position", + "FlixBus Wi-Fi" to "https://media.flixbus.com/services/pis/v1/position", + "FlixTrain Wi-Fi" to "https://media.flixbus.com/services/pis/v1/position", + "MAVSTART-WIFI" to "http://portal.mav.hu/portal/api/vehicle/realtime", + "AegeanWiFi" to "https://api.ife.ugo.aero/navigation/positions", + "Telekom_FlyNet" to "https://services.inflightpanasonic.aero/inflight/services/flightdata/v2/flightdata", + "Cathay Pacific" to "https://services.inflightpanasonic.aero/inflight/services/flightdata/v2/flightdata", + "KrisWorld" to "https://services.inflightpanasonic.aero/inflight/services/flightdata/v2/flightdata", + "SWISS Connect" to "https://services.inflightpanasonic.aero/inflight/services/flightdata/v2/flightdata", + "Edelweiss Entertainment" to "https://services.inflightpanasonic.aero/inflight/services/flightdata/v2/flightdata", + "FlyNet" to "https://ww2.lufthansa-flynet.com/map/api/flightData", + "CDWiFi" to "http://cdwifi.cz/portal/api/vehicle/realtime", + "ACWiFi" to "https://airbornemedia.inflightinternet.com/asp/api/flight/info", + "OUIFI" to "https://ouifi.ouigo.com:8084/api/gps", + "_SNCF_WIFI_INOUI" to "https://wifi.sncf/router/api/train/gps", + "_SNCF_WIFI_INTERCITES" to "https://wifi.intercites.sncf/router/api/train/gps", + "_WIFI_LYRIA" to "https://wifi.tgv-lyria.com/router/api/train/gps", + "NormandieTrainConnecte" to "https://wifi.normandie.fr/router/api/train/gps", + "agilis-Wifi" to "http://hsp.hotsplots.net/status.json", + ) + } +} + From 8f86e4da1f76f60ecab6efd1179fce1452f91b0c Mon Sep 17 00:00:00 2001 From: Marvin W Date: Fri, 16 Feb 2024 19:23:39 +0100 Subject: [PATCH 051/209] Location: Add svciob to moving wifi list --- .../org/microg/gms/location/network/wifi/MovingWifiHelper.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/MovingWifiHelper.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/MovingWifiHelper.kt index 5d5397f8..04adf1fb 100644 --- a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/MovingWifiHelper.kt +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/MovingWifiHelper.kt @@ -26,6 +26,7 @@ private val MOVING_WIFI_HOTSPOTS = setOf( // Austria "OEBB", "Austrian Flynet", + "svciob", // OEBB Service WIFI // Belgium "THALYSNET", // Canada From 7b5b7301f3673cfd6b453794d96edd097e862963 Mon Sep 17 00:00:00 2001 From: Marvin W Date: Fri, 16 Feb 2024 20:00:09 +0100 Subject: [PATCH 052/209] Only use a single Volley instance per process --- .../firebase/auth/IdentityToolkitClient.kt | 148 +++++++++ .../gms/appinivite/AppInviteActivity.kt | 185 +++++++++++ .../microg/gms/utils/SingleInstanceUtil.kt | 42 +++ .../gms/potokens/utils/PoTokenHelper.kt | 3 +- .../microg/gms/auth/appcert/AppCertManager.kt | 8 +- .../gms/auth/signin/AuthSignInService.kt | 156 +++++++++ .../kotlin/org/microg/gms/games/extensions.kt | 268 +++++++++++++++ .../org/microg/gms/ui/SafetyNetFragment.kt | 311 ++++++++++++++++++ .../core/DroidGuardResultCreator.kt | 62 ++++ .../gms/droidguard/core/HandleProxyFactory.kt | 226 +++++++++++++ .../microg/gms/fido/core/RequestHandling.kt | 262 +++++++++++++++ .../mozilla/MozillaLocationServiceClient.kt | 89 +++++ .../OpenStreetMapNominatimGeocodeProvider.kt | 188 +++++++++++ .../gms/recaptcha/RecaptchaGuardImpl.kt | 132 ++++++++ 14 files changed, 2078 insertions(+), 2 deletions(-) create mode 100644 firebase-auth/core/src/main/kotlin/org/microg/gms/firebase/auth/IdentityToolkitClient.kt create mode 100644 play-services-appinvite/core/src/main/kotlin/org/microg/gms/appinivite/AppInviteActivity.kt create mode 100644 play-services-base-core/src/main/kotlin/org/microg/gms/utils/SingleInstanceUtil.kt create mode 100644 play-services-core/src/main/kotlin/org/microg/gms/auth/signin/AuthSignInService.kt create mode 100644 play-services-core/src/main/kotlin/org/microg/gms/games/extensions.kt create mode 100644 play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetFragment.kt create mode 100644 play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/DroidGuardResultCreator.kt create mode 100644 play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/HandleProxyFactory.kt create mode 100644 play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/RequestHandling.kt create mode 100644 play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/mozilla/MozillaLocationServiceClient.kt create mode 100644 play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/OpenStreetMapNominatimGeocodeProvider.kt create mode 100644 play-services-recaptcha/core/src/main/kotlin/org/microg/gms/recaptcha/RecaptchaGuardImpl.kt diff --git a/firebase-auth/core/src/main/kotlin/org/microg/gms/firebase/auth/IdentityToolkitClient.kt b/firebase-auth/core/src/main/kotlin/org/microg/gms/firebase/auth/IdentityToolkitClient.kt new file mode 100644 index 00000000..2b8608e1 --- /dev/null +++ b/firebase-auth/core/src/main/kotlin/org/microg/gms/firebase/auth/IdentityToolkitClient.kt @@ -0,0 +1,148 @@ +/* + * SPDX-FileCopyrightText: 2020, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.firebase.auth + +import android.content.Context +import android.util.Log +import com.android.volley.NetworkResponse +import com.android.volley.ParseError +import com.android.volley.Request.Method.GET +import com.android.volley.Request.Method.POST +import com.android.volley.Response +import com.android.volley.VolleyError +import com.android.volley.toolbox.HttpHeaderParser +import com.android.volley.toolbox.JsonObjectRequest +import com.android.volley.toolbox.JsonRequest +import com.android.volley.toolbox.Volley +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject +import org.microg.gms.utils.singleInstanceOf +import org.microg.gms.utils.toHexString +import java.io.UnsupportedEncodingException +import java.lang.RuntimeException +import java.nio.charset.Charset +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine + +private const val TAG = "GmsFirebaseAuthClient" + +class IdentityToolkitClient(context: Context, private val apiKey: String, private val packageName: String? = null, private val certSha1Hash: ByteArray? = null) { + private val queue = singleInstanceOf { Volley.newRequestQueue(context.applicationContext) } + + private fun buildRelyingPartyUrl(method: String) = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/$method?key=$apiKey" + private fun buildStsUrl(method: String) = "https://securetoken.googleapis.com/v1/$method?key=$apiKey" + + private fun getRequestHeaders(): Map = hashMapOf().apply { + if (packageName != null) put("X-Android-Package", packageName) + if (certSha1Hash != null) put("X-Android-Cert", certSha1Hash.toHexString().uppercase()) + } + + private suspend fun request(method: String, data: JSONObject): JSONObject = suspendCoroutine { continuation -> + queue.add(object : JsonObjectRequest(POST, buildRelyingPartyUrl(method), data, { + continuation.resume(it) + }, { + Log.d(TAG, "Error: ${it.networkResponse?.data?.decodeToString() ?: it.message}") + continuation.resumeWithException(RuntimeException(it)) + }) { + override fun getHeaders(): Map = getRequestHeaders() + }) + } + + suspend fun createAuthUri(identifier: String? = null, tenantId: String? = null, continueUri: String? = "http://localhost"): JSONObject = + request("createAuthUri", JSONObject() + .put("identifier", identifier) + .put("tenantId", tenantId) + .put("continueUri", continueUri)) + + suspend fun getAccountInfo(idToken: String? = null): JSONObject = + request("getAccountInfo", JSONObject() + .put("idToken", idToken)) + + suspend fun getProjectConfig(): JSONObject = suspendCoroutine { continuation -> + queue.add(JsonObjectRequest(GET, buildRelyingPartyUrl("getProjectConfig"), null, { continuation.resume(it) }, { continuation.resumeWithException(RuntimeException(it)) })) + } + + suspend fun getOobConfirmationCode(requestType: String, email: String? = null, newEmail: String? = null, continueUrl: String? = null, idToken: String? = null, iOSBundleId: String? = null, iOSAppStoreId: String? = null, androidMinimumVersion: String? = null, androidInstallApp: Boolean? = null, androidPackageName: String? = null, canHandleCodeInApp: Boolean? = null): JSONObject = + request("getOobConfirmationCode", JSONObject() + .put("kind", "identitytoolkit#relyingparty") + .put("requestType", requestType) + .put("email", email) + .put("newEmail", newEmail) + .put("continueUrl", continueUrl) + .put("idToken", idToken) + .put("iOSBundleId", iOSBundleId) + .put("iOSAppStoreId", iOSAppStoreId) + .put("androidMinimumVersion", androidMinimumVersion) + .put("androidInstallApp", androidInstallApp) + .put("androidPackageName", androidPackageName) + .put("canHandleCodeInApp", canHandleCodeInApp)) + + + suspend fun sendVerificationCode(phoneNumber: String? = null, reCaptchaToken: String? = null): JSONObject = + request("sendVerificationCode", JSONObject() + .put("phoneNumber", phoneNumber) + .put("recaptchaToken", reCaptchaToken)) + + suspend fun setAccountInfo(idToken: String? = null, localId: String? = null, email: String? = null, password: String? = null, displayName: String? = null, photoUrl: String? = null, deleteAttribute: List = emptyList()): JSONObject = + request("setAccountInfo", JSONObject() + .put("idToken", idToken) + .put("localId", localId) + .put("email", email) + .put("password", password) + .put("displayName", displayName) + .put("photoUrl", photoUrl) + .put("deleteAttribute", JSONArray().apply { deleteAttribute.map { put(it) } })) + + suspend fun signupNewUser(email: String? = null, password: String? = null, tenantId: String? = null): JSONObject = + request("signupNewUser", JSONObject() + .put("email", email) + .put("password", password) + .put("tenantId", tenantId)) + + suspend fun verifyCustomToken(token: String? = null, returnSecureToken: Boolean = true): JSONObject = + request("verifyCustomToken", JSONObject() + .put("token", token) + .put("returnSecureToken", returnSecureToken)) + + suspend fun verifyPassword(email: String? = null, password: String? = null, tenantId: String? = null, returnSecureToken: Boolean = true): JSONObject = + request("verifyPassword", JSONObject() + .put("email", email) + .put("password", password) + .put("tenantId", tenantId) + .put("returnSecureToken", returnSecureToken)) + + suspend fun verifyPhoneNumber(phoneNumber: String? = null, sessionInfo: String? = null, code: String? = null, idToken: String? = null, verificationProof: String? = null, temporaryProof: String? = null): JSONObject = + request("verifyPhoneNumber", JSONObject() + .put("verificationProof", verificationProof) + .put("code", code) + .put("idToken", idToken) + .put("temporaryProof", temporaryProof) + .put("phoneNumber", phoneNumber) + .put("sessionInfo", sessionInfo)) + + suspend fun getTokenByRefreshToken(refreshToken: String): JSONObject = suspendCoroutine { continuation -> + queue.add(object : JsonRequest(POST, buildStsUrl("token"), "grant_type=refresh_token&refresh_token=$refreshToken", { continuation.resume(it) }, { continuation.resumeWithException(RuntimeException(it)) }) { + override fun parseNetworkResponse(response: NetworkResponse?): Response { + return try { + val jsonString = String(response!!.data, Charset.forName(HttpHeaderParser.parseCharset(response!!.headers, PROTOCOL_CHARSET))) + Response.success(JSONObject(jsonString), HttpHeaderParser.parseCacheHeaders(response)) + } catch (e: UnsupportedEncodingException) { + Response.error(ParseError(e)) + } catch (je: JSONException) { + Response.error(ParseError(je)) + } + } + + override fun getBodyContentType(): String { + return "application/x-www-form-urlencoded" + } + + override fun getHeaders(): Map = getRequestHeaders() + }) + } +} \ No newline at end of file diff --git a/play-services-appinvite/core/src/main/kotlin/org/microg/gms/appinivite/AppInviteActivity.kt b/play-services-appinvite/core/src/main/kotlin/org/microg/gms/appinivite/AppInviteActivity.kt new file mode 100644 index 00000000..526f10b1 --- /dev/null +++ b/play-services-appinvite/core/src/main/kotlin/org/microg/gms/appinivite/AppInviteActivity.kt @@ -0,0 +1,185 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.appinivite + +import android.content.Intent +import android.net.Uri +import android.os.Build.VERSION.SDK_INT +import android.os.Bundle +import android.os.LocaleList +import android.util.Log +import android.view.ViewGroup +import android.view.Window +import android.widget.ProgressBar +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.pm.PackageInfoCompat +import androidx.core.os.bundleOf +import androidx.core.view.setPadding +import androidx.lifecycle.lifecycleScope +import com.android.volley.* +import com.android.volley.toolbox.Volley +import com.squareup.wire.Message +import com.squareup.wire.ProtoAdapter +import kotlinx.coroutines.CompletableDeferred +import okio.ByteString.Companion.decodeHex +import org.microg.gms.appinvite.* +import org.microg.gms.common.Constants +import org.microg.gms.utils.singleInstanceOf +import java.util.* + +private const val TAG = "AppInviteActivity" + +private const val APPINVITE_DEEP_LINK = "com.google.android.gms.appinvite.DEEP_LINK" +private const val APPINVITE_INVITATION_ID = "com.google.android.gms.appinvite.INVITATION_ID" +private const val APPINVITE_OPENED_FROM_PLAY_STORE = "com.google.android.gms.appinvite.OPENED_FROM_PLAY_STORE" +private const val APPINVITE_REFERRAL_BUNDLE = "com.google.android.gms.appinvite.REFERRAL_BUNDLE" + +class AppInviteActivity : AppCompatActivity() { + private val queue by lazy { singleInstanceOf { Volley.newRequestQueue(applicationContext) } } + + private val Int.px: Int get() = (this * resources.displayMetrics.density).toInt() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + requestWindowFeature(Window.FEATURE_NO_TITLE) + setContentView(ProgressBar(this).apply { + layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT) + setPadding(20.px) + isIndeterminate = true + }) + val extras = intent.extras + extras?.keySet() + Log.d(TAG, "Intent: $intent $extras") + if (intent?.data == null) return finish() + lifecycleScope.launchWhenStarted { run() } + } + + private fun redirectToBrowser() { + try { + startActivity(Intent(Intent.ACTION_VIEW).apply { + addCategory(Intent.CATEGORY_DEFAULT) + data = intent.data + }) + } catch (e: Exception) { + Log.w(TAG, e) + } + finish() + } + + private fun open(appInviteLink: MutateAppInviteLinkResponse) { + val intent = Intent(Intent.ACTION_VIEW).apply { + addCategory(Intent.CATEGORY_DEFAULT) + data = appInviteLink.data_?.intentData?.let { Uri.parse(it) } + `package` = appInviteLink.data_?.packageName + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP + putExtra( + APPINVITE_REFERRAL_BUNDLE, bundleOf( + APPINVITE_DEEP_LINK to appInviteLink, + APPINVITE_INVITATION_ID to "", + APPINVITE_OPENED_FROM_PLAY_STORE to false + ) + ) + } + val fallbackIntent = Intent(Intent.ACTION_VIEW).apply { + addCategory(Intent.CATEGORY_DEFAULT) + data = appInviteLink.data_?.fallbackUrl?.let { Uri.parse(it) } + } + val installedVersionCode = runCatching { + intent.resolveActivity(packageManager)?.let { + PackageInfoCompat.getLongVersionCode(packageManager.getPackageInfo(it.packageName, 0)) + } + }.getOrNull() + if (installedVersionCode != null && (appInviteLink.data_?.app?.minAppVersion == null || installedVersionCode >= appInviteLink.data_.app.minAppVersion)) { + startActivity(intent) + finish() + } else { + try { + startActivity(fallbackIntent) + } catch (e: Exception) { + Log.w(TAG, e) + } + finish() + } + } + + private suspend fun run() { + val request = ProtobufPostRequest( + "https://datamixer-pa.googleapis.com/v1/mutateonekey?alt=proto&key=AIzaSyAP-gfH3qvi6vgHZbSYwQ_XHqV_mXHhzIk", MutateOperation( + id = MutateOperationId.AppInviteLink, + mutateRequest = MutateDataRequest( + appInviteLink = MutateAppInviteLinkRequest( + client = ClientIdInfo( + packageName = Constants.GMS_PACKAGE_NAME, + signature = Constants.GMS_PACKAGE_SIGNATURE_SHA1.decodeHex().base64(), + language = Locale.getDefault().language + ), + link = LinkInfo( + empty = "", + uri = intent.data.toString() + ), + system = SystemInfo( + gms = SystemInfo.GmsInfo( + versionCode = Constants.GMS_VERSION_CODE + ) + ) + ) + ) + ), MutateDataResponseWithError.ADAPTER + ) + val response = try { + request.sendAndAwait(queue) + } catch (e: Exception) { + Log.w(TAG, e) + return redirectToBrowser() + } + if (response.errorStatus != null || response.dataResponse?.appInviteLink == null) return redirectToBrowser() + open(response.dataResponse.appInviteLink) + } +} + +class ProtobufPostRequest, O>(url: String, private val i: I, private val oAdapter: ProtoAdapter) : + Request(Method.POST, url, null) { + private val deferred = CompletableDeferred() + + override fun getHeaders(): Map { + val headers = HashMap(super.getHeaders()) + headers["Accept-Language"] = if (SDK_INT >= 24) LocaleList.getDefault().toLanguageTags() else Locale.getDefault().language + headers["X-Android-Package"] = Constants.GMS_PACKAGE_NAME + headers["X-Android-Cert"] = Constants.GMS_PACKAGE_SIGNATURE_SHA1 + return headers + } + + override fun getBody(): ByteArray = i.encode() + + override fun getBodyContentType(): String = "application/x-protobuf" + + override fun parseNetworkResponse(response: NetworkResponse): Response { + try { + return Response.success(oAdapter.decode(response.data), null) + } catch (e: VolleyError) { + return Response.error(e) + } catch (e: Exception) { + return Response.error(VolleyError()) + } + } + + override fun deliverResponse(response: O) { + Log.d(TAG, "Got response: $response") + deferred.complete(response) + } + + override fun deliverError(error: VolleyError) { + deferred.completeExceptionally(error) + } + + suspend fun await(): O = deferred.await() + + suspend fun sendAndAwait(queue: RequestQueue): O { + Log.d(TAG, "Sending request: $i") + queue.add(this) + return await() + } +} \ No newline at end of file diff --git a/play-services-base-core/src/main/kotlin/org/microg/gms/utils/SingleInstanceUtil.kt b/play-services-base-core/src/main/kotlin/org/microg/gms/utils/SingleInstanceUtil.kt new file mode 100644 index 00000000..0b5dc89d --- /dev/null +++ b/play-services-base-core/src/main/kotlin/org/microg/gms/utils/SingleInstanceUtil.kt @@ -0,0 +1,42 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.utils + +private val singleInstanceLock = Any() +private val singleInstanceMap: MutableMap, Any> = hashMapOf() + +fun singleInstanceOf(tClass: Class, tCreator: () -> T): T { + val tVolatileItem = singleInstanceMap[tClass] + @Suppress("UNCHECKED_CAST") + if (tVolatileItem != null && tClass.isAssignableFrom(tVolatileItem.javaClass)) return tVolatileItem as T + val tLock = synchronized(singleInstanceLock) { + val tItem = singleInstanceMap[tClass] + if (tItem != null) { + @Suppress("UNCHECKED_CAST") + if (tClass.isAssignableFrom(tItem.javaClass)) return tItem as T + tItem + } else { + val tLock = Any() + singleInstanceMap[tClass] = tLock + tLock + } + } + synchronized(tLock) { + val tItem = synchronized(singleInstanceMap) { singleInstanceMap[tClass] } + if (tItem == null) throw IllegalStateException() + @Suppress("UNCHECKED_CAST") + if (tClass.isAssignableFrom(tItem.javaClass)) return tItem as T + if (tItem != tLock) throw IllegalStateException() + + val tNewItem = tCreator() + synchronized(singleInstanceMap) { + singleInstanceMap[tClass] = tNewItem + } + return tNewItem + } +} + +inline fun singleInstanceOf(noinline tCreator: () -> T): T = singleInstanceOf(T::class.java, tCreator) \ No newline at end of file diff --git a/play-services-core/src/main/kotlin/com/google/android/gms/potokens/utils/PoTokenHelper.kt b/play-services-core/src/main/kotlin/com/google/android/gms/potokens/utils/PoTokenHelper.kt index ed378145..e9d6634a 100644 --- a/play-services-core/src/main/kotlin/com/google/android/gms/potokens/utils/PoTokenHelper.kt +++ b/play-services-core/src/main/kotlin/com/google/android/gms/potokens/utils/PoTokenHelper.kt @@ -34,6 +34,7 @@ import okio.ByteString.Companion.toByteString import org.microg.gms.common.Constants import org.microg.gms.profile.Build import org.microg.gms.utils.getFirstSignatureDigest +import org.microg.gms.utils.singleInstanceOf import java.nio.ByteBuffer import java.security.SecureRandom import java.util.Random @@ -45,7 +46,7 @@ import kotlin.math.abs class PoTokenHelper(context: Context) { - private val volleyQueue = Volley.newRequestQueue(context) + private val volleyQueue = singleInstanceOf { Volley.newRequestQueue(context.applicationContext) } private fun buildKeySet(): KeySet { val keyId = abs(Random().nextInt()) diff --git a/play-services-core/src/main/kotlin/org/microg/gms/auth/appcert/AppCertManager.kt b/play-services-core/src/main/kotlin/org/microg/gms/auth/appcert/AppCertManager.kt index 3120a555..d5ebaed2 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/auth/appcert/AppCertManager.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/auth/appcert/AppCertManager.kt @@ -28,14 +28,20 @@ import org.microg.gms.gcm.RegisterRequest import org.microg.gms.gcm.completeRegisterRequest import org.microg.gms.profile.Build import org.microg.gms.profile.ProfileManager +<<<<<<< HEAD import org.microg.mgms.settings.SettingsContract.CheckIn import org.microg.mgms.settings.SettingsContract.getSettings +======= +import org.microg.gms.settings.SettingsContract.CheckIn +import org.microg.gms.settings.SettingsContract.getSettings +import org.microg.gms.utils.singleInstanceOf +>>>>>>> d69a0f5a (Only use a single Volley instance per process) import javax.crypto.Mac import javax.crypto.spec.SecretKeySpec import kotlin.random.Random class AppCertManager(private val context: Context) { - private val queue = Volley.newRequestQueue(context) + private val queue = singleInstanceOf { Volley.newRequestQueue(context.applicationContext) } private fun readDeviceKey() { try { diff --git a/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/AuthSignInService.kt b/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/AuthSignInService.kt new file mode 100644 index 00000000..88cac7e3 --- /dev/null +++ b/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/AuthSignInService.kt @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2013-2017 microG Project Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.microg.gms.auth.signin + +import android.accounts.Account +import android.content.Context +import android.os.Bundle +import android.os.Parcel +import android.util.Log +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import com.android.volley.toolbox.JsonObjectRequest +import com.android.volley.toolbox.Volley +import com.google.android.gms.auth.api.signin.GoogleSignInAccount +import com.google.android.gms.auth.api.signin.GoogleSignInOptions +import com.google.android.gms.auth.api.signin.internal.ISignInCallbacks +import com.google.android.gms.auth.api.signin.internal.ISignInService +import com.google.android.gms.common.Feature +import com.google.android.gms.common.api.CommonStatusCodes +import com.google.android.gms.common.api.Scope +import com.google.android.gms.common.api.Status +import com.google.android.gms.common.internal.ConnectionInfo +import com.google.android.gms.common.internal.GetServiceRequest +import com.google.android.gms.common.internal.IGmsCallbacks +import org.microg.gms.BaseService +import org.microg.gms.auth.AuthPrefs +import org.microg.gms.common.GmsService +import org.microg.gms.common.PackageUtils +import org.microg.gms.utils.singleInstanceOf +import org.microg.gms.utils.warnOnTransactionIssues +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine + +private const val TAG = "AuthSignInService" + +class AuthSignInService : BaseService(TAG, GmsService.AUTH_SIGN_IN) { + override fun handleServiceRequest(callback: IGmsCallbacks, request: GetServiceRequest, service: GmsService) { + val packageName = PackageUtils.getAndCheckCallingPackage(this, request.packageName) + ?: throw IllegalArgumentException("Missing package name") + val binder = AuthSignInServiceImpl(this, lifecycle, packageName, request.account, request.scopes.asList(), request.extras).asBinder() + callback.onPostInitCompleteWithConnectionInfo(CommonStatusCodes.SUCCESS, binder, ConnectionInfo().apply { + features = arrayOf(Feature("user_service_account_management", 1)) + }) + } +} + +class AuthSignInServiceImpl( + private val context: Context, + private val lifecycle: Lifecycle, + private val packageName: String, + private val account: Account?, + private val scopes: List, + private val extras: Bundle +) : ISignInService.Stub(), LifecycleOwner { + private val queue = singleInstanceOf { Volley.newRequestQueue(context.applicationContext) } + override fun getLifecycle(): Lifecycle = lifecycle + + override fun silentSignIn(callbacks: ISignInCallbacks, options: GoogleSignInOptions?) { + Log.d(TAG, "$packageName:silentSignIn($options)") + fun sendResult(account: GoogleSignInAccount?, status: Status) { + Log.d(TAG, "Result[$status]: $account") + runCatching { callbacks.onSignIn(account, status) } + } + lifecycleScope.launchWhenStarted { + try { + val account = account ?: options?.account ?: SignInConfigurationService.getDefaultAccount(context, packageName) + if (account != null && options?.isForceCodeForRefreshToken != true) { + if (getOAuthManager(context, packageName, options, account).isPermitted || AuthPrefs.isTrustGooglePermitted(context)) { + val googleSignInAccount = performSignIn(context, packageName, options, account) + if (googleSignInAccount != null) { + sendResult(googleSignInAccount, Status(CommonStatusCodes.SUCCESS)) + } else { + sendResult(null, Status(CommonStatusCodes.DEVELOPER_ERROR)) + } + } else { + sendResult(null, Status(CommonStatusCodes.SIGN_IN_REQUIRED)) + } + } else { + sendResult(null, Status(CommonStatusCodes.SIGN_IN_REQUIRED)) + } + } catch (e: Exception) { + Log.w(TAG, e) + sendResult(null, Status.INTERNAL_ERROR) + } + } + } + + override fun signOut(callbacks: ISignInCallbacks, options: GoogleSignInOptions?) { + Log.d(TAG, "$packageName:signOut($options)") + lifecycleScope.launchWhenStarted { + try { + val account = account ?: options?.account ?: SignInConfigurationService.getDefaultAccount(context, packageName) + if (account != null) performSignOut(context, packageName, options, account) + SignInConfigurationService.setDefaultAccount(context, packageName, null) + runCatching { callbacks.onSignOut(Status.SUCCESS) } + } catch (e: Exception) { + Log.w(TAG, e) + runCatching { callbacks.onSignIn(null, Status.INTERNAL_ERROR) } + } + } + } + + override fun revokeAccess(callbacks: ISignInCallbacks, options: GoogleSignInOptions?) { + Log.d(TAG, "$packageName:revokeAccess($options)") + lifecycleScope.launchWhenStarted { + val account = account ?: options?.account ?: SignInConfigurationService.getDefaultAccount(context, packageName) + if (account != null) { + try { + val authManager = getOAuthManager(context, packageName, options, account) + val token = authManager.peekAuthToken() + if (token != null) { + suspendCoroutine { continuation -> + queue.add(object : JsonObjectRequest( + "https://accounts.google.com/o/oauth2/revoke?token=$token", + { continuation.resume(it) }, + { continuation.resumeWithException(it) }) { + override fun getHeaders(): MutableMap { + return hashMapOf( + "Authorization" to "OAuth $token" + ) + } + }) + } + authManager.invalidateAuthToken(token) + authManager.isPermitted = false + } + SignInConfigurationService.setDefaultAccount(context, packageName, account) + runCatching { callbacks.onRevokeAccess(Status.SUCCESS) } + } catch (e: Exception) { + Log.w(TAG, e) + runCatching { callbacks.onRevokeAccess(Status.INTERNAL_ERROR) } + } + } else { + runCatching { callbacks.onRevokeAccess(Status.SUCCESS) } + } + } + } + + override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean = + warnOnTransactionIssues(code, reply, flags, TAG) { super.onTransact(code, data, reply, flags) } +} \ No newline at end of file diff --git a/play-services-core/src/main/kotlin/org/microg/gms/games/extensions.kt b/play-services-core/src/main/kotlin/org/microg/gms/games/extensions.kt new file mode 100644 index 00000000..dc31f7a6 --- /dev/null +++ b/play-services-core/src/main/kotlin/org/microg/gms/games/extensions.kt @@ -0,0 +1,268 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.games + +import android.accounts.Account +import android.content.ContentValues +import android.content.Context +import android.database.Cursor +import androidx.core.content.contentValuesOf +import androidx.core.net.toUri +import com.android.volley.* +import com.android.volley.Response.success +import com.android.volley.toolbox.JsonObjectRequest +import com.android.volley.toolbox.Volley +import com.google.android.gms.common.Scopes +import com.google.android.gms.common.api.Scope +import com.google.android.gms.games.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.json.JSONObject +import org.microg.gms.auth.AuthManager +import org.microg.gms.common.Constants +import org.microg.gms.common.Utils +import org.microg.gms.settings.SettingsContract.CheckIn +import org.microg.gms.settings.SettingsContract.getSettings +import org.microg.gms.utils.singleInstanceOf +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine + + +const val ACTION_START_1P = "com.google.android.play.games.service.START_1P" +const val ACTION_VIEW_LEADERBOARDS = "com.google.android.gms.games.VIEW_LEADERBOARDS" +const val ACTION_VIEW_ACHIEVEMENTS = "com.google.android.gms.games.VIEW_ACHIEVEMENTS" +const val ACTION_PLAYER_SEARCH = "com.google.android.gms.games.PLAYER_SEARCH" +const val ACTION_VIEW_PROFILE = "com.google.android.gms.games.VIEW_PROFILE" +const val ACTION_ADD_FRIEND = "com.google.android.gms.games.ADD_FRIEND" + +const val EXTRA_GAME_PACKAGE_NAME = "com.google.android.gms.games.GAME_PACKAGE_NAME" +const val EXTRA_GAME_ID = "com.google.android.gms.games.GAME_ID" +const val EXTRA_PLAYER = "com.google.android.gms.games.PLAYER" +const val EXTRA_PLAYER_ID = "com.google.android.gms.games.PLAYER_ID" +const val EXTRA_IS_SELF = "com.google.android.gms.games.IS_SELF" +const val EXTRA_ACCOUNT = "com.google.android.gms.games.ACCOUNT" +const val EXTRA_SCOPES = "com.google.android.gms.games.SCOPES" +const val EXTRA_POPUP_GRAVITY = "com.google.android.gms.games.key.connectingPopupGravity" +const val EXTRA_SELF_IN_GAME_NAME = "com.google.android.gms.games.EXTRA_SELF_IN_GAME_NAME" +const val EXTRA_OTHER_PLAYER_IN_GAME_NAME = "com.google.android.gms.games.EXTRA_OTHER_PLAYER_IN_GAME_NAME" + +const val GAMES_PACKAGE_NAME = "com.google.android.play.games" + +fun PlayerEntity.toContentValues(): ContentValues = contentValuesOf( + PlayerColumns.externalPlayerId to playerId, + PlayerColumns.profileName to displayName, + PlayerColumns.gamerTag to gamerTag, + PlayerColumns.realName to name, + PlayerColumns.profileIconImageUri to iconImageUri?.toString(), + PlayerColumns.profileIconImageUrl to iconImageUrl, + PlayerColumns.profileHiResImageUri to hiResImageUri?.toString(), + PlayerColumns.profileHiResImageUrl to hiResImageUrl, + PlayerColumns.bannerImageLandscapeUri to bannerImageLandscapeUri?.toString(), + PlayerColumns.bannerImageLandscapeUrl to bannerImageLandscapeUrl, + PlayerColumns.bannerImagePortraitUri to bannerImagePortraitUri?.toString(), + PlayerColumns.bannerImagePortraitUrl to bannerImagePortraitUrl, + PlayerColumns.lastUpdated to retrievedTimestamp, + PlayerColumns.isInCircles to isInCircles, + PlayerColumns.playedWithTimestamp to lastPlayedWithTimestamp, + PlayerColumns.playerTitle to title, + PlayerColumns.isProfileVisible to isProfileVisible, + PlayerColumns.hasDebugAccess to hasDebugAccess, + PlayerColumns.gamerFriendStatus to 0, + PlayerColumns.gamerFriendUpdateTimestamp to 0L, + PlayerColumns.isMuted to false, + PlayerColumns.totalUnlockedAchievements to totalUnlockedAchievement, + PlayerColumns.alwaysAutoSignIn to isAlwaysAutoSignIn, + PlayerColumns.hasAllPublicAcls to isProfileVisible, + + PlayerColumns.currentLevel to levelInfo?.currentLevel?.levelNumber, + PlayerColumns.currentLevelMinXp to levelInfo?.currentLevel?.minXp, + PlayerColumns.currentLevelMaxXp to levelInfo?.currentLevel?.maxXp, + PlayerColumns.nextLevel to levelInfo?.nextLevel?.levelNumber, + PlayerColumns.nextLevelMaxXp to levelInfo?.nextLevel?.maxXp, + PlayerColumns.lastLevelUpTimestamp to (levelInfo?.lastLevelUpTimestamp ?: -1), + PlayerColumns.currentXpTotal to (levelInfo?.currentXpTotal ?: -1L), + + PlayerColumns.mostRecentExternalGameId to mostRecentGameInfo?.gameId, + PlayerColumns.mostRecentGameName to mostRecentGameInfo?.gameName, + PlayerColumns.mostRecentActivityTimestamp to mostRecentGameInfo?.activityTimestampMillis, + PlayerColumns.mostRecentGameIconUri to mostRecentGameInfo?.gameIconImageUri?.toString(), + PlayerColumns.mostRecentGameHiResUri to mostRecentGameInfo?.gameHiResImageUri?.toString(), + PlayerColumns.mostRecentGameFeaturedUri to mostRecentGameInfo?.gameFeaturedImageUri?.toString(), + + PlayerColumns.playTogetherFriendStatus to relationshipInfo?.friendStatus, + PlayerColumns.playTogetherNickname to (relationshipInfo as? PlayerRelationshipInfoEntity)?.nickname, + PlayerColumns.playTogetherInvitationNickname to (relationshipInfo as? PlayerRelationshipInfoEntity)?.invitationNickname, + PlayerColumns.nicknameAbuseReportToken to (relationshipInfo as? PlayerRelationshipInfoEntity)?.nicknameAbuseReportToken, + + PlayerColumns.friendsListVisibility to currentPlayerInfo?.friendsListVisibilityStatus +) + +fun JSONObject.toPlayer() = PlayerEntity( + getString("playerId"), + getString("displayName"), + optString("avatarImageUrl").takeIf { it.isNotBlank() }?.toUri(), + optString("avatarImageUrl").takeIf { it.isNotBlank() }?.toUri(), + System.currentTimeMillis(), + 0, 0, + optString("avatarImageUrl").takeIf { it.isNotBlank() }, + optString("avatarImageUrl").takeIf { it.isNotBlank() }, + getString("title"), + null, + getJSONObject("experienceInfo")?.let { + PlayerLevelInfo( + it.optLong("currentExperiencePoints"), + 0, + it.getJSONObject("currentLevel")?.let { + PlayerLevel(it.getInt("level"), it.optLong("minExperiencePoints"), it.optLong("maxExperiencePoints")) + }, + it.getJSONObject("nextLevel")?.let { + PlayerLevel(it.getInt("level"), it.optLong("minExperiencePoints"), it.optLong("maxExperiencePoints")) + } + ) + }, + optJSONObject("profileSettings")?.optBoolean("profileVisible") ?: false, + false, + null, null, + optString("bannerUrlLandscape").takeIf { it.isNotBlank() }?.toUri(), + optString("bannerUrlLandscape").takeIf { it.isNotBlank() }, + optString("bannerUrlPortrait").takeIf { it.isNotBlank() }?.toUri(), + optString("bannerUrlPortrait").takeIf { it.isNotBlank() }, + 0, null, + optJSONObject("profileSettings")?.optString("friendsListVisibility")?.takeIf { it.isNotBlank() }?.let { + CurrentPlayerInfoEntity( + when (it) { + "VISIBLE" -> Player.FriendsListVisibilityStatus.VISIBLE + "REQUEST_REQUIRED" -> Player.FriendsListVisibilityStatus.REQUEST_REQUIRED + "FEATURE_UNAVAILABLE" -> Player.FriendsListVisibilityStatus.FEATURE_UNAVAILABLE + else -> Player.FriendsListVisibilityStatus.UNKNOWN + } + ) + }, + false, + null +) + +suspend fun registerForGames(context: Context, account: Account, queue: RequestQueue = singleInstanceOf { Volley.newRequestQueue(context.applicationContext) }) { + val authManager = AuthManager(context, account.name, Constants.GMS_PACKAGE_NAME, "oauth2:${Scopes.GAMES_FIRSTPARTY}") + authManager.setOauth2Foreground("1") + val authToken = withContext(Dispatchers.IO) { authManager.requestAuth(false).auth } + val androidId = getSettings(context, CheckIn.getContentUri(context), arrayOf(CheckIn.ANDROID_ID)) { cursor: Cursor -> cursor.getLong(0) } + val result = suspendCoroutine { continuation -> + queue.add( + object : JsonObjectRequest( + "https://www.googleapis.com/games/v1whitelisted/players/me/profilesettings?requestRandomGamerTag=true&language=${Utils.getLocale(context)}", + { continuation.resume(it) }, + { continuation.resumeWithException(RuntimeException(it)) }) { + override fun getHeaders(): MutableMap { + return mutableMapOf( + "Authorization" to "OAuth $authToken", + "X-Device-ID" to androidId.toString(16) + ) + } + } + ) + } + suspendCoroutine { continuation -> + queue.add( + object : JsonObjectRequest( + Method.PUT, + "https://www.googleapis.com/games/v1whitelisted/players/me/profilesettings?language=${Utils.getLocale(context)}", + JSONObject().apply { + put("alwaysAutoSignIn", false) + put("autoSignIn", false) + put("gamerTagIsDefault", true) + put("gamerTagIsExplicitlySet", false) + put("gamesLitePlayerStatsEnabled", false) + put("profileDiscoverableViaGoogleAccount", false) + put("profileVisibilityWasChosenByPlayer", false) + put("profileVisible", false) + put("gamerTag", result.getString("gamerTag")) + if (result.has("stockGamerAvatarUrl")) put("stockGamerAvatarUrl", result.getString("stockGamerAvatarUrl")) + }, + { continuation.resume(it) }, + { continuation.resumeWithException(RuntimeException(it)) }) { + override fun getHeaders(): MutableMap { + return mutableMapOf( + "Content-Type" to "application/json; charset=utf-8", + "Authorization" to "OAuth $authToken", + "X-Device-ID" to androidId.toString(16) + ) + } + } + ) + } +} + +suspend fun performGamesSignIn( + context: Context, + packageName: String, + account: Account, + permitted: Boolean = false, + scopes: List = emptyList(), + queue: RequestQueue = singleInstanceOf { Volley.newRequestQueue(context.applicationContext) } +): Boolean { + val scopes = (scopes.toSet() + Scope(Scopes.GAMES_LITE)).toList().sortedBy { it.scopeUri } + val authManager = AuthManager(context, account.name, packageName, "oauth2:${scopes.joinToString(" ")}") + if (scopes.size == 1) authManager.setItCaveatTypes("2") + if (permitted) authManager.isPermitted = true + val authResponse = withContext(Dispatchers.IO) { authManager.requestAuth(true) } + if (authResponse.auth == null) return false + if (authResponse.issueAdvice != "stored" || GamesConfigurationService.getPlayer(context, packageName, account) == null) { + suspend fun fetchSelfPlayer() = suspendCoroutine { continuation -> + queue.add( + object : JsonObjectRequest( + "https://www.googleapis.com/games/v1/players/me", + { continuation.resume(it) }, + { continuation.resumeWithException(it) }) { + override fun getHeaders(): MutableMap { + return mutableMapOf( + "Authorization" to "OAuth ${authResponse.auth}" + ) + } + } + ) + } + + val result = try { + fetchSelfPlayer() + } catch (e: Exception) { + if (e is VolleyError && e.networkResponse?.statusCode == 404) { + registerForGames(context, account, queue) + fetchSelfPlayer() + } else { + throw e + } + } + GamesConfigurationService.setPlayer(context, packageName, account, result.toString()) + if (packageName != GAMES_PACKAGE_NAME) { + try { + suspendCoroutine { continuation -> + queue.add(object : Request(Method.POST, "https://www.googleapis.com/games/v1/applications/played", { + continuation.resumeWithException(it) + }) { + override fun parseNetworkResponse(response: NetworkResponse): Response { + if (response.statusCode == 204) return success(Unit, null) + return Response.error(VolleyError(response)) + } + + override fun deliverResponse(response: Unit) { + continuation.resume(response) + } + + override fun getHeaders(): MutableMap { + return mutableMapOf( + "Authorization" to "OAuth ${authResponse.auth}" + ) + } + }) + } + } catch (ignored: Exception) { + } + } + } + return true +} \ No newline at end of file diff --git a/play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetFragment.kt b/play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetFragment.kt new file mode 100644 index 00000000..d9339a55 --- /dev/null +++ b/play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetFragment.kt @@ -0,0 +1,311 @@ +/* + * SPDX-FileCopyrightText: 2020, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.ui + +import android.annotation.SuppressLint +import android.os.Bundle +import android.util.Base64 +import android.util.Log +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import androidx.core.content.ContextCompat +import androidx.core.os.bundleOf +import androidx.lifecycle.lifecycleScope +import androidx.navigation.fragment.findNavController +import androidx.preference.Preference +import androidx.preference.PreferenceCategory +import androidx.preference.PreferenceFragmentCompat +import com.android.volley.Request +import com.android.volley.toolbox.JsonObjectRequest +import com.android.volley.toolbox.Volley +import com.google.android.gms.BuildConfig +import com.google.android.gms.R +import com.google.android.gms.recaptcha.Recaptcha +import com.google.android.gms.recaptcha.RecaptchaAction +import com.google.android.gms.recaptcha.RecaptchaActionType +import com.google.android.gms.safetynet.SafetyNet +import com.google.android.gms.tasks.await +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.json.JSONObject +import org.microg.gms.checkin.CheckinPreferences +import org.microg.gms.droidguard.core.DroidGuardPreferences +import org.microg.gms.safetynet.SafetyNetDatabase +import org.microg.gms.safetynet.SafetyNetPreferences +import org.microg.gms.safetynet.SafetyNetRequestType.* +import org.microg.gms.utils.singleInstanceOf +import java.net.URLEncoder +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine +import kotlin.random.Random + +class SafetyNetFragment : PreferenceFragmentCompat() { + private lateinit var switchBarPreference: SwitchBarPreference + private lateinit var runAttest: Preference + private lateinit var runReCaptcha: Preference + private lateinit var runReCaptchaEnterprise: Preference + private lateinit var apps: PreferenceCategory + private lateinit var appsAll: Preference + private lateinit var appsNone: Preference + private lateinit var droidguardUnsupported: Preference + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + addPreferencesFromResource(R.xml.preferences_safetynet) + + switchBarPreference = preferenceScreen.findPreference("pref_safetynet_enabled") ?: switchBarPreference + runAttest = preferenceScreen.findPreference("pref_safetynet_run_attest") ?: runAttest + runReCaptcha = preferenceScreen.findPreference("pref_recaptcha_run_test") ?: runReCaptcha + runReCaptchaEnterprise = preferenceScreen.findPreference("pref_recaptcha_enterprise_run_test") ?: runReCaptchaEnterprise + apps = preferenceScreen.findPreference("prefcat_safetynet_apps") ?: apps + appsAll = preferenceScreen.findPreference("pref_safetynet_apps_all") ?: appsAll + appsNone = preferenceScreen.findPreference("pref_safetynet_apps_none") ?: appsNone + droidguardUnsupported = preferenceScreen.findPreference("pref_droidguard_unsupported") ?: droidguardUnsupported + + runAttest.isVisible = SAFETYNET_API_KEY != null + runReCaptcha.isVisible = RECAPTCHA_SITE_KEY != null + runReCaptchaEnterprise.isVisible = RECAPTCHA_ENTERPRISE_SITE_KEY != null + + runAttest.setOnPreferenceClickListener { runSafetyNetAttest(); true } + runReCaptcha.setOnPreferenceClickListener { runReCaptchaAttest(); true } + runReCaptchaEnterprise.setOnPreferenceClickListener { runReCaptchaEnterpriseAttest();true } + appsAll.setOnPreferenceClickListener { findNavController().navigate(requireContext(), R.id.openAllSafetyNetApps);true } + switchBarPreference.setOnPreferenceChangeListener { _, newValue -> + val newStatus = newValue as Boolean + SafetyNetPreferences.setEnabled(requireContext(), newStatus) + DroidGuardPreferences.setEnabled(requireContext(), newStatus) + droidguardUnsupported.isVisible = switchBarPreference.isChecked && !DroidGuardPreferences.isAvailable(requireContext()) + true + } + } + + private fun runSafetyNetAttest() { + val context = context ?: return + runAttest.setIcon(R.drawable.ic_circle_pending) + runAttest.setSummary(R.string.pref_test_summary_running) + lifecycleScope.launchWhenResumed { + try { + val response = SafetyNet.getClient(requireActivity()) + .attest(Random.nextBytes(32), SAFETYNET_API_KEY).await() + val (_, payload, _) = try { + response.jwsResult.split(".") + } catch (e: Exception) { + listOf(null, null, null) + } + formatSummaryForSafetyNetResult( + context, + payload?.let { Base64.decode(it, Base64.URL_SAFE).decodeToString() }, + response.result.status, + ATTESTATION + ) + .let { (summary, icon) -> + runAttest.summary = summary + runAttest.icon = icon + } + } catch (e: Exception) { + runAttest.summary = getString(R.string.pref_test_summary_failed, e.message) + runAttest.icon = ContextCompat.getDrawable(context, R.drawable.ic_circle_warn) + } + updateContent() + } + } + + private fun runReCaptchaAttest() { + val context = context ?: return + runReCaptcha.setIcon(R.drawable.ic_circle_pending) + runReCaptcha.setSummary(R.string.pref_test_summary_running) + lifecycleScope.launchWhenResumed { + try { + val response = SafetyNet.getClient(requireActivity()) + .verifyWithRecaptcha(RECAPTCHA_SITE_KEY).await() + val result = if (response.tokenResult != null) { + val queue = singleInstanceOf { Volley.newRequestQueue(context.applicationContext) } + val json = + if (RECAPTCHA_SECRET != null) { + suspendCoroutine { continuation -> + queue.add(object : JsonObjectRequest( + Method.POST, + "https://www.google.com/recaptcha/api/siteverify", + null, + { continuation.resume(it) }, + { continuation.resumeWithException(it) } + ) { + override fun getBodyContentType(): String = "application/x-www-form-urlencoded; charset=UTF-8" + override fun getBody(): ByteArray = + "secret=$RECAPTCHA_SECRET&response=${URLEncoder.encode(response.tokenResult, "UTF-8")}".encodeToByteArray() + }) + } + } else { + // Can't properly verify, everything becomes a success + JSONObject(mapOf("success" to true)) + } + Log.d(TAG, "Result: $json") + json.toString() + } else { + null + } + formatSummaryForSafetyNetResult(context, result, response.result.status, RECAPTCHA) + .let { (summary, icon) -> + runReCaptcha.summary = summary + runReCaptcha.icon = icon + } + } catch (e: Exception) { + runReCaptcha.summary = getString(R.string.pref_test_summary_failed, e.message) + runReCaptcha.icon = ContextCompat.getDrawable(context, R.drawable.ic_circle_warn) + } + updateContent() + } + } + + private fun runReCaptchaEnterpriseAttest() { + val context = context ?: return + runReCaptchaEnterprise.setIcon(R.drawable.ic_circle_pending) + runReCaptchaEnterprise.setSummary(R.string.pref_test_summary_running) + lifecycleScope.launchWhenResumed { + try { + val client = Recaptcha.getClient(requireActivity()) + val handle = client.init(RECAPTCHA_ENTERPRISE_SITE_KEY).await() + val actionType = RecaptchaActionType.SIGNUP + val response = client.execute(handle, RecaptchaAction(RecaptchaActionType(actionType))).await() + Log.d(TAG, "Recaptcha Token: " + response.tokenResult) + client.close(handle).await() + val result = if (response.tokenResult != null) { + val queue = singleInstanceOf { Volley.newRequestQueue(context.applicationContext) } + val json = if (RECAPTCHA_ENTERPRISE_API_KEY != null) { + suspendCoroutine { continuation -> + queue.add(JsonObjectRequest( + Request.Method.POST, + "https://recaptchaenterprise.googleapis.com/v1/projects/$RECAPTCHA_ENTERPRISE_PROJECT_ID/assessments?key=$RECAPTCHA_ENTERPRISE_API_KEY", + JSONObject( + mapOf( + "event" to JSONObject( + mapOf( + "token" to response.tokenResult, + "siteKey" to RECAPTCHA_ENTERPRISE_SITE_KEY, + "expectedAction" to actionType + ) + ) + ) + ), + { continuation.resume(it) }, + { continuation.resumeWithException(it) } + )) + } + } else { + // Can't properly verify, everything becomes a success + JSONObject(mapOf("tokenProperties" to JSONObject(mapOf("valid" to true)), "riskAnalysis" to JSONObject(mapOf("score" to "unknown")))) + } + Log.d(TAG, "Result: $json") + json.toString() + } else { + null + } + formatSummaryForSafetyNetResult(context, result, null, RECAPTCHA_ENTERPRISE) + .let { (summary, icon) -> + runReCaptchaEnterprise.summary = summary + runReCaptchaEnterprise.icon = icon + } + } catch (e: Exception) { + runReCaptchaEnterprise.summary = getString(R.string.pref_test_summary_failed, e.message) + runReCaptchaEnterprise.icon = ContextCompat.getDrawable(context, R.drawable.ic_circle_warn) + } + updateContent() + } + } + + override fun onResume() { + super.onResume() + + switchBarPreference.isEnabled = CheckinPreferences.isEnabled(requireContext()) + switchBarPreference.isChecked = SafetyNetPreferences.isEnabled(requireContext()) && DroidGuardPreferences.isEnabled(requireContext()) + droidguardUnsupported.isVisible = switchBarPreference.isChecked && !DroidGuardPreferences.isAvailable(requireContext()) + + updateContent() + } + + fun updateContent() { + lifecycleScope.launchWhenResumed { + val context = requireContext() + val (apps, showAll) = withContext(Dispatchers.IO) { + val db = SafetyNetDatabase(context) + val apps = try { + db.recentApps + } finally { + db.close() + } + apps.map { app -> + app to context.packageManager.getApplicationInfoIfExists(app.first) + }.mapNotNull { (app, info) -> + if (info == null) null else app to info + }.take(3).mapIndexed { idx, (app, applicationInfo) -> + val pref = AppIconPreference(context) + pref.order = idx + pref.applicationInfo = applicationInfo + pref.onPreferenceClickListener = Preference.OnPreferenceClickListener { + findNavController().navigate( + requireContext(), R.id.openSafetyNetAppDetails, bundleOf( + "package" to app.first + ) + ) + true + } + pref.key = "pref_safetynet_app_" + app.first + pref + }.let { it to (it.size < apps.size) } + } + appsAll.isVisible = showAll + this@SafetyNetFragment.apps.removeAll() + for (app in apps) { + this@SafetyNetFragment.apps.addPreference(app) + } + if (showAll) { + this@SafetyNetFragment.apps.addPreference(appsAll) + } else if (apps.isEmpty()) { + this@SafetyNetFragment.apps.addPreference(appsNone) + } + + } + } + + init { + setHasOptionsMenu(true) + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + menu.add(0, MENU_ADVANCED, 0, org.microg.gms.base.core.R.string.menu_advanced) + menu.add(0, MENU_CLEAR_REQUESTS, 0, R.string.menu_clear_recent_requests) + super.onCreateOptionsMenu(menu, inflater) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + MENU_ADVANCED -> { + findNavController().navigate(requireContext(), R.id.openSafetyNetAdvancedSettings) + true + } + MENU_CLEAR_REQUESTS -> { + val db = SafetyNetDatabase(requireContext()) + db.clearAllRequests() + db.close() + updateContent() + true + } + else -> super.onOptionsItemSelected(item) + } + } + + companion object { + private val SAFETYNET_API_KEY: String? = BuildConfig.SAFETYNET_KEY.takeIf { it.isNotBlank() } + private val RECAPTCHA_SITE_KEY: String? = BuildConfig.RECAPTCHA_SITE_KEY.takeIf { it.isNotBlank() } + private val RECAPTCHA_SECRET: String? = BuildConfig.RECAPTCHA_SECRET.takeIf { it.isNotBlank() } + private val RECAPTCHA_ENTERPRISE_PROJECT_ID: String? = BuildConfig.RECAPTCHA_ENTERPRISE_PROJECT_ID.takeIf { it.isNotBlank() } + private val RECAPTCHA_ENTERPRISE_SITE_KEY: String? = BuildConfig.RECAPTCHA_ENTERPRISE_SITE_KEY.takeIf { it.isNotBlank() } + private val RECAPTCHA_ENTERPRISE_API_KEY: String? = BuildConfig.RECAPTCHA_ENTERPRISE_API_KEY.takeIf { it.isNotBlank() } + private const val MENU_ADVANCED = Menu.FIRST + private const val MENU_CLEAR_REQUESTS = Menu.FIRST + 1 + } +} diff --git a/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/DroidGuardResultCreator.kt b/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/DroidGuardResultCreator.kt new file mode 100644 index 00000000..a2e7d4d9 --- /dev/null +++ b/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/DroidGuardResultCreator.kt @@ -0,0 +1,62 @@ +/* + * SPDX-FileCopyrightText: 2021 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.droidguard.core + +import android.content.Context +import com.android.volley.VolleyError +import com.android.volley.toolbox.StringRequest +import com.android.volley.toolbox.Volley +import com.google.android.gms.droidguard.DroidGuardClient +import com.google.android.gms.tasks.await +import org.microg.gms.utils.singleInstanceOf +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine + +interface DroidGuardResultCreator { + suspend fun getResults(flow: String, data: Map): String + + companion object { + fun getInstance(context: Context): DroidGuardResultCreator = + if (DroidGuardPreferences.isAvailable(context)) { + when (DroidGuardPreferences.getMode(context)) { + DroidGuardPreferences.Mode.Embedded -> EmbeddedDroidGuardResultCreator(context) + DroidGuardPreferences.Mode.Network -> NetworkDroidGuardResultCreator(context) + } + } else { + throw RuntimeException("DroidGuard disabled") + } + + suspend fun getResults(context: Context, flow: String, data: Map): String = + getInstance(context).getResults(flow, data) + } +} + +private class NetworkDroidGuardResultCreator(private val context: Context) : DroidGuardResultCreator { + private val queue = singleInstanceOf { Volley.newRequestQueue(context.applicationContext) } + private val url: String + get() = DroidGuardPreferences.getNetworkServerUrl(context) ?: throw IllegalStateException("Network URL required") + + override suspend fun getResults(flow: String, data: Map): String = suspendCoroutine { continuation -> + queue.add(PostParamsStringRequest("$url?flow=$flow", data, { + continuation.resume(it) + }, { + continuation.resumeWithException(it.cause ?: it) + })) + } + + companion object { + class PostParamsStringRequest(url: String, private val data: Map, listener: (String) -> Unit, errorListener: (VolleyError) -> Unit) : StringRequest(Method.POST, url, listener, errorListener) { + override fun getParams(): Map = data + } + } +} + +private class EmbeddedDroidGuardResultCreator(private val context: Context) : DroidGuardResultCreator { + override suspend fun getResults(flow: String, data: Map): String { + return DroidGuardClient.getResults(context, flow, data).await() + } +} diff --git a/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/HandleProxyFactory.kt b/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/HandleProxyFactory.kt new file mode 100644 index 00000000..cfbef662 --- /dev/null +++ b/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/HandleProxyFactory.kt @@ -0,0 +1,226 @@ +/* + * SPDX-FileCopyrightText: 2021 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.droidguard.core + +import android.content.Context +import com.android.volley.NetworkResponse +import com.android.volley.RequestQueue +import com.android.volley.VolleyError +import com.android.volley.toolbox.RequestFuture +import com.android.volley.toolbox.Volley +import com.google.android.gms.droidguard.internal.DroidGuardResultsRequest +import dalvik.system.DexClassLoader +import okio.ByteString.Companion.decodeHex +import okio.ByteString.Companion.of +import org.microg.gms.droidguard.* +import org.microg.gms.profile.Build +import org.microg.gms.profile.ProfileManager +import org.microg.gms.utils.singleInstanceOf +import java.io.File +import java.io.IOException +import java.security.MessageDigest +import java.security.cert.Certificate +import java.util.* +import com.android.volley.Request as VolleyRequest +import com.android.volley.Response as VolleyResponse + +class HandleProxyFactory(private val context: Context) { + private val classMap = hashMapOf>() + private val dgDb: DgDatabaseHelper = DgDatabaseHelper(context) + private val version = VersionUtil(context) + private val queue = singleInstanceOf { Volley.newRequestQueue(context.applicationContext) } + + fun createHandle(packageName: String, flow: String?, callback: GuardCallback, request: DroidGuardResultsRequest?): HandleProxy { + if (!DroidGuardPreferences.isLocalAvailable(context)) throw IllegalAccessException("DroidGuard should not be available locally") + val (vmKey, byteCode, bytes) = readFromDatabase(flow) ?: fetchFromServer(flow, packageName) + return createHandleProxy(flow, vmKey, byteCode, bytes, callback, request) + } + + fun createPingHandle(packageName: String, flow: String, callback: GuardCallback, pingData: PingData?): HandleProxy { + if (!DroidGuardPreferences.isLocalAvailable(context)) throw IllegalAccessException("DroidGuard should not be available locally") + val (vmKey, byteCode, bytes) = fetchFromServer(flow, createRequest(flow, packageName, pingData)) + return createHandleProxy(flow, vmKey, byteCode, bytes, callback, DroidGuardResultsRequest().also { it.clientVersion = 0 }) + } + + fun createLowLatencyHandle(flow: String?, callback: GuardCallback, request: DroidGuardResultsRequest?): HandleProxy { + if (!DroidGuardPreferences.isLocalAvailable(context)) throw IllegalAccessException("DroidGuard should not be available locally") + val (vmKey, byteCode, bytes) = readFromDatabase("fast") ?: throw Exception("low latency (fast) flow not available") + return createHandleProxy(flow, vmKey, byteCode, bytes, callback, request) + } + + fun SignedResponse.unpack(): Response { + if (SignatureVerifier.verifySignature(data_!!.toByteArray(), signature!!.toByteArray())) { + return Response.ADAPTER.decode(data_!!) + } else { + throw SecurityException("Signature invalid") + } + } + + private fun readFromDatabase(flow: String?): Triple? { + ProfileManager.ensureInitialized(context) + val id = "$flow/${version.versionString}/${Build.FINGERPRINT}" + return dgDb.get(id) + } + + fun createRequest(flow: String?, packageName: String, pingData: PingData? = null, extra: ByteArray? = null): Request { + ProfileManager.ensureInitialized(context) + return Request( + usage = Usage(flow, packageName), + info = listOf( + KeyValuePair("BOARD", Build.BOARD), + KeyValuePair("BOOTLOADER", Build.BOOTLOADER), + KeyValuePair("BRAND", Build.BRAND), + KeyValuePair("CPU_ABI", Build.CPU_ABI), + KeyValuePair("CPU_ABI2", Build.CPU_ABI2), + KeyValuePair("SUPPORTED_ABIS", Build.SUPPORTED_ABIS.joinToString(",")), + KeyValuePair("DEVICE", Build.DEVICE), + KeyValuePair("DISPLAY", Build.DISPLAY), + KeyValuePair("FINGERPRINT", Build.FINGERPRINT), + KeyValuePair("HARDWARE", Build.HARDWARE), + KeyValuePair("HOST", Build.HOST), + KeyValuePair("ID", Build.ID), + KeyValuePair("MANUFACTURER", Build.MANUFACTURER), + KeyValuePair("MODEL", Build.MODEL), + KeyValuePair("PRODUCT", Build.PRODUCT), + KeyValuePair("RADIO", Build.RADIO), + KeyValuePair("SERIAL", Build.SERIAL), + KeyValuePair("TAGS", Build.TAGS), + KeyValuePair("TIME", Build.TIME.toString()), + KeyValuePair("TYPE", Build.TYPE), + KeyValuePair("USER", Build.USER), + KeyValuePair("VERSION.CODENAME", Build.VERSION.CODENAME), + KeyValuePair("VERSION.INCREMENTAL", Build.VERSION.INCREMENTAL), + KeyValuePair("VERSION.RELEASE", Build.VERSION.RELEASE), + KeyValuePair("VERSION.SDK", Build.VERSION.SDK), + KeyValuePair("VERSION.SDK_INT", Build.VERSION.SDK_INT.toString()), + ), + versionName = version.versionString, + versionCode = BuildConfig.VERSION_CODE, + hasAccount = false, + isGoogleCn = false, + enableInlineVm = true, + cached = getCacheDir().list()?.map { it.decodeHex() }.orEmpty(), + arch = System.getProperty("os.arch"), + ping = pingData, + field10 = extra?.let { of(*it) }, + ) + } + + fun fetchFromServer(flow: String?, packageName: String): Triple { + return fetchFromServer(flow, createRequest(flow, packageName)) + } + + fun fetchFromServer(flow: String?, request: Request): Triple { + ProfileManager.ensureInitialized(context) + val future = RequestFuture.newFuture() + queue.add(object : VolleyRequest(Method.POST, SERVER_URL, future) { + override fun parseNetworkResponse(response: NetworkResponse): VolleyResponse { + return try { + VolleyResponse.success(SignedResponse.ADAPTER.decode(response.data), null) + } catch (e: Exception) { + VolleyResponse.error(VolleyError(e)) + } + } + + override fun deliverResponse(response: SignedResponse) { + future.onResponse(response) + } + + override fun getBody(): ByteArray = request.encode() + + override fun getBodyContentType(): String = "application/x-protobuf" + + override fun getHeaders(): Map { + return mapOf( + "User-Agent" to "DroidGuard/${version.versionCode}" + ) + } + }) + val signed: SignedResponse = future.get() + val response = signed.unpack() + val vmKey = response.vmChecksum!!.hex() + if (!isValidCache(vmKey)) { + val temp = File(getCacheDir(), "${UUID.randomUUID()}.apk") + temp.parentFile!!.mkdirs() + temp.writeBytes(response.content!!.toByteArray()) + getOptDir(vmKey).mkdirs() + temp.renameTo(getTheApkFile(vmKey)) + updateCacheTimestamp(vmKey) + if (!isValidCache(vmKey)) { + getCacheDir(vmKey).deleteRecursively() + throw IllegalStateException() + } + } + val id = "$flow/${version.versionString}/${Build.FINGERPRINT}" + val expiry = (response.expiryTimeSecs ?: 0).toLong() + val byteCode = response.byteCode?.toByteArray() ?: ByteArray(0) + val extra = response.extra?.toByteArray() ?: ByteArray(0) + if (response.save != false) { + dgDb.put(id, expiry, vmKey, byteCode, extra) + } + return Triple(vmKey, byteCode, extra) + } + + private fun createHandleProxy(flow: String?, vmKey: String, byteCode: ByteArray, extra: ByteArray, callback: GuardCallback, request: DroidGuardResultsRequest?): HandleProxy { + ProfileManager.ensureInitialized(context) + val clazz = loadClass(vmKey, extra) + return HandleProxy(clazz, context, flow, byteCode, callback, vmKey, extra, request?.bundle) + } + + fun getTheApkFile(vmKey: String) = File(getCacheDir(vmKey), "the.apk") + private fun getCacheDir() = context.getDir(CACHE_FOLDER_NAME, Context.MODE_PRIVATE) + private fun getCacheDir(vmKey: String) = File(getCacheDir(), vmKey) + private fun getOptDir(vmKey: String) = File(getCacheDir(vmKey), "opt") + private fun isValidCache(vmKey: String) = getTheApkFile(vmKey).isFile && getOptDir(vmKey).isDirectory + + private fun updateCacheTimestamp(vmKey: String) { + try { + val timestampFile = File(getCacheDir(vmKey), "t") + if (!timestampFile.exists() && !timestampFile.createNewFile()) { + throw Exception("Failed to touch last-used file for $vmKey.") + } + if (!timestampFile.setLastModified(System.currentTimeMillis())) { + throw Exception("Failed to update last-used timestamp for $vmKey.") + } + } catch (e: IOException) { + throw Exception("Failed to touch last-used file for $vmKey.") + } + } + + private fun verifyApkSignature(apk: File): Boolean { + return true + val certificates: Array = TODO() + if (certificates.size != 1) return false + return Arrays.equals(MessageDigest.getInstance("SHA-256").digest(certificates[0].encoded), PROD_CERT_HASH) + } + + private fun loadClass(vmKey: String, bytes: ByteArray): Class<*> { + val clazz = classMap[vmKey] + if (clazz != null) { + updateCacheTimestamp(vmKey) + return clazz + } else { + if (!isValidCache(vmKey)) { + throw BytesException(bytes, "VM key $vmKey not found in cache") + } + if (!verifyApkSignature(getTheApkFile(vmKey))) { + getCacheDir(vmKey).deleteRecursively() + throw ClassNotFoundException("APK signature verification failed") + } + val loader = DexClassLoader(getTheApkFile(vmKey).absolutePath, getOptDir(vmKey).absolutePath, null, context.classLoader) + val clazz = loader.loadClass(CLASS_NAME) + classMap[vmKey] = clazz + return clazz + } + } + + companion object { + const val CLASS_NAME = "com.google.ccc.abuse.droidguard.DroidGuard" + const val SERVER_URL = "https://www.googleapis.com/androidantiabuse/v1/x/create?alt=PROTO&key=AIzaSyBofcZsgLSS7BOnBjZPEkk4rYwzOIz-lTI" + const val CACHE_FOLDER_NAME = "cache_dg" + val PROD_CERT_HASH = byteArrayOf(61, 122, 18, 35, 1, -102, -93, -99, -98, -96, -29, 67, 106, -73, -64, -119, 107, -5, 79, -74, 121, -12, -34, 95, -25, -62, 63, 50, 108, -113, -103, 74) + } +} diff --git a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/RequestHandling.kt b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/RequestHandling.kt new file mode 100644 index 00000000..3947e175 --- /dev/null +++ b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/RequestHandling.kt @@ -0,0 +1,262 @@ +/* + * SPDX-FileCopyrightText: 2022 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.fido.core + +import android.content.Context +import android.net.Uri +import android.util.Base64 +import com.android.volley.toolbox.JsonArrayRequest +import com.android.volley.toolbox.JsonObjectRequest +import com.android.volley.toolbox.Volley +import com.google.android.gms.fido.fido2.api.common.* +import com.google.android.gms.fido.fido2.api.common.ErrorCode.* +import com.google.common.net.InternetDomainName +import kotlinx.coroutines.CompletableDeferred +import org.json.JSONArray +import org.json.JSONObject +import org.microg.gms.fido.core.RequestOptionsType.REGISTER +import org.microg.gms.fido.core.RequestOptionsType.SIGN +import org.microg.gms.utils.* +import java.net.HttpURLConnection +import java.security.MessageDigest + +class RequestHandlingException(val errorCode: ErrorCode, message: String? = null) : Exception(message) + +enum class RequestOptionsType { REGISTER, SIGN } + +val RequestOptions.registerOptions: PublicKeyCredentialCreationOptions + get() = when (this) { + is BrowserPublicKeyCredentialCreationOptions -> publicKeyCredentialCreationOptions + is PublicKeyCredentialCreationOptions -> this + else -> throw RequestHandlingException(DATA_ERR, "The request options are not valid") + } + +val RequestOptions.signOptions: PublicKeyCredentialRequestOptions + get() = when (this) { + is BrowserPublicKeyCredentialRequestOptions -> publicKeyCredentialRequestOptions + is PublicKeyCredentialRequestOptions -> this + else -> throw RequestHandlingException(DATA_ERR, "The request options are not valid") + } + +val RequestOptions.type: RequestOptionsType + get() = when (this) { + is PublicKeyCredentialCreationOptions, is BrowserPublicKeyCredentialCreationOptions -> REGISTER + is PublicKeyCredentialRequestOptions, is BrowserPublicKeyCredentialRequestOptions -> SIGN + else -> throw RequestHandlingException(INVALID_STATE_ERR) + } + +val RequestOptions.webAuthnType: String + get() = when (type) { + REGISTER -> "webauthn.create" + SIGN -> "webauthn.get" + } + +val RequestOptions.challenge: ByteArray + get() = when (type) { + REGISTER -> registerOptions.challenge + SIGN -> signOptions.challenge + } + +val RequestOptions.rpId: String + get() = when (type) { + REGISTER -> registerOptions.rp.id + SIGN -> signOptions.rpId + } + +val PublicKeyCredentialCreationOptions.skipAttestation: Boolean + get() = attestationConveyancePreference in setOf(AttestationConveyancePreference.NONE, null) + +fun topDomainOf(string: String?) = + string?.let { InternetDomainName.from(string).topDomainUnderRegistrySuffix().toString() } + +fun JSONArray.map(fn: JSONArray.(Int) -> T): List = (0 until length()).map { fn(this, it) } + +private suspend fun isFacetIdTrusted(context: Context, facetId: String, appId: String): Boolean { + val trustedFacets = try { + val deferred = CompletableDeferred() + HttpURLConnection.setFollowRedirects(false) + singleInstanceOf { Volley.newRequestQueue(context.applicationContext) } + .add(JsonObjectRequest(appId, { deferred.complete(it) }, { deferred.completeExceptionally(it) })) + val obj = deferred.await() + val arr = obj.getJSONArray("trustedFacets") + if (arr.length() > 1) { + // Unsupported + emptyList() + } else { + arr.getJSONObject(0).getJSONArray("ids").map(JSONArray::getString) + } + } catch (e: Exception) { + // Ignore and fail + emptyList() + } + return trustedFacets.contains(facetId) +} + +private const val ASSET_LINK_REL = "delegate_permission/common.get_login_creds" +private suspend fun isAssetLinked(context: Context, rpId: String, facetId: String, packageName: String?): Boolean { + try { + if (!facetId.startsWith("android:apk-key-hash-sha256:")) return false + val fp = Base64.decode(facetId.substring(28), HASH_BASE64_FLAGS).toHexString(":") + val deferred = CompletableDeferred() + HttpURLConnection.setFollowRedirects(true) + val url = "https://$rpId/.well-known/assetlinks.json" + singleInstanceOf { Volley.newRequestQueue(context.applicationContext) } + .add(JsonArrayRequest(url, { deferred.complete(it) }, { deferred.completeExceptionally(it) })) + val arr = deferred.await() + for (obj in arr.map(JSONArray::getJSONObject)) { + if (!obj.getJSONArray("relation").map(JSONArray::getString).contains(ASSET_LINK_REL)) continue + val target = obj.getJSONObject("target") + if (target.getString("namespace") != "android_app") continue + if (packageName != null && target.getString("package_name") != packageName) continue + for (fingerprint in target.getJSONArray("sha256_cert_fingerprints").map(JSONArray::getString)) { + if (fingerprint.equals(fp, ignoreCase = true)) return true + } + } + return false + } catch (e: Exception) { + return false + } +} + +// Note: This assumes the RP ID is allowed +private suspend fun isAppIdAllowed(context: Context, appId: String, facetId: String, rpId: String): Boolean { + return try { + when { + topDomainOf(Uri.parse(appId).host) == topDomainOf(rpId) -> { + // Valid: AppId TLD+1 matches RP ID + true + } + topDomainOf(Uri.parse(appId).host) == "gstatic.com" && rpId == "google.com" -> { + // Valid: Hardcoded support for Google putting their app id under gstatic.com. + // This is gonna save us a ton of requests + true + } + isFacetIdTrusted(context, facetId, appId) -> { + // Valid: Allowed by TrustedFacets list + true + } + else -> { + false + } + } + } catch (e: Exception) { + false + } +} + +suspend fun RequestOptions.checkIsValid(context: Context, facetId: String, packageName: String?) { + if (type == REGISTER) { + if (registerOptions.authenticatorSelection?.requireResidentKey == true) { + throw RequestHandlingException( + NOT_SUPPORTED_ERR, + "Resident credentials or empty 'allowCredentials' lists are not supported at this time." + ) + } + } + if (type == SIGN) { + if (signOptions.allowList.isNullOrEmpty()) { + throw RequestHandlingException(NOT_ALLOWED_ERR, "Request doesn't have a valid list of allowed credentials.") + } + } + if (facetId.startsWith("https://")) { + if (topDomainOf(Uri.parse(facetId).host) != topDomainOf(rpId)) { + throw RequestHandlingException(NOT_ALLOWED_ERR, "RP ID $rpId not allowed from facet $facetId") + } + // FIXME: Standard suggests doing additional checks, but this is already sensible enough + } else if (facetId.startsWith("android:apk-key-hash:") && packageName != null) { + val sha256FacetId = getAltFacetId(context, packageName, facetId) ?: + throw RequestHandlingException(NOT_ALLOWED_ERR, "Can't resolve $facetId to SHA-256 Facet") + if (!isAssetLinked(context, rpId, sha256FacetId, packageName)) { + throw RequestHandlingException(NOT_ALLOWED_ERR, "RP ID $rpId not allowed from facet $sha256FacetId") + } + } else if (facetId.startsWith("android:apk-key-hash-sha256:")) { + if (!isAssetLinked(context, rpId, facetId, packageName)) { + throw RequestHandlingException(NOT_ALLOWED_ERR, "RP ID $rpId not allowed from facet $facetId") + } + } else { + throw RequestHandlingException(NOT_SUPPORTED_ERR, "Facet $facetId not supported") + } + val appId = authenticationExtensions?.fidoAppIdExtension?.appId + if (appId != null) { + if (!appId.startsWith("https://")) { + throw RequestHandlingException(NOT_ALLOWED_ERR, "AppId $appId must start with https://") + } + if (Uri.parse(appId).host.isNullOrEmpty()) { + throw RequestHandlingException(NOT_ALLOWED_ERR, "AppId $appId must have a valid hostname") + } + val altFacetId = packageName?.let { getAltFacetId(context, it, facetId) } + if (!isAppIdAllowed(context, appId, facetId, rpId) && + (altFacetId == null || !isAppIdAllowed(context, appId, altFacetId, rpId)) + ) { + throw RequestHandlingException(NOT_ALLOWED_ERR, "AppId $appId not allowed from facet $facetId/$altFacetId") + } + } +} + +private const val HASH_BASE64_FLAGS = Base64.NO_PADDING + Base64.NO_WRAP + Base64.URL_SAFE + +fun RequestOptions.getWebAuthnClientData(callingPackage: String, origin: String): ByteArray { + val obj = JSONObject() + .put("type", webAuthnType) + .put("challenge", challenge.toBase64(HASH_BASE64_FLAGS)) + .put("androidPackageName", callingPackage) + .put("tokenBinding", tokenBinding?.toJsonObject()) + .put("origin", origin) + return obj.toString().encodeToByteArray() +} + +fun getApplicationName(context: Context, options: RequestOptions, callingPackage: String): String = when (options) { + is BrowserPublicKeyCredentialCreationOptions, is BrowserPublicKeyCredentialRequestOptions -> options.rpId + else -> context.packageManager.getApplicationLabel(callingPackage).toString() +} + +fun getApkKeyHashFacetId(context: Context, packageName: String): String { + val digest = context.packageManager.getFirstSignatureDigest(packageName, "SHA1") + ?: throw RequestHandlingException(NOT_ALLOWED_ERR, "Unknown package $packageName") + return "android:apk-key-hash:${digest.toBase64(HASH_BASE64_FLAGS)}" +} + +fun getAltFacetId(context: Context, packageName: String, facetId: String): String? { + val firstSignature = context.packageManager.getSignatures(packageName).firstOrNull() + ?: throw RequestHandlingException(NOT_ALLOWED_ERR, "Unknown package $packageName") + return when (facetId) { + "android:apk-key-hash:${firstSignature.digest("SHA1").toBase64(HASH_BASE64_FLAGS)}" -> { + "android:apk-key-hash-sha256:${firstSignature.digest("SHA-256").toBase64(HASH_BASE64_FLAGS)}" + } + "android:apk-key-hash-sha256:${firstSignature.digest("SHA-256").toBase64(HASH_BASE64_FLAGS)}" -> { + "android:apk-key-hash:${firstSignature.digest("SHA1").toBase64(HASH_BASE64_FLAGS)}" + } + else -> null + } +} + +fun getFacetId(context: Context, options: RequestOptions, callingPackage: String): String = when { + options is BrowserRequestOptions -> { + if (options.origin.scheme == null || options.origin.authority == null) { + throw RequestHandlingException(NOT_ALLOWED_ERR, "Bad url ${options.origin}") + } + "${options.origin.scheme}://${options.origin.authority}" + } + else -> getApkKeyHashFacetId(context, callingPackage) +} + +fun ByteArray.digest(md: String): ByteArray = MessageDigest.getInstance(md).digest(this) + +fun getClientDataAndHash( + context: Context, + options: RequestOptions, + callingPackage: String +): Pair { + val clientData: ByteArray? + var clientDataHash = (options as? BrowserPublicKeyCredentialCreationOptions)?.clientDataHash + if (clientDataHash == null) { + clientData = options.getWebAuthnClientData(callingPackage, getFacetId(context, options, callingPackage)) + clientDataHash = clientData.digest("SHA-256") + } else { + clientData = "".toByteArray() + } + return clientData to clientDataHash +} diff --git a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/mozilla/MozillaLocationServiceClient.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/mozilla/MozillaLocationServiceClient.kt new file mode 100644 index 00000000..1f20378c --- /dev/null +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/mozilla/MozillaLocationServiceClient.kt @@ -0,0 +1,89 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.network.mozilla + +import android.content.Context +import android.location.Location +import android.net.Uri +import android.os.Bundle +import android.util.Log +import com.android.volley.Request.Method +import com.android.volley.toolbox.JsonObjectRequest +import com.android.volley.toolbox.Volley +import org.microg.gms.location.provider.BuildConfig +import org.microg.gms.location.network.precision +import org.microg.gms.location.network.wifi.WifiDetails +import org.microg.gms.location.network.wifi.isMoving +import org.microg.gms.utils.singleInstanceOf +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine + +class MozillaLocationServiceClient(context: Context) { + private val queue = singleInstanceOf { Volley.newRequestQueue(context.applicationContext) } + + suspend fun retrieveMultiWifiLocation(wifis: List): Location = geoLocate( + GeolocateRequest( + considerIp = false, + wifiAccessPoints = wifis.filter { it.ssid?.endsWith("_nomap") != true && !it.isMoving }.map(WifiDetails::toWifiAccessPoint), + fallbacks = Fallback(lacf = false, ipf = false) + ) + ).apply { + precision = wifis.size.toDouble()/ WIFI_BASE_PRECISION_COUNT + } + + suspend fun retrieveSingleCellLocation(cell: org.microg.gms.location.network.cell.CellDetails): Location = geoLocate( + GeolocateRequest( + considerIp = false, + cellTowers = listOf(cell.toCellTower()), + fallbacks = Fallback( + lacf = true, + ipf = false + ) + ) + ).apply { + precision = if (extras?.getString(LOCATION_EXTRA_FALLBACK) != null) CELL_FALLBACK_PRECISION else CELL_DEFAULT_PRECISION + } + + private suspend fun geoLocate(request: GeolocateRequest): Location { + val response = rawGeoLocate(request) + Log.d(TAG, "$request -> $response") + if (response.location != null) { + return Location("mozilla").apply { + latitude = response.location.lat + longitude = response.location.lng + if (response.accuracy != null) accuracy = response.accuracy.toFloat() + if (response.fallback != null) extras = Bundle().apply { putString(LOCATION_EXTRA_FALLBACK, response.fallback) } + } + } else if (response.error != null) { + throw ServiceException(response.error) + } else { + throw RuntimeException("Invalid response JSON") + } + } + + private suspend fun rawGeoLocate(request: GeolocateRequest): GeolocateResponse = suspendCoroutine { continuation -> + queue.add(JsonObjectRequest(Method.POST, Uri.parse(GEOLOCATE_URL).buildUpon().apply { + if (API_KEY != null) appendQueryParameter("key", API_KEY) + }.build().toString(), request.toJson(), { + continuation.resume(it.toGeolocateResponse()) + }, { + continuation.resumeWithException(it) + })) + } + + companion object { + private const val TAG = "MozillaLocation" + private const val GEOLOCATE_URL = "https://location.services.mozilla.com/v1/geolocate" + private const val WIFI_BASE_PRECISION_COUNT = 4.0 + private const val CELL_DEFAULT_PRECISION = 1.0 + private const val CELL_FALLBACK_PRECISION = 0.5 + @JvmField + val API_KEY: String? = BuildConfig.ICHNAEA_KEY.takeIf { it.isNotBlank() } + const val LOCATION_EXTRA_FALLBACK = "fallback" + } + +} \ No newline at end of file diff --git a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/OpenStreetMapNominatimGeocodeProvider.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/OpenStreetMapNominatimGeocodeProvider.kt new file mode 100644 index 00000000..3104f548 --- /dev/null +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/OpenStreetMapNominatimGeocodeProvider.kt @@ -0,0 +1,188 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.provider + +import android.content.Context +import android.location.Address +import android.location.GeocoderParams +import android.net.Uri +import android.util.Log +import android.util.LruCache +import com.android.location.provider.GeocodeProvider +import com.android.volley.toolbox.JsonArrayRequest +import com.android.volley.toolbox.JsonObjectRequest +import com.android.volley.toolbox.Volley +import com.google.android.gms.location.internal.ClientIdentity +import org.json.JSONObject +import org.microg.address.Formatter +import org.microg.gms.location.LocationSettings +import org.microg.gms.utils.singleInstanceOf +import java.io.PrintWriter +import java.util.* +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicReference + + +class OpenStreetMapNominatimGeocodeProvider(private val context: Context) : GeocodeProvider() { + private val queue = singleInstanceOf { Volley.newRequestQueue(context.applicationContext) } + private val formatter = runCatching { Formatter() }.getOrNull() + private val addressCache = LruCache(CACHE_SIZE) + private val settings by lazy { LocationSettings(context) } + + override fun onGetFromLocation(latitude: Double, longitude: Double, maxResults: Int, params: GeocoderParams, addresses: MutableList

): String? { + val clientIdentity = params.clientIdentity ?: return "null client package" + val locale = params.locale ?: return "null locale" + if (!settings.geocoderNominatim) return "disabled" + val cacheKey = CacheKey(clientIdentity, locale, latitude, longitude) + addressCache[cacheKey]?.let {address -> + addresses.add(address) + return null + } + val uri = Uri.Builder() + .scheme("https").authority(NOMINATIM_SERVER).path("/reverse") + .appendQueryParameter("format", "json") + .appendQueryParameter("accept-language", locale.language) + .appendQueryParameter("addressdetails", "1") + .appendQueryParameter("lat", latitude.toString()) + .appendQueryParameter("lon", longitude.toString()) + val result = AtomicReference("timeout reached") + val returnedAddress = AtomicReference(null) + val latch = CountDownLatch(1) + queue.add(object : JsonObjectRequest(uri.build().toString(), { + parseResponse(locale, it)?.let(returnedAddress::set) + result.set(null) + latch.countDown() + }, { + result.set(it.message) + latch.countDown() + }) { + override fun getHeaders(): Map = mapOf("User-Agent" to "microG/${context.versionName}") + }) + latch.await(5, TimeUnit.SECONDS) + val address = returnedAddress.get() + if (address != null) { + Log.d(TAG, "Returned $address for $latitude,$longitude") + addresses.add(address) + addressCache.put(cacheKey, address) + } + return result.get() + } + + override fun onGetFromLocationName( + locationName: String, + lowerLeftLatitude: Double, + lowerLeftLongitude: Double, + upperRightLatitude: Double, + upperRightLongitude: Double, + maxResults: Int, + params: GeocoderParams, + addresses: MutableList
+ ): String? { + val clientIdentity = params.clientIdentity ?: return "null client package" + val locale = params.locale ?: return "null locale" + if (!settings.geocoderNominatim) return "disabled" + val uri = Uri.Builder() + .scheme("https").authority(NOMINATIM_SERVER).path("/search") + .appendQueryParameter("format", "json") + .appendQueryParameter("accept-language", locale.language) + .appendQueryParameter("addressdetails", "1") + .appendQueryParameter("bounded", "1") + .appendQueryParameter("q", locationName) + .appendQueryParameter("limit", maxResults.toString()) + if (lowerLeftLatitude != upperRightLatitude && lowerLeftLongitude != upperRightLongitude) { + uri.appendQueryParameter("viewbox", "$lowerLeftLongitude,$upperRightLatitude,$upperRightLongitude,$lowerLeftLatitude") + } + val result = AtomicReference("timeout reached") + val latch = CountDownLatch(1) + queue.add(object : JsonArrayRequest(uri.build().toString(), { + for (i in 0 until it.length()) { + parseResponse(locale, it.getJSONObject(i))?.let(addresses::add) + } + result.set(null) + latch.countDown() + }, { + result.set(it.message) + latch.countDown() + }) { + override fun getHeaders(): Map = mapOf("User-Agent" to "microG/${context.versionName}") + }) + latch.await(5, TimeUnit.SECONDS) + return result.get() + } + + private fun parseResponse(locale: Locale, result: JSONObject): Address? { + if (!result.has(WIRE_LATITUDE) || !result.has(WIRE_LONGITUDE) || + !result.has(WIRE_ADDRESS) + ) { + return null + } + Log.d(TAG, "Result: $result") + val address = Address(locale) + address.latitude = result.getDouble(WIRE_LATITUDE) + address.longitude = result.getDouble(WIRE_LONGITUDE) + val a = result.getJSONObject(WIRE_ADDRESS) + address.thoroughfare = a.optString(WIRE_THOROUGHFARE) + address.subLocality = a.optString(WIRE_SUBLOCALITY) + address.postalCode = a.optString(WIRE_POSTALCODE) + address.subAdminArea = a.optString(WIRE_SUBADMINAREA) + address.adminArea = a.optString(WIRE_ADMINAREA) + address.countryName = a.optString(WIRE_COUNTRYNAME) + address.countryCode = a.optString(WIRE_COUNTRYCODE) + if (a.has(WIRE_LOCALITY_CITY)) { + address.locality = a.getString(WIRE_LOCALITY_CITY) + } else if (a.has(WIRE_LOCALITY_TOWN)) { + address.locality = a.getString(WIRE_LOCALITY_TOWN) + } else if (a.has(WIRE_LOCALITY_VILLAGE)) { + address.locality = a.getString(WIRE_LOCALITY_VILLAGE) + } + if (formatter != null) { + val components = mutableMapOf() + for (s in a.keys()) { + if (s !in WIRE_IGNORED) { + components[s] = a[s].toString() + } + } + val split = formatter.formatAddress(components).split("\n") + for (i in split.indices) { + address.setAddressLine(i, split[i]) + } + address.featureName = formatter.guessName(components) + } + return address + } + + fun dump(writer: PrintWriter?) { + writer?.println("Enabled: ${settings.geocoderNominatim}") + writer?.println("Address cache: size=${addressCache.size()} hits=${addressCache.hitCount()} miss=${addressCache.missCount()} puts=${addressCache.putCount()} evicts=${addressCache.evictionCount()}") + } + + companion object { + private const val CACHE_SIZE = 200 + + private const val NOMINATIM_SERVER = "nominatim.openstreetmap.org" + + private const val WIRE_LATITUDE = "lat" + private const val WIRE_LONGITUDE = "lon" + private const val WIRE_ADDRESS = "address" + private const val WIRE_THOROUGHFARE = "road" + private const val WIRE_SUBLOCALITY = "suburb" + private const val WIRE_POSTALCODE = "postcode" + private const val WIRE_LOCALITY_CITY = "city" + private const val WIRE_LOCALITY_TOWN = "town" + private const val WIRE_LOCALITY_VILLAGE = "village" + private const val WIRE_SUBADMINAREA = "county" + private const val WIRE_ADMINAREA = "state" + private const val WIRE_COUNTRYNAME = "country" + private const val WIRE_COUNTRYCODE = "country_code" + + private val WIRE_IGNORED = setOf("ISO3166-2-lvl4") + + private data class CacheKey(val uid: Int, val packageName: String?, val locale: Locale, val latitude: Int, val longitude: Int) { + constructor(clientIdentity: ClientIdentity, locale: Locale, latitude: Double, longitude: Double) : this(clientIdentity.uid, clientIdentity.packageName.takeIf { clientIdentity.uid != 0 }, locale, (latitude * 100000.0).toInt(), (longitude * 100000.0).toInt()) + } + } +} \ No newline at end of file diff --git a/play-services-recaptcha/core/src/main/kotlin/org/microg/gms/recaptcha/RecaptchaGuardImpl.kt b/play-services-recaptcha/core/src/main/kotlin/org/microg/gms/recaptcha/RecaptchaGuardImpl.kt new file mode 100644 index 00000000..837ce3d9 --- /dev/null +++ b/play-services-recaptcha/core/src/main/kotlin/org/microg/gms/recaptcha/RecaptchaGuardImpl.kt @@ -0,0 +1,132 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.recaptcha + +import android.content.Context +import android.os.Build.VERSION.SDK_INT +import android.os.LocaleList +import android.util.Log +import com.android.volley.* +import com.android.volley.toolbox.Volley +import com.google.android.gms.recaptcha.RecaptchaHandle +import com.google.android.gms.recaptcha.RecaptchaResultData +import com.google.android.gms.recaptcha.internal.ExecuteParams +import com.google.android.gms.recaptcha.internal.InitParams +import com.squareup.wire.Message +import com.squareup.wire.ProtoAdapter +import kotlinx.coroutines.CompletableDeferred +import org.microg.gms.droidguard.core.DroidGuardResultCreator +import org.microg.gms.droidguard.core.VersionUtil +import org.microg.gms.utils.singleInstanceOf +import java.util.* +import kotlin.collections.HashMap + +private const val TAG = "RecaptchaGuard" + +class RecaptchaGuardImpl(private val context: Context, private val packageName: String) : RecaptchaImpl { + private val queue = singleInstanceOf { Volley.newRequestQueue(context.applicationContext) } + private var lastToken: String? = null + + override suspend fun init(params: InitParams): RecaptchaHandle { + val response = ProtobufPostRequest( + "https://www.recaptcha.net/recaptcha/api3/ac", RecaptchaInitRequest( + data_ = RecaptchaInitRequest.Data( + siteKey = params.siteKey, + packageName = packageName, + version = "${VersionUtil(context).versionCode};${params.version}" + ) + ), RecaptchaInitResponse.ADAPTER + ).sendAndAwait(queue) + lastToken = response.token + return RecaptchaHandle(params.siteKey, packageName, response.acceptableAdditionalArgs.toList()) + } + + override suspend fun execute(params: ExecuteParams): RecaptchaResultData { + if (params.handle.clientPackageName != null && params.handle.clientPackageName != packageName) throw IllegalArgumentException("invalid handle") + val timestamp = System.currentTimeMillis() + val additionalArgs = mutableMapOf() + val guardMap = mutableMapOf() + for (key in params.action.additionalArgs.keySet()) { + val value = params.action.additionalArgs.getString(key) + ?: throw Exception("Only string values are allowed as an additional arg in RecaptchaAction") + if (key !in params.handle.acceptableAdditionalArgs) + throw Exception("AdditionalArgs key[ \"$key\" ] is not accepted by reCATPCHA server") + additionalArgs.put(key, value) + } + Log.d(TAG, "Additional arguments: $additionalArgs") + if (lastToken == null) { + init(InitParams().apply { siteKey = params.handle.siteKey; version = params.version }) + } + val token = lastToken!! + guardMap["token"] = token + guardMap["action"] = params.action.toString() + guardMap["timestamp_millis"] to timestamp.toString() + guardMap.putAll(additionalArgs) + if (params.action.verificationHistoryToken != null) + guardMap["verification_history_token"] = params.action.verificationHistoryToken + val dg = DroidGuardResultCreator.getResults(context, "recaptcha-android", guardMap) + val response = ProtobufPostRequest( + "https://www.recaptcha.net/recaptcha/api3/ae", RecaptchaExecuteRequest( + token = token, + action = params.action.toString(), + timestamp = timestamp, + dg = dg, + additionalArgs = additionalArgs, + verificationHistoryToken = params.action.verificationHistoryToken + ), RecaptchaExecuteResponse.ADAPTER + ).sendAndAwait(queue) + return RecaptchaResultData(response.token) + } + + override suspend fun close(handle: RecaptchaHandle): Boolean { + if (handle.clientPackageName != null && handle.clientPackageName != packageName) throw IllegalArgumentException("invalid handle") + val closed = lastToken != null + lastToken = null + return closed + } +} + +class ProtobufPostRequest, O>(url: String, private val i: I, private val oAdapter: ProtoAdapter) : + Request(Request.Method.POST, url, null) { + private val deferred = CompletableDeferred() + + override fun getHeaders(): Map { + val headers = HashMap(super.getHeaders()) + headers["Accept-Language"] = if (SDK_INT >= 24) LocaleList.getDefault().toLanguageTags() else Locale.getDefault().language + return headers + } + + override fun getBody(): ByteArray = i.encode() + + override fun getBodyContentType(): String = "application/x-protobuf" + + override fun parseNetworkResponse(response: NetworkResponse): Response { + try { + return Response.success(oAdapter.decode(response.data), null) + } catch (e: VolleyError) { + return Response.error(e) + } catch (e: Exception) { + return Response.error(VolleyError()) + } + } + + override fun deliverResponse(response: O) { + Log.d(TAG, "Got response: $response") + deferred.complete(response) + } + + override fun deliverError(error: VolleyError) { + deferred.completeExceptionally(error) + } + + suspend fun await(): O = deferred.await() + + suspend fun sendAndAwait(queue: RequestQueue): O { + Log.d(TAG, "Sending request: $i") + queue.add(this) + return await() + } +} \ No newline at end of file From af9cfa6b53422a6a5ac9c36785a91a540527e2a3 Mon Sep 17 00:00:00 2001 From: Marvin W Date: Sat, 17 Feb 2024 20:32:48 +0100 Subject: [PATCH 053/209] Cleanup PoToken and Audit --- .../src/main/AndroidManifest.xml | 4 +- .../potokens/internal/PoTokensApiService.kt | 32 --------- .../internal/PoTokensApiServiceImpl.kt | 41 ----------- .../org/microg/gms/audit/AuditService.kt | 45 ++++++++++++ .../microg/gms/potokens}/PoTokenConstants.kt | 10 ++- .../microg/gms/potokens}/PoTokenHelper.kt | 5 +- .../gms/potokens}/PoTokenPreferences.kt | 8 +-- .../microg/gms/potokens/PoTokensService.kt | 69 +++++++++++++++++++ 8 files changed, 125 insertions(+), 89 deletions(-) delete mode 100644 play-services-core/src/main/kotlin/com/google/android/gms/potokens/internal/PoTokensApiService.kt delete mode 100644 play-services-core/src/main/kotlin/com/google/android/gms/potokens/internal/PoTokensApiServiceImpl.kt create mode 100644 play-services-core/src/main/kotlin/org/microg/gms/audit/AuditService.kt rename play-services-core/src/main/kotlin/{com/google/android/gms/potokens/utils => org/microg/gms/potokens}/PoTokenConstants.kt (57%) rename play-services-core/src/main/kotlin/{com/google/android/gms/potokens/utils => org/microg/gms/potokens}/PoTokenHelper.kt (98%) rename play-services-core/src/main/kotlin/{com/google/android/gms/potokens/utils => org/microg/gms/potokens}/PoTokenPreferences.kt (84%) create mode 100644 play-services-core/src/main/kotlin/org/microg/gms/potokens/PoTokensService.kt diff --git a/play-services-core/src/main/AndroidManifest.xml b/play-services-core/src/main/AndroidManifest.xml index 20017ded..eccb6cad 100644 --- a/play-services-core/src/main/AndroidManifest.xml +++ b/play-services-core/src/main/AndroidManifest.xml @@ -509,7 +509,7 @@ - + @@ -648,7 +648,7 @@ - + diff --git a/play-services-core/src/main/kotlin/com/google/android/gms/potokens/internal/PoTokensApiService.kt b/play-services-core/src/main/kotlin/com/google/android/gms/potokens/internal/PoTokensApiService.kt deleted file mode 100644 index 483020a0..00000000 --- a/play-services-core/src/main/kotlin/com/google/android/gms/potokens/internal/PoTokensApiService.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2023 microG Project Team - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.google.android.gms.potokens.internal - -import android.util.Log -import androidx.lifecycle.lifecycleScope -import com.google.android.gms.common.Feature -import com.google.android.gms.common.api.CommonStatusCodes -import com.google.android.gms.common.internal.ConnectionInfo -import com.google.android.gms.common.internal.GetServiceRequest -import com.google.android.gms.common.internal.IGmsCallbacks -import com.google.android.gms.potokens.utils.PoTokenConstants -import org.microg.gms.BaseService -import org.microg.gms.common.GmsService - -const val TAG = "PoTokensApi" - -class PoTokensApiService : BaseService(TAG, GmsService.POTOKENS) { - override fun handleServiceRequest(callback: IGmsCallbacks, request: GetServiceRequest, service: GmsService) { - val connectionInfo = ConnectionInfo() - connectionInfo.features = arrayOf(Feature(PoTokenConstants.PO_TOKENS, 1)) - Log.d(TAG, "PoTokensApiService handleServiceRequest") - callback.onPostInitCompleteWithConnectionInfo( - CommonStatusCodes.SUCCESS, PoTokensApiServiceImpl( - applicationContext, request.packageName, lifecycleScope - ), connectionInfo - ) - } -} diff --git a/play-services-core/src/main/kotlin/com/google/android/gms/potokens/internal/PoTokensApiServiceImpl.kt b/play-services-core/src/main/kotlin/com/google/android/gms/potokens/internal/PoTokensApiServiceImpl.kt deleted file mode 100644 index b7c8100b..00000000 --- a/play-services-core/src/main/kotlin/com/google/android/gms/potokens/internal/PoTokensApiServiceImpl.kt +++ /dev/null @@ -1,41 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2023 microG Project Team - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.google.android.gms.potokens.internal - -import android.content.Context -import android.util.Log -import androidx.lifecycle.LifecycleCoroutineScope -import com.google.android.gms.common.api.Status -import com.google.android.gms.common.api.internal.IStatusCallback -import com.google.android.gms.potokens.PoToken -import com.google.android.gms.potokens.utils.PoTokenHelper -import org.microg.gms.profile.ProfileManager - -class PoTokensApiServiceImpl( - private val context: Context, - private val packageName: String, - private val lifecycleCoroutineScope: LifecycleCoroutineScope -) : IPoTokensService.Stub() { - - override fun responseStatus(call: IStatusCallback, code: Int) { - Log.e(TAG, "responseStatus success") - call.onResult(Status.SUCCESS) - } - - override fun responseStatusToken(call: ITokenCallbacks, i: Int, bArr: ByteArray) { - Log.d(TAG, "responseStatusToken packageName: $packageName") - ProfileManager.ensureInitialized(context) - - lifecycleCoroutineScope.launchWhenCreated { - Log.d(TAG, "responseStatusToken start") - val bytes = PoTokenHelper.get(context).callPoToken(context, packageName, bArr) - Log.d(TAG, "responseStatusToken result: ${bytes.size}") - call.responseToken(Status.SUCCESS, PoToken(bytes)) - Log.d(TAG, "responseStatusToken end") - } - } - -} \ No newline at end of file diff --git a/play-services-core/src/main/kotlin/org/microg/gms/audit/AuditService.kt b/play-services-core/src/main/kotlin/org/microg/gms/audit/AuditService.kt new file mode 100644 index 00000000..d36ef999 --- /dev/null +++ b/play-services-core/src/main/kotlin/org/microg/gms/audit/AuditService.kt @@ -0,0 +1,45 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.audit + +import android.util.Log +import com.google.android.gms.audit.LogAuditRecordsRequest +import com.google.android.gms.audit.internal.IAuditService +import com.google.android.gms.common.ConnectionResult +import com.google.android.gms.common.api.Status +import com.google.android.gms.common.api.internal.IStatusCallback +import com.google.android.gms.common.internal.GetServiceRequest +import com.google.android.gms.common.internal.IGmsCallbacks +import org.microg.gms.BaseService +import org.microg.gms.common.GmsService + +private const val TAG = "AuditApiService" + +class AuditService : BaseService(TAG, GmsService.AUDIT) { + override fun handleServiceRequest(callback: IGmsCallbacks, request: GetServiceRequest, service: GmsService) { + callback.onPostInitComplete(ConnectionResult.SUCCESS, AuditServiceImpl().asBinder(), null) + } + +} + +class AuditServiceImpl : IAuditService.Stub() { + + override fun logAuditRecords(request: LogAuditRecordsRequest?, callback: IStatusCallback) { + Log.d(TAG, "method 'logAuditRecords' not fully implemented, only return Status.SUCCESS") + when (request?.writeMode) { + 1 -> { + callback.onResult(Status.SUCCESS) + } + 2 -> { + callback.onResult(Status.SUCCESS_CACHE) + } + else -> { + callback.onResult(Status.SUCCESS) + } + } + } + +} \ No newline at end of file diff --git a/play-services-core/src/main/kotlin/com/google/android/gms/potokens/utils/PoTokenConstants.kt b/play-services-core/src/main/kotlin/org/microg/gms/potokens/PoTokenConstants.kt similarity index 57% rename from play-services-core/src/main/kotlin/com/google/android/gms/potokens/utils/PoTokenConstants.kt rename to play-services-core/src/main/kotlin/org/microg/gms/potokens/PoTokenConstants.kt index 09aefc9a..90e21146 100644 --- a/play-services-core/src/main/kotlin/com/google/android/gms/potokens/utils/PoTokenConstants.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/potokens/PoTokenConstants.kt @@ -1,14 +1,12 @@ /* - * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-FileCopyrightText: 2024 microG Project Team * SPDX-License-Identifier: Apache-2.0 */ -package com.google.android.gms.potokens.utils +package org.microg.gms.potokens object PoTokenConstants { - const val PO_TOKENS = "PO_TOKENS" - const val TOKEN_URL = - "https://deviceintegritytokens-pa.googleapis.com/v1/getPoIntegrityToken?alt=proto&key=AIzaSyBtL0AK6Hzgr69rQyeyhi-V1lmtsPGZd1M" + const val TOKEN_URL = "https://deviceintegritytokens-pa.googleapis.com/v1/getPoIntegrityToken?alt=proto&key=AIzaSyBtL0AK6Hzgr69rQyeyhi-V1lmtsPGZd1M" const val TYPE_URL = "type.googleapis.com/google.crypto.tink.AesGcmKey" const val KEY_TOKEN = "po-token-fast" const val KEY_FAST = "po-fast-key" @@ -16,4 +14,4 @@ object PoTokenConstants { const val KEY_DESC = "tokenDesc" const val KEY_BACKUP = "tokenBackup" const val KEY_SET_STR = "keySetStr" -} \ No newline at end of file +} diff --git a/play-services-core/src/main/kotlin/com/google/android/gms/potokens/utils/PoTokenHelper.kt b/play-services-core/src/main/kotlin/org/microg/gms/potokens/PoTokenHelper.kt similarity index 98% rename from play-services-core/src/main/kotlin/com/google/android/gms/potokens/utils/PoTokenHelper.kt rename to play-services-core/src/main/kotlin/org/microg/gms/potokens/PoTokenHelper.kt index e9d6634a..5002367f 100644 --- a/play-services-core/src/main/kotlin/com/google/android/gms/potokens/utils/PoTokenHelper.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/potokens/PoTokenHelper.kt @@ -1,9 +1,9 @@ /* - * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-FileCopyrightText: 2024 microG Project Team * SPDX-License-Identifier: Apache-2.0 */ -package com.google.android.gms.potokens.utils +package org.microg.gms.potokens import android.content.Context import android.text.TextUtils @@ -26,7 +26,6 @@ import com.google.android.gms.potokens.KeySet import com.google.android.gms.potokens.PoTokenInfo import com.google.android.gms.potokens.PoTokenResult import com.google.android.gms.potokens.PoTokenResultWrap -import com.google.android.gms.potokens.internal.TAG import com.google.android.gms.tasks.Tasks import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext diff --git a/play-services-core/src/main/kotlin/com/google/android/gms/potokens/utils/PoTokenPreferences.kt b/play-services-core/src/main/kotlin/org/microg/gms/potokens/PoTokenPreferences.kt similarity index 84% rename from play-services-core/src/main/kotlin/com/google/android/gms/potokens/utils/PoTokenPreferences.kt rename to play-services-core/src/main/kotlin/org/microg/gms/potokens/PoTokenPreferences.kt index 3eca2cd3..6e0acfac 100644 --- a/play-services-core/src/main/kotlin/com/google/android/gms/potokens/utils/PoTokenPreferences.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/potokens/PoTokenPreferences.kt @@ -1,14 +1,12 @@ /* - * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-FileCopyrightText: 2024 microG Project Team * SPDX-License-Identifier: Apache-2.0 */ -package com.google.android.gms.potokens.utils +package org.microg.gms.potokens import android.content.Context import android.content.SharedPreferences -import com.android.volley.toolbox.Volley -import org.microg.gms.droidguard.core.HandleProxyFactory class PoTokenPreferences(context: Context) { @@ -44,4 +42,4 @@ class PoTokenPreferences(context: Context) { } } } -} \ No newline at end of file +} diff --git a/play-services-core/src/main/kotlin/org/microg/gms/potokens/PoTokensService.kt b/play-services-core/src/main/kotlin/org/microg/gms/potokens/PoTokensService.kt new file mode 100644 index 00000000..7c12604f --- /dev/null +++ b/play-services-core/src/main/kotlin/org/microg/gms/potokens/PoTokensService.kt @@ -0,0 +1,69 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.potokens + +import android.content.Context +import android.util.Log +import androidx.lifecycle.LifecycleCoroutineScope +import androidx.lifecycle.lifecycleScope +import com.google.android.gms.common.Feature +import com.google.android.gms.common.api.CommonStatusCodes +import com.google.android.gms.common.api.Status +import com.google.android.gms.common.api.internal.IStatusCallback +import com.google.android.gms.common.internal.ConnectionInfo +import com.google.android.gms.common.internal.GetServiceRequest +import com.google.android.gms.common.internal.IGmsCallbacks +import com.google.android.gms.potokens.PoToken +import com.google.android.gms.potokens.internal.IPoTokensService +import com.google.android.gms.potokens.internal.ITokenCallbacks +import org.microg.gms.BaseService +import org.microg.gms.common.GmsService +import org.microg.gms.profile.ProfileManager + +const val TAG = "PoTokens" +private val FEATURES = arrayOf(Feature("PO_TOKENS", 1)) + +class PoTokensService : BaseService(TAG, GmsService.POTOKENS) { + override fun handleServiceRequest(callback: IGmsCallbacks, request: GetServiceRequest, service: GmsService) { + Log.d(TAG, "PoTokensApiService handleServiceRequest") + callback.onPostInitCompleteWithConnectionInfo( + CommonStatusCodes.SUCCESS, + PoTokensServiceImpl( + applicationContext, request.packageName, lifecycleScope + ), + ConnectionInfo().apply { features = FEATURES } + ) + } +} + + +class PoTokensServiceImpl( + private val context: Context, + private val packageName: String, + private val lifecycleCoroutineScope: LifecycleCoroutineScope +) : IPoTokensService.Stub() { + + override fun responseStatus(call: IStatusCallback, code: Int) { + Log.e(TAG, "responseStatus success") + call.onResult(Status.SUCCESS) + } + + override fun responseStatusToken(call: ITokenCallbacks, i: Int, bArr: ByteArray) { + Log.d(TAG, "responseStatusToken packageName: $packageName") + ProfileManager.ensureInitialized(context) + + lifecycleCoroutineScope.launchWhenCreated { + Log.d(TAG, "responseStatusToken start") + runCatching { + val bytes = PoTokenHelper.get(context).callPoToken(context, packageName, bArr) + Log.d(TAG, "responseStatusToken result: ${bytes.size}") + call.responseToken(Status.SUCCESS, PoToken(bytes)) + } + Log.d(TAG, "responseStatusToken end") + } + } + +} From 7af2623d269b882dbbe4e5ebe5743e813c9444bc Mon Sep 17 00:00:00 2001 From: Marvin W Date: Mon, 19 Feb 2024 20:42:13 +0100 Subject: [PATCH 054/209] License Check: Correctly check callingUid when having multiple accounts Also fixes crash if callingUid check fails on V1 checker. --- .../vending/licensing/LicenseChecker.java | 8 ++++---- .../vending/licensing/LicensingService.java | 20 ++++++++++--------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/vending-app/src/main/java/com/android/vending/licensing/LicenseChecker.java b/vending-app/src/main/java/com/android/vending/licensing/LicenseChecker.java index 83e76c2f..3421a8f7 100644 --- a/vending-app/src/main/java/com/android/vending/licensing/LicenseChecker.java +++ b/vending-app/src/main/java/com/android/vending/licensing/LicenseChecker.java @@ -83,7 +83,7 @@ public abstract class LicenseChecker { BiConsumer then, Response.ErrorListener errorListener); public void checkLicense(Account account, AccountManager accountManager, String androidId, - String packageName, PackageManager packageManager, + String packageName, int callingUid, PackageManager packageManager, RequestQueue queue, D queryData, BiConsumerWithException onResult) throws RemoteException { @@ -92,10 +92,10 @@ public abstract class LicenseChecker { int versionCode = packageInfo.versionCode; // Verify caller identity - if (packageInfo.applicationInfo.uid != getCallingUid()) { + if (packageInfo.applicationInfo.uid != callingUid) { Log.e(TAG, - "an app illegally tried to request licenses for another app (caller: " + getCallingUid() + ")"); - onResult.accept(ERROR_NON_MATCHING_UID, null); + "an app illegally tried to request licenses for another app (caller: " + callingUid + ")"); + safeSendResult(onResult, ERROR_NON_MATCHING_UID, null); } else { BiConsumer onRequestFinished = (Integer integer, R r) -> { diff --git a/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java b/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java index c66e9b72..5042c3fc 100644 --- a/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java +++ b/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java @@ -84,6 +84,7 @@ public class LicensingService extends Service { @Override public void checkLicense(long nonce, String packageName, ILicenseResultListener listener) throws RemoteException { Log.v(TAG, "checkLicense(" + nonce + ", " + packageName + ")"); + int callingUid = getCallingUid(); if (!shouldCheckLicense()) { Log.d(TAG, "not checking license, as it is disabled by user"); @@ -96,20 +97,20 @@ public class LicensingService extends Service { if (accounts.length == 0) { handleNoAccounts(packageName, packageManager); } else { - checkLicense(nonce, packageName, packageManager, listener, new LinkedList<>(Arrays.asList(accounts))); + checkLicense(callingUid, nonce, packageName, packageManager, listener, new LinkedList<>(Arrays.asList(accounts))); } } - private void checkLicense(long nonce, String packageName, PackageManager packageManager, + private void checkLicense(int callingUid, long nonce, String packageName, PackageManager packageManager, ILicenseResultListener listener, Queue remainingAccounts) throws RemoteException { new LicenseChecker.V1().checkLicense( - remainingAccounts.poll(), accountManager, androidId, packageName, packageManager, + remainingAccounts.poll(), accountManager, androidId, packageName, callingUid, packageManager, queue, nonce, (responseCode, stringTuple) -> { if (responseCode != LICENSED && !remainingAccounts.isEmpty()) { - checkLicense(nonce, packageName, packageManager, listener, remainingAccounts); + checkLicense(callingUid, nonce, packageName, packageManager, listener, remainingAccounts); } else { - listener.verifyLicense(responseCode, stringTuple.a, stringTuple.b); + listener.verifyLicense(responseCode, stringTuple != null ? stringTuple.a : null, stringTuple != null ? stringTuple.b : null); } } ); @@ -118,6 +119,7 @@ public class LicensingService extends Service { @Override public void checkLicenseV2(String packageName, ILicenseV2ResultListener listener, Bundle extraParams) throws RemoteException { Log.v(TAG, "checkLicenseV2(" + packageName + ", " + extraParams + ")"); + int callingUid = getCallingUid(); if (!shouldCheckLicense()) { Log.d(TAG, "not checking license, as it is disabled by user"); @@ -130,15 +132,15 @@ public class LicensingService extends Service { if (accounts.length == 0) { handleNoAccounts(packageName, packageManager); } else { - checkLicenseV2(packageName, packageManager, listener, extraParams, new LinkedList<>(Arrays.asList(accounts))); + checkLicenseV2(callingUid, packageName, packageManager, listener, extraParams, new LinkedList<>(Arrays.asList(accounts))); } } - private void checkLicenseV2(String packageName, PackageManager packageManager, + private void checkLicenseV2(int callingUid, String packageName, PackageManager packageManager, ILicenseV2ResultListener listener, Bundle extraParams, Queue remainingAccounts) throws RemoteException { new LicenseChecker.V2().checkLicense( - remainingAccounts.poll(), accountManager, androidId, packageName, packageManager, queue, Unit.INSTANCE, + remainingAccounts.poll(), accountManager, androidId, packageName, callingUid, packageManager, queue, Unit.INSTANCE, (responseCode, data) -> { /* * Suppress failures on V2. V2 is commonly used by free apps whose checker @@ -153,7 +155,7 @@ public class LicensingService extends Service { listener.verifyLicense(responseCode, bundle); } else if (!remainingAccounts.isEmpty()) { - checkLicenseV2(packageName, packageManager, listener, extraParams, remainingAccounts); + checkLicenseV2(callingUid, packageName, packageManager, listener, extraParams, remainingAccounts); } else { Log.i(TAG, "Suppressed negative license result for package " + packageName); } From 9f043e25668d1e79b87edfa21505e51a5617b245 Mon Sep 17 00:00:00 2001 From: Marvin W Date: Mon, 19 Feb 2024 21:02:20 +0100 Subject: [PATCH 055/209] UI: Port more to Material 3 This also activates Material 3 (with fixed colors) for older Android versions. We don't want to maintain multiple designs, so the most reasonable approach is to use a single design for all, but focus on the best UX for more recent versions (as those are used by more people). --- .../org/microg/gms/ui/SwitchBarPreference.kt | 62 ++++++++++ .../res/layout/preference_app_heading.xml | 46 +++++++ .../core/src/main/res/values/themes.xml | 52 ++++++++ .../org/microg/gms/ui/SettingsActivity.java | 18 ++- .../org/microg/gms/ui/SafetyNetFragment.kt | 2 +- .../color/material_switch_bar_text_color.xml | 9 ++ .../material_switch_bar_background.xml | 14 +++ ...aterial_switch_bar_background_disabled.xml | 14 +++ .../material_switch_bar_background.xml | 9 ++ ...aterial_switch_bar_background_disabled.xml | 9 ++ .../res/layout/preference_material_switch.xml | 10 ++ .../layout/preference_material_switch_bar.xml | 58 +++++++++ .../res/layout/settings_root_activity.xml | 49 ++++++-- .../src/main/res/values-night-v8/themes.xml | 59 +++++++++ .../src/main/res/values-v23/themes.xml | 11 ++ .../src/main/res/values-v27/themes.xml | 3 + .../src/main/res/values-v31/themes.xml | 5 + .../src/main/res/values/colors.xml | 54 ++++++++ .../src/main/res/values/themes.xml | 116 +++++++++++++++--- .../src/main/res/xml/preferences_account.xml | 19 +++ .../main/res/xml/preferences_gcm_advanced.xml | 10 ++ .../main/res/xml/preferences_google_more.xml | 16 +++ .../preferences_push_notifications_app.xml | 12 +- .../res/xml/preferences_safetynet_app.xml | 24 ++++ .../src/main/res/xml/preferences_vending.xml | 20 +++ .../xml/preferences_location_app_details.xml | 34 +++++ 26 files changed, 711 insertions(+), 24 deletions(-) create mode 100644 play-services-base/core/src/main/kotlin/org/microg/gms/ui/SwitchBarPreference.kt create mode 100644 play-services-base/core/src/main/res/layout/preference_app_heading.xml create mode 100644 play-services-base/core/src/main/res/values/themes.xml create mode 100644 play-services-core/src/main/res/color/material_switch_bar_text_color.xml create mode 100644 play-services-core/src/main/res/drawable-v21/material_switch_bar_background.xml create mode 100644 play-services-core/src/main/res/drawable-v21/material_switch_bar_background_disabled.xml create mode 100644 play-services-core/src/main/res/drawable/material_switch_bar_background.xml create mode 100644 play-services-core/src/main/res/drawable/material_switch_bar_background_disabled.xml create mode 100644 play-services-core/src/main/res/layout/preference_material_switch.xml create mode 100644 play-services-core/src/main/res/layout/preference_material_switch_bar.xml create mode 100644 play-services-core/src/main/res/values-night-v8/themes.xml create mode 100644 play-services-core/src/main/res/values-v23/themes.xml create mode 100644 play-services-core/src/main/res/values-v27/themes.xml create mode 100644 play-services-core/src/main/res/xml/preferences_safetynet_app.xml create mode 100644 play-services-core/src/main/res/xml/preferences_vending.xml create mode 100644 play-services-location/core/src/main/res/xml/preferences_location_app_details.xml diff --git a/play-services-base/core/src/main/kotlin/org/microg/gms/ui/SwitchBarPreference.kt b/play-services-base/core/src/main/kotlin/org/microg/gms/ui/SwitchBarPreference.kt new file mode 100644 index 00000000..446be354 --- /dev/null +++ b/play-services-base/core/src/main/kotlin/org/microg/gms/ui/SwitchBarPreference.kt @@ -0,0 +1,62 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.ui + +import android.content.Context +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import androidx.appcompat.widget.SwitchCompat +import androidx.core.content.res.TypedArrayUtils +import androidx.preference.PreferenceViewHolder +import androidx.preference.TwoStatePreference +import org.microg.gms.base.core.R + +// TODO +class SwitchBarPreference : TwoStatePreference { + private val frameId: Int + private val backgroundOn: Drawable? + private val backgroundOff: Drawable? + private val backgroundDisabled: Drawable? + + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) { + val a = context.obtainStyledAttributes(attrs, R.styleable.SwitchBarPreference, defStyleAttr, defStyleRes) + frameId = a.getResourceId(R.styleable.SwitchBarPreference_switchBarFrameId, 0) + backgroundOn = a.getDrawable(R.styleable.SwitchBarPreference_switchBarFrameBackgroundOn) + backgroundOff = a.getDrawable(R.styleable.SwitchBarPreference_switchBarFrameBackgroundOff) + backgroundDisabled = a.getDrawable(R.styleable.SwitchBarPreference_switchBarFrameBackgroundDisabled) + } + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : this(context, attrs, defStyleAttr, R.style.Preference_SwitchBar) + constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, R.attr.switchBarPreferenceStyle) + constructor(context: Context) : this(context, null) + + override fun onBindViewHolder(holder: PreferenceViewHolder) { + super.onBindViewHolder(holder) + holder.isDividerAllowedBelow = false + holder.isDividerAllowedAbove = false + val switch = holder.findViewById(R.id.switch_widget) as SwitchCompat + switch.setOnCheckedChangeListener(null) + switch.isChecked = isChecked + switch.setOnCheckedChangeListener { view, isChecked -> + if (!callChangeListener(isChecked)) { + view.isChecked = !isChecked + return@setOnCheckedChangeListener + } + this.isChecked = isChecked + } + val frame = if (frameId == 0) null else holder.findViewById(frameId) + val backgroundView = frame ?: holder.itemView + val (backgroundDrawable, backgroundColorAttribute) = when { + !isEnabled -> Pair(backgroundDisabled, androidx.appcompat.R.attr.colorControlHighlight) + isChecked -> Pair(backgroundOn, androidx.appcompat.R.attr.colorControlActivated) + else -> Pair(backgroundOff, androidx.appcompat.R.attr.colorButtonNormal) + } + if (backgroundDrawable != null) { + backgroundView.setBackgroundDrawable(backgroundDrawable) + } else { + backgroundView.setBackgroundColorAttribute(backgroundColorAttribute) + } + } +} \ No newline at end of file diff --git a/play-services-base/core/src/main/res/layout/preference_app_heading.xml b/play-services-base/core/src/main/res/layout/preference_app_heading.xml new file mode 100644 index 00000000..1a229987 --- /dev/null +++ b/play-services-base/core/src/main/res/layout/preference_app_heading.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/play-services-base/core/src/main/res/values/themes.xml b/play-services-base/core/src/main/res/values/themes.xml new file mode 100644 index 00000000..be7e8ff4 --- /dev/null +++ b/play-services-base/core/src/main/res/values/themes.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/play-services-core/src/main/java/org/microg/gms/ui/SettingsActivity.java b/play-services-core/src/main/java/org/microg/gms/ui/SettingsActivity.java index 32230055..3f129006 100755 --- a/play-services-core/src/main/java/org/microg/gms/ui/SettingsActivity.java +++ b/play-services-core/src/main/java/org/microg/gms/ui/SettingsActivity.java @@ -10,8 +10,14 @@ import androidx.navigation.fragment.NavHostFragment; import androidx.navigation.ui.AppBarConfiguration; import androidx.navigation.ui.NavigationUI; +<<<<<<< HEAD:play-services-core/src/main/java/org/microg/gms/ui/SettingsActivity.java import com.google.android.material.color.DynamicColors; import com.mgoogle.android.gms.R; +======= +import com.google.android.gms.R; +import com.google.android.material.appbar.CollapsingToolbarLayout; +import org.microg.gms.ui.settings.SettingsProvider; +>>>>>>> 566215bd (UI: Port more to Material 3):play-services-core/src/main/java/org/microg/gms/ui/MainSettingsActivity.java import java.util.Objects; @@ -26,11 +32,21 @@ public class SettingsActivity extends AppCompatActivity { protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.settings_root_activity); +<<<<<<< HEAD:play-services-core/src/main/java/org/microg/gms/ui/SettingsActivity.java if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { DynamicColors.applyToActivityIfAvailable(this); +======= + + Toolbar toolbar = findViewById(R.id.toolbar); + CollapsingToolbarLayout toolbarLayout = findViewById(R.id.collapsing_toolbar); + setSupportActionBar(toolbar); + + for (SettingsProvider settingsProvider : getAllSettingsProviders(this)) { + settingsProvider.extendNavigation(getNavController()); +>>>>>>> 566215bd (UI: Port more to Material 3):play-services-core/src/main/java/org/microg/gms/ui/MainSettingsActivity.java } appBarConfiguration = new AppBarConfiguration.Builder(getNavController().getGraph()).build(); - NavigationUI.setupActionBarWithNavController(this, getNavController(), appBarConfiguration); + NavigationUI.setupWithNavController(toolbarLayout, toolbar, getNavController(), appBarConfiguration); } @Override diff --git a/play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetFragment.kt b/play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetFragment.kt index d9339a55..e2a0090d 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetFragment.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetFragment.kt @@ -78,7 +78,7 @@ class SafetyNetFragment : PreferenceFragmentCompat() { val newStatus = newValue as Boolean SafetyNetPreferences.setEnabled(requireContext(), newStatus) DroidGuardPreferences.setEnabled(requireContext(), newStatus) - droidguardUnsupported.isVisible = switchBarPreference.isChecked && !DroidGuardPreferences.isAvailable(requireContext()) + droidguardUnsupported.isVisible = newStatus && !DroidGuardPreferences.isAvailable(requireContext()) true } } diff --git a/play-services-core/src/main/res/color/material_switch_bar_text_color.xml b/play-services-core/src/main/res/color/material_switch_bar_text_color.xml new file mode 100644 index 00000000..9f2dc812 --- /dev/null +++ b/play-services-core/src/main/res/color/material_switch_bar_text_color.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/play-services-core/src/main/res/drawable-v21/material_switch_bar_background.xml b/play-services-core/src/main/res/drawable-v21/material_switch_bar_background.xml new file mode 100644 index 00000000..6c98b244 --- /dev/null +++ b/play-services-core/src/main/res/drawable-v21/material_switch_bar_background.xml @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/play-services-core/src/main/res/drawable-v21/material_switch_bar_background_disabled.xml b/play-services-core/src/main/res/drawable-v21/material_switch_bar_background_disabled.xml new file mode 100644 index 00000000..15613e79 --- /dev/null +++ b/play-services-core/src/main/res/drawable-v21/material_switch_bar_background_disabled.xml @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/play-services-core/src/main/res/drawable/material_switch_bar_background.xml b/play-services-core/src/main/res/drawable/material_switch_bar_background.xml new file mode 100644 index 00000000..f86ccede --- /dev/null +++ b/play-services-core/src/main/res/drawable/material_switch_bar_background.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/play-services-core/src/main/res/drawable/material_switch_bar_background_disabled.xml b/play-services-core/src/main/res/drawable/material_switch_bar_background_disabled.xml new file mode 100644 index 00000000..3704056d --- /dev/null +++ b/play-services-core/src/main/res/drawable/material_switch_bar_background_disabled.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/play-services-core/src/main/res/layout/preference_material_switch.xml b/play-services-core/src/main/res/layout/preference_material_switch.xml new file mode 100644 index 00000000..1c731b85 --- /dev/null +++ b/play-services-core/src/main/res/layout/preference_material_switch.xml @@ -0,0 +1,10 @@ + + + diff --git a/play-services-core/src/main/res/layout/preference_material_switch_bar.xml b/play-services-core/src/main/res/layout/preference_material_switch_bar.xml new file mode 100644 index 00000000..fc5ee795 --- /dev/null +++ b/play-services-core/src/main/res/layout/preference_material_switch_bar.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + diff --git a/play-services-core/src/main/res/layout/settings_root_activity.xml b/play-services-core/src/main/res/layout/settings_root_activity.xml index d854b3a3..05da4223 100755 --- a/play-services-core/src/main/res/layout/settings_root_activity.xml +++ b/play-services-core/src/main/res/layout/settings_root_activity.xml @@ -1,19 +1,54 @@ - - - + android:orientation="vertical" + android:fitsSystemWindows="true"> +<<<<<<< HEAD + + + + + + + + + + >>>>>> 566215bd (UI: Port more to Material 3) android:layout_width="match_parent" android:layout_height="match_parent" - app:defaultNavHost="true" - app:navGraph="@navigation/nav_settings" /> - + android:fillViewport="true" + app:layout_behavior="@string/appbar_scrolling_view_behavior"> + + + + diff --git a/play-services-core/src/main/res/values-night-v8/themes.xml b/play-services-core/src/main/res/values-night-v8/themes.xml new file mode 100644 index 00000000..3ed65363 --- /dev/null +++ b/play-services-core/src/main/res/values-night-v8/themes.xml @@ -0,0 +1,59 @@ + + + + + \ No newline at end of file diff --git a/play-services-core/src/main/res/values-v23/themes.xml b/play-services-core/src/main/res/values-v23/themes.xml new file mode 100644 index 00000000..236d4087 --- /dev/null +++ b/play-services-core/src/main/res/values-v23/themes.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/play-services-core/src/main/res/values-v27/themes.xml b/play-services-core/src/main/res/values-v27/themes.xml new file mode 100644 index 00000000..045e125f --- /dev/null +++ b/play-services-core/src/main/res/values-v27/themes.xml @@ -0,0 +1,3 @@ + + + diff --git a/play-services-core/src/main/res/values-v31/themes.xml b/play-services-core/src/main/res/values-v31/themes.xml index 1719a989..009a86db 100644 --- a/play-services-core/src/main/res/values-v31/themes.xml +++ b/play-services-core/src/main/res/values-v31/themes.xml @@ -1,4 +1,5 @@ +<<<<<<< HEAD diff --git a/play-services-core/src/main/res/values/colors.xml b/play-services-core/src/main/res/values/colors.xml index 12402999..79d019ed 100755 --- a/play-services-core/src/main/res/values/colors.xml +++ b/play-services-core/src/main/res/values/colors.xml @@ -16,9 +16,63 @@ +<<<<<<< HEAD #ff9a82db #ff9a82db #ff9a82db #ff9a82db +======= + #88CCCCCC + #fff44336 + + #ff410e0b + #ffffffff + #ff601410 + #ff8c1d18 + #ffb3261e + #ffe46962 + #fff2b8b5 + #fff9dedc + #ff474747 + #ff757575 + #ff1f1f1f + #ffffffff + #ff303030 + #ff131314 + #ffe3e3e3 + #fff2f2f2 + #ff2d312f + #ff444746 + #ff5c5f5e + #ff747775 + #ff8e918f + #ffa9acaa + #ffc4c7c5 + #ffe1e3e1 + #ff041e49 + #ffffffff + #ff062e6f + #ff0842a0 + #ff0b57d0 + #ff7cacf8 + #ffa8c7fa + #ffd3e3fd + #ff001d35 + #ffffffff + #ff003355 + #ff004a77 + #ff00639b + #ff5ab3f0 + #ff7fcfff + #ffc2e7ff + #ff072711 + #ffffffff + #ff0a3818 + #ff0f5223 + #ff146c2e + #ff6dd58c + #ffc4eed0 + #ffffffff +>>>>>>> 566215bd (UI: Port more to Material 3) diff --git a/play-services-core/src/main/res/values/themes.xml b/play-services-core/src/main/res/values/themes.xml index d5cb5705..c8ce1496 100755 --- a/play-services-core/src/main/res/values/themes.xml +++ b/play-services-core/src/main/res/values/themes.xml @@ -1,34 +1,122 @@ +<<<<<<< HEAD +======= + + + + + + + + + + + + + + + + + +>>>>>>> 566215bd (UI: Port more to Material 3) +<<<<<<< HEAD +======= +>>>>>>> 566215bd (UI: Port more to Material 3) diff --git a/play-services-core/src/main/res/xml/preferences_account.xml b/play-services-core/src/main/res/xml/preferences_account.xml index c952b6e0..4b564524 100755 --- a/play-services-core/src/main/res/xml/preferences_account.xml +++ b/play-services-core/src/main/res/xml/preferences_account.xml @@ -11,6 +11,7 @@ android:targetPackage="com.android.settings" /> +<<<<<<< HEAD +======= + + + + +>>>>>>> 566215bd (UI: Port more to Material 3) +<<<<<<< HEAD +======= + + +>>>>>>> 566215bd (UI: Port more to Material 3) diff --git a/play-services-core/src/main/res/xml/preferences_google_more.xml b/play-services-core/src/main/res/xml/preferences_google_more.xml index 879f0a18..ee6d2e74 100755 --- a/play-services-core/src/main/res/xml/preferences_google_more.xml +++ b/play-services-core/src/main/res/xml/preferences_google_more.xml @@ -16,6 +16,7 @@ --> +<<<<<<< HEAD +======= +>>>>>>> 566215bd (UI: Port more to Material 3) +======= + android:summary="If disabled, all connections to Google servers usually done by microG will be denied. This overrides service specific settings." + android:title="Allow connecting to Google servers"/> + + +>>>>>>> 566215bd (UI: Port more to Material 3) diff --git a/play-services-core/src/main/res/xml/preferences_push_notifications_app.xml b/play-services-core/src/main/res/xml/preferences_push_notifications_app.xml index 01d83e52..3ee717f7 100755 --- a/play-services-core/src/main/res/xml/preferences_push_notifications_app.xml +++ b/play-services-core/src/main/res/xml/preferences_push_notifications_app.xml @@ -4,8 +4,18 @@ ~ SPDX-License-Identifier: Apache-2.0 --> + xmlns:tools="http://schemas.android.com/tools" + xmlns:app="http://schemas.android.com/apk/res-auto"> +<<<<<<< HEAD +======= + + +>>>>>>> 566215bd (UI: Port more to Material 3) diff --git a/play-services-core/src/main/res/xml/preferences_safetynet_app.xml b/play-services-core/src/main/res/xml/preferences_safetynet_app.xml new file mode 100644 index 00000000..7b3a6a9a --- /dev/null +++ b/play-services-core/src/main/res/xml/preferences_safetynet_app.xml @@ -0,0 +1,24 @@ + + + + + + + + + + diff --git a/play-services-core/src/main/res/xml/preferences_vending.xml b/play-services-core/src/main/res/xml/preferences_vending.xml new file mode 100644 index 00000000..2d7d0f03 --- /dev/null +++ b/play-services-core/src/main/res/xml/preferences_vending.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + diff --git a/play-services-location/core/src/main/res/xml/preferences_location_app_details.xml b/play-services-location/core/src/main/res/xml/preferences_location_app_details.xml new file mode 100644 index 00000000..a6a52745 --- /dev/null +++ b/play-services-location/core/src/main/res/xml/preferences_location_app_details.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + \ No newline at end of file From cc0799dfc64218256366450dcbe8ce8c05449d49 Mon Sep 17 00:00:00 2001 From: Marvin W Date: Tue, 20 Feb 2024 13:23:35 +0100 Subject: [PATCH 056/209] Vending: Redirect intents targeted and vending package Redirect all intents opening market://, https://market.android.com and https://play.google.com with target package set to com.android.vending. Some apps hardcode the intent to have the target package com.android.vending. Additionally, also has minimal rewriting logic to change the URI from market:// to a web URL in case no handler for market:// exists on the system, so it can still be handled by web browsers. --- vending-app/src/main/AndroidManifest.xml | 21 ++++++ .../microg/vending/MarketIntentRedirect.java | 74 +++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 vending-app/src/main/java/org/microg/vending/MarketIntentRedirect.java diff --git a/vending-app/src/main/AndroidManifest.xml b/vending-app/src/main/AndroidManifest.xml index bfc54213..e63b8580 100644 --- a/vending-app/src/main/AndroidManifest.xml +++ b/vending-app/src/main/AndroidManifest.xml @@ -68,6 +68,27 @@ + + + + + + + + + + + + + + + + + + + Date: Tue, 20 Feb 2024 21:57:54 +0800 Subject: [PATCH 057/209] Location: handle invalid or dead old binder on location request update (#2190) --- .../manager/LocationRequestManager.kt | 457 ++++++++++++++++++ 1 file changed, 457 insertions(+) create mode 100644 play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationRequestManager.kt diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationRequestManager.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationRequestManager.kt new file mode 100644 index 00000000..a56e96a4 --- /dev/null +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationRequestManager.kt @@ -0,0 +1,457 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.manager + +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.location.Location +import android.os.* +import android.util.Log +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import com.google.android.gms.location.* +import com.google.android.gms.location.Granularity.* +import com.google.android.gms.location.Priority.* +import com.google.android.gms.location.internal.ClientIdentity +import kotlinx.coroutines.delay +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import org.microg.gms.location.GranularityUtil +import org.microg.gms.location.PriorityUtil +import org.microg.gms.location.elapsedMillis +import org.microg.gms.location.formatDuration +import org.microg.gms.utils.IntentCacheManager +import org.microg.gms.utils.WorkSourceUtil +import java.io.PrintWriter +import kotlin.math.max + +class LocationRequestManager(private val context: Context, private val lifecycle: Lifecycle, private val postProcessor: LocationPostProcessor, private val database: LocationAppsDatabase = LocationAppsDatabase(context), private val requestDetailsUpdatedCallback: () -> Unit) : + IBinder.DeathRecipient, LifecycleOwner { + private val lock = Mutex() + private val binderRequests = mutableMapOf() + private val pendingIntentRequests = mutableMapOf() + private val cacheManager by lazy { IntentCacheManager.create(context, CACHE_TYPE) } + var priority: @Priority Int = PRIORITY_PASSIVE + var granularity: @Granularity Int = GRANULARITY_PERMISSION_LEVEL + private set + var intervalMillis: Long = Long.MAX_VALUE + private set + var workSource = WorkSource() + private set + private var requestDetailsUpdated = false + private var checkingWhileHighAccuracy = false + + override fun getLifecycle(): Lifecycle = lifecycle + + override fun binderDied() { + lifecycleScope.launchWhenStarted { + lock.withLock { + val toRemove = binderRequests.keys.filter { !it.isBinderAlive }.toList() + for (binder in toRemove) { + binderRequests.remove(binder) + } + recalculateRequests() + } + notifyRequestDetailsUpdated() + } + } + + suspend fun add(binder: IBinder, clientIdentity: ClientIdentity, callback: ILocationCallback, request: LocationRequest, lastLocationCapsule: LastLocationCapsule) { + lock.withLock { + val holder = LocationRequestHolder(context, clientIdentity, request, callback, null) + try { + holder.start().also { + var effectiveGranularity = it.effectiveGranularity + if (effectiveGranularity == GRANULARITY_FINE && database.getForceCoarse(it.clientIdentity.packageName)) effectiveGranularity = GRANULARITY_COARSE + val lastLocation = lastLocationCapsule.getLocation(effectiveGranularity, request.maxUpdateAgeMillis) + if (lastLocation != null) it.processNewLocation(lastLocation) + } + binderRequests[binder] = holder + binder.linkToDeath(this, 0) + } catch (e: Exception) { + holder.cancel() + } + recalculateRequests() + } + notifyRequestDetailsUpdated() + } + + suspend fun update(oldBinder: IBinder, binder: IBinder, clientIdentity: ClientIdentity, callback: ILocationCallback, request: LocationRequest, lastLocationCapsule: LastLocationCapsule) { + lock.withLock { + try { + oldBinder.unlinkToDeath(this, 0) + } catch (e: Exception) { + Log.w(TAG, "update: ", e) + } + val holder = binderRequests.remove(oldBinder) + try { + val startedHolder = holder?.update(callback, request) ?: LocationRequestHolder(context, clientIdentity, request, callback, null).start().also { + var effectiveGranularity = it.effectiveGranularity + if (effectiveGranularity == GRANULARITY_FINE && database.getForceCoarse(it.clientIdentity.packageName)) effectiveGranularity = GRANULARITY_COARSE + val lastLocation = lastLocationCapsule.getLocation(effectiveGranularity, request.maxUpdateAgeMillis) + if (lastLocation != null) it.processNewLocation(lastLocation) + } + binderRequests[binder] = startedHolder + binder.linkToDeath(this, 0) + } catch (e: Exception) { + holder?.cancel() + } + recalculateRequests() + } + notifyRequestDetailsUpdated() + } + + suspend fun remove(oldBinder: IBinder) { + lock.withLock { + oldBinder.unlinkToDeath(this, 0) + if (binderRequests.remove(oldBinder) != null) recalculateRequests() + } + notifyRequestDetailsUpdated() + } + + suspend fun add(pendingIntent: PendingIntent, clientIdentity: ClientIdentity, request: LocationRequest, lastLocationCapsule: LastLocationCapsule) { + lock.withLock { + try { + pendingIntentRequests[pendingIntent] = LocationRequestHolder(context, clientIdentity, request, null, pendingIntent).start().also { + cacheManager.add(it.asParcelable()) { it.pendingIntent == pendingIntent } + var effectiveGranularity = it.effectiveGranularity + if (effectiveGranularity == GRANULARITY_FINE && database.getForceCoarse(it.clientIdentity.packageName)) effectiveGranularity = GRANULARITY_COARSE + val lastLocation = lastLocationCapsule.getLocation(effectiveGranularity, request.maxUpdateAgeMillis) + if (lastLocation != null) it.processNewLocation(lastLocation) + } + } catch (e: Exception) { + // Ignore + } + recalculateRequests() + } + notifyRequestDetailsUpdated() + } + + suspend fun remove(pendingIntent: PendingIntent) { + lock.withLock { + cacheManager.removeIf { it.pendingIntent == pendingIntent } + if (pendingIntentRequests.remove(pendingIntent) != null) recalculateRequests() + } + notifyRequestDetailsUpdated() + } + + private fun processNewLocation(lastLocationCapsule: LastLocationCapsule, map: Map): Pair, Set> { + val toRemove = mutableSetOf() + val updated = mutableSetOf() + for ((key, holder) in map) { + try { + var effectiveGranularity = holder.effectiveGranularity + if (effectiveGranularity == GRANULARITY_FINE && database.getForceCoarse(holder.clientIdentity.packageName)) effectiveGranularity = GRANULARITY_COARSE + val location = lastLocationCapsule.getLocation(effectiveGranularity, holder.maxUpdateDelayMillis) + postProcessor.process(location, effectiveGranularity, holder.clientIdentity.isGoogle(context))?.let { + if (holder.processNewLocation(it)) { + database.noteAppLocation(holder.clientIdentity.packageName, it) + updated.add(key) + } + } + } catch (e: Exception) { + Log.w(TAG, "Exception while processing for ${holder.workSource}: ${e.message}") + toRemove.add(key) + } + } + return toRemove to updated + } + + suspend fun processNewLocation(lastLocationCapsule: LastLocationCapsule) { + lock.withLock { + val (pendingIntentsToRemove, pendingIntentsUpdated) = processNewLocation(lastLocationCapsule, pendingIntentRequests) + for (pendingIntent in pendingIntentsToRemove) { + cacheManager.removeIf { it.pendingIntent == pendingIntent } + pendingIntentRequests.remove(pendingIntent) + } + for (pendingIntent in pendingIntentsUpdated) { + cacheManager.add(pendingIntentRequests[pendingIntent]!!.asParcelable()) { it.pendingIntent == pendingIntent } + } + val (bindersToRemove, _) = processNewLocation(lastLocationCapsule, binderRequests) + for (binder in bindersToRemove) { + try { + binderRequests[binder]?.cancel() + } catch (e: Exception) { + // Ignore + } + binderRequests.remove(binder) + } + if (pendingIntentsToRemove.isNotEmpty() || bindersToRemove.isNotEmpty()) { + recalculateRequests() + } + } + notifyRequestDetailsUpdated() + } + + private fun recalculateRequests() { + val merged = binderRequests.values + pendingIntentRequests.values + val newGranularity = merged.maxOfOrNull { it.effectiveGranularity } ?: GRANULARITY_PERMISSION_LEVEL + val newPriority = merged.minOfOrNull { it.effectivePriority } ?: PRIORITY_PASSIVE + val newIntervalMillis = merged.minOfOrNull { it.intervalMillis } ?: Long.MAX_VALUE + val newWorkSource = WorkSource() + for (holder in merged) { + newWorkSource.add(holder.workSource) + } + if (newPriority == PRIORITY_HIGH_ACCURACY && priority != PRIORITY_HIGH_ACCURACY) lifecycleScope.launchWhenStarted { checkWhileHighAccuracy() } + if (newPriority != priority || newGranularity != granularity || newIntervalMillis != intervalMillis || newWorkSource != workSource) { + priority = newPriority + granularity = newGranularity + intervalMillis = newIntervalMillis + workSource = newWorkSource + requestDetailsUpdated = true + } + } + + private suspend fun check() { + lock.withLock { + val pendingIntentsToRemove = mutableSetOf() + for ((key, holder) in pendingIntentRequests) { + try { + holder.check() + } catch (e: Exception) { + Log.w(TAG, "Exception while checking for ${holder.workSource}", e) + pendingIntentsToRemove.add(key) + } + } + for (pendingIntent in pendingIntentsToRemove) { + cacheManager.removeIf { it.pendingIntent == pendingIntent } + pendingIntentRequests.remove(pendingIntent) + } + val bindersToRemove = mutableSetOf() + for ((key, holder) in binderRequests) { + try { + holder.check() + } catch (e: Exception) { + Log.w(TAG, "Exception while checking for ${holder.workSource}", e) + bindersToRemove.add(key) + } + } + for (binder in bindersToRemove) { + try { + binderRequests[binder]?.cancel() + } catch (e: Exception) { + // Ignore + } + binderRequests.remove(binder) + } + if (pendingIntentsToRemove.isNotEmpty() || bindersToRemove.isNotEmpty()) { + recalculateRequests() + } + } + notifyRequestDetailsUpdated() + } + + private suspend fun checkWhileHighAccuracy() { + if (checkingWhileHighAccuracy) return + checkingWhileHighAccuracy = true + while (priority == PRIORITY_HIGH_ACCURACY) { + check() + delay(1000) + } + checkingWhileHighAccuracy = false + } + + private fun notifyRequestDetailsUpdated() { + if (!requestDetailsUpdated) return + requestDetailsUpdatedCallback() + requestDetailsUpdated = false + } + + fun stop() { + binderRequests.clear() + pendingIntentRequests.clear() + recalculateRequests() + } + + fun start() { + recalculateRequests() + notifyRequestDetailsUpdated() + } + + fun dump(writer: PrintWriter) { + writer.println("Request cache: id=${cacheManager.getId()} size=${cacheManager.getEntries().size}") + writer.println("Current location request (${GranularityUtil.granularityToString(granularity)}, ${PriorityUtil.priorityToString(priority)}, ${intervalMillis.formatDuration()} from ${workSource})") + for (request in binderRequests.values.toList()) { + writer.println("- bound ${request.workSource} ${request.intervalMillis.formatDuration()} ${GranularityUtil.granularityToString(request.effectiveGranularity)}, ${PriorityUtil.priorityToString(request.effectivePriority)} (pending: ${request.updatesPending.let { if (it == Int.MAX_VALUE) "\u221e" else "$it" }} ${request.timePendingMillis.formatDuration()})") + } + for (request in pendingIntentRequests.values.toList()) { + writer.println("- pending intent ${request.workSource} ${request.intervalMillis.formatDuration()} ${GranularityUtil.granularityToString(request.effectiveGranularity)}, ${PriorityUtil.priorityToString(request.effectivePriority)} (pending: ${request.updatesPending.let { if (it == Int.MAX_VALUE) "\u221e" else "$it" }} ${request.timePendingMillis.formatDuration()})") + } + } + + fun handleCacheIntent(intent: Intent) { + cacheManager.processIntent(intent) + for (parcelable in cacheManager.getEntries()) { + pendingIntentRequests[parcelable.pendingIntent] = LocationRequestHolder(context, parcelable) + } + recalculateRequests() + notifyRequestDetailsUpdated() + } + + companion object { + const val CACHE_TYPE = 1 + + private class LocationRequestHolderParcelable( + val clientIdentity: ClientIdentity, + val request: LocationRequest, + val pendingIntent: PendingIntent, + val start: Long, + val updates: Int, + val lastLocation: Location? + ) : Parcelable { + constructor(parcel: Parcel) : this( + parcel.readParcelable(ClientIdentity::class.java.classLoader)!!, + parcel.readParcelable(LocationRequest::class.java.classLoader)!!, + parcel.readParcelable(PendingIntent::class.java.classLoader)!!, + parcel.readLong(), + parcel.readInt(), + parcel.readParcelable(Location::class.java.classLoader) + ) + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeParcelable(clientIdentity, flags) + parcel.writeParcelable(request, flags) + parcel.writeParcelable(pendingIntent, flags) + parcel.writeLong(start) + parcel.writeInt(updates) + parcel.writeParcelable(lastLocation, flags) + } + + override fun describeContents(): Int { + return 0 + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): LocationRequestHolderParcelable { + return LocationRequestHolderParcelable(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } + } + + private class LocationRequestHolder( + private val context: Context, + val clientIdentity: ClientIdentity, + private var request: LocationRequest, + private var callback: ILocationCallback?, + private val pendingIntent: PendingIntent? + ) { + private var start = SystemClock.elapsedRealtime() + private var updates = 0 + private var lastLocation: Location? = null + + constructor(context: Context, parcelable: LocationRequestHolderParcelable) : this(context, parcelable.clientIdentity, parcelable.request, null, parcelable.pendingIntent) { + start = parcelable.start + updates = parcelable.updates + lastLocation = parcelable.lastLocation + } + + fun asParcelable() = LocationRequestHolderParcelable(clientIdentity, request, pendingIntent!!, start, updates, lastLocation) + + val permissionGranularity: @Granularity Int + get() = context.granularityFromPermission(clientIdentity) + val effectiveGranularity: @Granularity Int + get() = getEffectiveGranularity(request.granularity, permissionGranularity) + val effectivePriority: @Priority Int + get() { + if (request.priority == PRIORITY_HIGH_ACCURACY && permissionGranularity < GRANULARITY_FINE) { + return PRIORITY_BALANCED_POWER_ACCURACY + } + return request.priority + } + val maxUpdateDelayMillis: Long + get() = max(max(request.maxUpdateDelayMillis, intervalMillis), 1000L) + val intervalMillis: Long + get() = request.intervalMillis + val updatesPending: Int + get() = request.maxUpdates - updates + val timePendingMillis: Long + get() = request.durationMillis - (SystemClock.elapsedRealtime() - start) + var workSource: WorkSource = WorkSource(request.workSource).also { WorkSourceUtil.add(it, clientIdentity.uid, clientIdentity.packageName) } + private set + val shouldPersistOp: Boolean + get() = request.intervalMillis < 60000 || effectivePriority == PRIORITY_HIGH_ACCURACY + + fun update(callback: ILocationCallback, request: LocationRequest): LocationRequestHolder { + val changedGranularity = request.granularity != this.request.granularity + if (changedGranularity && shouldPersistOp) context.finishAppOpForEffectiveGranularity(clientIdentity, effectiveGranularity) + this.callback = callback + this.request = request + this.start = SystemClock.elapsedRealtime() + this.updates = 0 + this.workSource = WorkSource(request.workSource).also { WorkSourceUtil.add(it, clientIdentity.uid, clientIdentity.packageName) } + if (changedGranularity) { + if (shouldPersistOp) { + if (!context.startAppOpForEffectiveGranularity(clientIdentity, effectiveGranularity)) throw RuntimeException("Lack of permission") + } else { + if (!context.checkAppOpForEffectiveGranularity(clientIdentity, effectiveGranularity)) throw RuntimeException("Lack of permission") + } + } + return this + } + + fun start(): LocationRequestHolder { + if (shouldPersistOp) { + if (!context.startAppOpForEffectiveGranularity(clientIdentity, effectiveGranularity)) throw RuntimeException("Lack of permission") + } else { + if (!context.checkAppOpForEffectiveGranularity(clientIdentity, effectiveGranularity)) throw RuntimeException("Lack of permission") + } + // TODO: Register app op watch + return this + } + + fun cancel() { + try { + if (shouldPersistOp) context.finishAppOpForEffectiveGranularity(clientIdentity, effectiveGranularity) + callback?.cancel() + } catch (e: Exception) { + Log.w(TAG, e) + } + } + + fun check() { + if (!context.checkAppOpForEffectiveGranularity(clientIdentity, effectiveGranularity)) throw RuntimeException("Lack of permission") + if (effectiveGranularity > permissionGranularity) throw RuntimeException("Lack of permission") + if (timePendingMillis < 0) throw RuntimeException("duration limit reached (active for ${(SystemClock.elapsedRealtime() - start).formatDuration()}, duration ${request.durationMillis.formatDuration()})") + if (updatesPending <= 0) throw RuntimeException("max updates reached") + if (callback?.asBinder()?.isBinderAlive == false) throw RuntimeException("Binder died") + } + + fun processNewLocation(location: Location): Boolean { + check() + if (lastLocation != null && location.elapsedMillis - lastLocation!!.elapsedMillis < request.minUpdateIntervalMillis) return false + if (lastLocation != null && location.distanceTo(lastLocation!!) < request.minUpdateDistanceMeters) return false + if (lastLocation == location) return false + val returnedLocation = if (effectiveGranularity > permissionGranularity) { + throw RuntimeException("Lack of permission") + } else { + if (!context.noteAppOpForEffectiveGranularity(clientIdentity, effectiveGranularity)) { + throw RuntimeException("app op denied") + } else if (clientIdentity.isSelfProcess()) { + Location(location) + } else { + Location(location).apply { provider = "fused" } + } + } + val result = LocationResult.create(listOf(returnedLocation)) + callback?.onLocationResult(result) + pendingIntent?.send(context, 0, Intent().apply { putExtra(LocationResult.EXTRA_LOCATION_RESULT, result) }) + if (request.maxUpdates != Int.MAX_VALUE) updates++ + check() + return true + } + + init { + require(callback != null || pendingIntent != null) + } + } + } +} \ No newline at end of file From 4aef3d6ff6e8f746f7de8a62d019cd77cfe9e73d Mon Sep 17 00:00:00 2001 From: DaVinci9196 <150454414+DaVinci9196@users.noreply.github.com> Date: Tue, 20 Feb 2024 21:59:44 +0800 Subject: [PATCH 058/209] Improve dummy for feedback service (#2185) --- .../gms/common/data/BitmapTeleporter.aidl | 8 + .../android/gms/feedback/ErrorReport.aidl | 8 + .../android/gms/feedback/FileTeleporter.aidl | 8 + .../android/gms/feedback/LogOptions.aidl | 8 + .../android/gms/feedback/ThemeSettings.aidl | 8 + .../feedback/internal/IFeedbackCallbacks.aidl | 10 ++ .../feedback/internal/IFeedbackService.aidl | 33 ++++ .../gms/common/data/BitmapTeleporter.java | 110 ++++++++++++ .../android/gms/feedback/ErrorReport.java | 163 ++++++++++++++++++ .../android/gms/feedback/FeedbackOptions.java | 67 +++++++ .../android/gms/feedback/FileTeleporter.java | 29 ++++ .../android/gms/feedback/LogOptions.java | 37 ++++ .../microg/gms/feedback/FeedbackService.kt | 86 +++++++++ .../src/main/res/values-zh-rCN/strings.xml | 48 ++++++ .../src/main/res/values/strings.xml | 45 +++++ 15 files changed, 668 insertions(+) create mode 100644 play-services-api/src/main/aidl/com/google/android/gms/common/data/BitmapTeleporter.aidl create mode 100644 play-services-api/src/main/aidl/com/google/android/gms/feedback/ErrorReport.aidl create mode 100644 play-services-api/src/main/aidl/com/google/android/gms/feedback/FileTeleporter.aidl create mode 100644 play-services-api/src/main/aidl/com/google/android/gms/feedback/LogOptions.aidl create mode 100644 play-services-api/src/main/aidl/com/google/android/gms/feedback/ThemeSettings.aidl create mode 100644 play-services-api/src/main/aidl/com/google/android/gms/feedback/internal/IFeedbackCallbacks.aidl create mode 100644 play-services-api/src/main/aidl/com/google/android/gms/feedback/internal/IFeedbackService.aidl create mode 100644 play-services-api/src/main/java/com/google/android/gms/common/data/BitmapTeleporter.java create mode 100644 play-services-api/src/main/java/com/google/android/gms/feedback/ErrorReport.java create mode 100644 play-services-api/src/main/java/com/google/android/gms/feedback/FeedbackOptions.java create mode 100644 play-services-api/src/main/java/com/google/android/gms/feedback/FileTeleporter.java create mode 100644 play-services-api/src/main/java/com/google/android/gms/feedback/LogOptions.java create mode 100644 play-services-core/src/main/kotlin/org/microg/gms/feedback/FeedbackService.kt diff --git a/play-services-api/src/main/aidl/com/google/android/gms/common/data/BitmapTeleporter.aidl b/play-services-api/src/main/aidl/com/google/android/gms/common/data/BitmapTeleporter.aidl new file mode 100644 index 00000000..832f0fd2 --- /dev/null +++ b/play-services-api/src/main/aidl/com/google/android/gms/common/data/BitmapTeleporter.aidl @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.common.data; + +parcelable BitmapTeleporter; diff --git a/play-services-api/src/main/aidl/com/google/android/gms/feedback/ErrorReport.aidl b/play-services-api/src/main/aidl/com/google/android/gms/feedback/ErrorReport.aidl new file mode 100644 index 00000000..0b4980d3 --- /dev/null +++ b/play-services-api/src/main/aidl/com/google/android/gms/feedback/ErrorReport.aidl @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.feedback; + +parcelable ErrorReport; diff --git a/play-services-api/src/main/aidl/com/google/android/gms/feedback/FileTeleporter.aidl b/play-services-api/src/main/aidl/com/google/android/gms/feedback/FileTeleporter.aidl new file mode 100644 index 00000000..d6dd1583 --- /dev/null +++ b/play-services-api/src/main/aidl/com/google/android/gms/feedback/FileTeleporter.aidl @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.feedback; + +parcelable FileTeleporter; diff --git a/play-services-api/src/main/aidl/com/google/android/gms/feedback/LogOptions.aidl b/play-services-api/src/main/aidl/com/google/android/gms/feedback/LogOptions.aidl new file mode 100644 index 00000000..433575b0 --- /dev/null +++ b/play-services-api/src/main/aidl/com/google/android/gms/feedback/LogOptions.aidl @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.feedback; + +parcelable LogOptions; diff --git a/play-services-api/src/main/aidl/com/google/android/gms/feedback/ThemeSettings.aidl b/play-services-api/src/main/aidl/com/google/android/gms/feedback/ThemeSettings.aidl new file mode 100644 index 00000000..7b189c99 --- /dev/null +++ b/play-services-api/src/main/aidl/com/google/android/gms/feedback/ThemeSettings.aidl @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.feedback; + +parcelable ThemeSettings; diff --git a/play-services-api/src/main/aidl/com/google/android/gms/feedback/internal/IFeedbackCallbacks.aidl b/play-services-api/src/main/aidl/com/google/android/gms/feedback/internal/IFeedbackCallbacks.aidl new file mode 100644 index 00000000..eeb50c94 --- /dev/null +++ b/play-services-api/src/main/aidl/com/google/android/gms/feedback/internal/IFeedbackCallbacks.aidl @@ -0,0 +1,10 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.feedback.internal; + +interface IFeedbackCallbacks { + void onServiceDestroy(); +} diff --git a/play-services-api/src/main/aidl/com/google/android/gms/feedback/internal/IFeedbackService.aidl b/play-services-api/src/main/aidl/com/google/android/gms/feedback/internal/IFeedbackService.aidl new file mode 100644 index 00000000..48401b0d --- /dev/null +++ b/play-services-api/src/main/aidl/com/google/android/gms/feedback/internal/IFeedbackService.aidl @@ -0,0 +1,33 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.feedback.internal; + +import com.google.android.gms.feedback.FeedbackOptions; +import android.os.IBinder; +import android.os.IInterface; +import android.os.Parcel; +import com.google.android.gms.googlehelp.GoogleHelp; +import com.google.android.gms.feedback.ErrorReport; +import android.content.Context; +import android.os.Bundle; +import android.content.Intent; + + +interface IFeedbackService { + + boolean startFeedbackFlow(in ErrorReport errorReport) = 0; + + boolean silentSendFeedback(in ErrorReport errorReport) = 2; + + void saveFeedbackDataAsync(in Bundle bundle, long id) = 3; + + void saveFeedbackDataAsyncWithOption(in FeedbackOptions options, in Bundle bundle, long id) = 4; + + void startFeedbackFlowAsync(in ErrorReport errorReport, long id) = 5; + + boolean isValidConfiguration(in FeedbackOptions options) = 6; + +} \ No newline at end of file diff --git a/play-services-api/src/main/java/com/google/android/gms/common/data/BitmapTeleporter.java b/play-services-api/src/main/java/com/google/android/gms/common/data/BitmapTeleporter.java new file mode 100644 index 00000000..1af37b66 --- /dev/null +++ b/play-services-api/src/main/java/com/google/android/gms/common/data/BitmapTeleporter.java @@ -0,0 +1,110 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.common.data; + +import android.graphics.Bitmap; +import android.os.Parcel; +import android.os.ParcelFileDescriptor; +import android.util.Log; + +import androidx.annotation.NonNull; + +import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; + +import java.io.Closeable; +import java.io.DataInputStream; +import java.io.File; +import java.io.IOException; +import java.nio.ByteBuffer; + +@SafeParcelable.Class +public class BitmapTeleporter extends AbstractSafeParcelable { + + @Field(1) + public int versionCode; + @Field(2) + public ParcelFileDescriptor fileDescriptor; + @Field(3) + public int status; + public boolean isParceled; + public Bitmap targetBitmap; + public File targetDirectory; + + public BitmapTeleporter() { + } + + public BitmapTeleporter(int version, ParcelFileDescriptor parcelFileDescriptor, int status) { + this.versionCode = version; + this.fileDescriptor = parcelFileDescriptor; + this.status = status; + this.targetBitmap = null; + this.isParceled = false; + } + + public BitmapTeleporter(Bitmap bitmap) { + this.versionCode = 1; + this.fileDescriptor = null; + this.status = 0; + this.targetBitmap = bitmap; + this.isParceled = true; + } + + public final Bitmap createTargetBitmap() { + if (!this.isParceled) { + ParcelFileDescriptor parcelFileDescriptor = this.fileDescriptor; + if (parcelFileDescriptor == null) { + throw new NullPointerException("null reference"); + } + DataInputStream dataInputStream = new DataInputStream(new ParcelFileDescriptor.AutoCloseInputStream(parcelFileDescriptor)); + try { + try { + byte[] bArr = new byte[dataInputStream.readInt()]; + int readInt = dataInputStream.readInt(); + int readInt2 = dataInputStream.readInt(); + Bitmap.Config valueOf = Bitmap.Config.valueOf(dataInputStream.readUTF()); + dataInputStream.read(bArr); + close(dataInputStream); + ByteBuffer wrap = ByteBuffer.wrap(bArr); + Bitmap createBitmap = Bitmap.createBitmap(readInt, readInt2, valueOf); + createBitmap.copyPixelsFromBuffer(wrap); + this.targetBitmap = createBitmap; + this.isParceled = true; + } catch (IOException e) { + throw new IllegalStateException("Could not read from parcel file descriptor", e); + } + } catch (Throwable th) { + close(dataInputStream); + throw th; + } + } + return this.targetBitmap; + } + + public final void setTargetDirectory(File file) { + if (file == null) { + throw new NullPointerException("Cannot set null temp directory"); + } + this.targetDirectory = file; + } + + private static void close(Closeable closeable) { + try { + closeable.close(); + } catch (IOException e) { + Log.w("BitmapTeleporter", "Could not close stream", e); + } + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } + + public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(BitmapTeleporter.class); + +} diff --git a/play-services-api/src/main/java/com/google/android/gms/feedback/ErrorReport.java b/play-services-api/src/main/java/com/google/android/gms/feedback/ErrorReport.java new file mode 100644 index 00000000..55d6d359 --- /dev/null +++ b/play-services-api/src/main/java/com/google/android/gms/feedback/ErrorReport.java @@ -0,0 +1,163 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.feedback; + +import android.app.ApplicationErrorReport; +import android.graphics.Bitmap; +import android.graphics.RectF; +import android.os.Bundle; +import android.os.Parcel; + +import androidx.annotation.NonNull; + +import com.google.android.gms.common.data.BitmapTeleporter; +import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; + +import java.util.List; + +@SafeParcelable.Class +public class ErrorReport extends AbstractSafeParcelable { + @Field(2) + public ApplicationErrorReport applicationErrorReport; + @Field(3) + public String feedbackMsg; + @Field(4) + public int versionCode; + @Field(5) + public String versionName; + @Field(6) + public String mobileDevice; + @Field(7) + public String mobileDisplay; + @Field(8) + public String mobileType; + @Field(9) + public String mobileModel; + @Field(10) + public String mobileProduct; + @Field(11) + public String mobileFingerprint; + @Field(12) + public int mobileSdkInt; + @Field(13) + public String mobileRelease; + @Field(14) + public String mobileIncremental; + @Field(15) + public String mobileCodeName; + @Field(16) + public String mobileBoard; + @Field(17) + public String mobileBrand; + @Field(18) + public String[] unknownStringArray18; + @Field(19) + public String[] unknownStringArray19; + @Field(20) + public String[] unknownStringArray20; + @Field(21) + public String unknownString21; + @Field(22) + public String screenshotImgSrc; + @Field(23) + public byte[] screenshotImgData; + @Field(24) + public int screenshotHeight; + @Field(25) + public int screenshotWidth; + @Field(26) + public int phoneType; + @Field(27) + public int networkType; + @Field(28) + public String networkOperatorName; + @Field(29) + public String email; + @Field(30) + public String languageTag; + @Field(31) + public Bundle bundle; + @Field(32) + public boolean isFixedUri; + @Field(33) + public int mobileCountryCode; + @Field(34) + public int mobileNetworkCode; + @Field(35) + public boolean unknownBool35; + @Field(36) + public String exceptionClassName; + @Field(37) + public String throwFileName; + @Field(38) + public int throwLineNumber; + @Field(39) + public String throwClassName; + @Field(40) + public String throwMethodName; + @Field(41) + public String stackTrace; + @Field(42) + public String exceptionMessage; + @Field(43) + public String unknownString43; + @Field(44) + public String unknownString44; + @Field(45) + public String packageName; + @Field(46) + public BitmapTeleporter bitmapTeleporter; + @Field(47) + public String unknownString47; + @Field(48) + public FileTeleporter[] files; + @Field(49) + public String[] unknownByteArray49; + @Field(50) + public boolean unknownBool50; + @Field(51) + public String unknownString51; + @Field(52) + public ThemeSettings themeSettings; + @Field(53) + public LogOptions logOptions; + @Field(54) + public String unknownString54; + @Field(55) + public boolean unknownBool55; + @Field(56) + public Bundle bundleText; + @Field(57) + public List rectFS; + @Field(58) + public boolean unknownBool58; + @Field(59) + public Bitmap bitmap; + @Field(60) + public String unknownString60; + @Field(61) + public List camList; + @Field(62) + public int unknownInt62; + @Field(63) + public int unknownInt63; + @Field(64) + public String[] unknownStringArray64; + @Field(65) + public String[] unknownStringArray65; + @Field(66) + public String[] unknownStringArray66; + + public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(ErrorReport.class); + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } + +} diff --git a/play-services-api/src/main/java/com/google/android/gms/feedback/FeedbackOptions.java b/play-services-api/src/main/java/com/google/android/gms/feedback/FeedbackOptions.java new file mode 100644 index 00000000..679fe0f2 --- /dev/null +++ b/play-services-api/src/main/java/com/google/android/gms/feedback/FeedbackOptions.java @@ -0,0 +1,67 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.feedback; + +import android.app.ApplicationErrorReport; +import android.graphics.Bitmap; +import android.os.Bundle; +import android.os.Parcel; + +import androidx.annotation.NonNull; + +import com.google.android.gms.common.data.BitmapTeleporter; +import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; + +import java.util.List; + +@SafeParcelable.Class +public class FeedbackOptions extends AbstractSafeParcelable { + + public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(FeedbackOptions.class); + + @Field(2) + public String unknownString2; + @Field(3) + public Bundle unknownBundle3; + @Field(5) + public String unknownString3; + @Field(6) + public ApplicationErrorReport applicationErrorReport; + @Field(7) + public String unknownString7; + @Field(8) + public BitmapTeleporter bitmapTeleporter; + @Field(9) + public String packageName; + @Field(10) + public List files; + @Field(11) + public boolean unknownBoolean11; + @Field(12) + public ThemeSettings themeSettings; + @Field(13) + public LogOptions logOptions; + @Field(14) + public boolean unknownBoolean14; + @Field(15) + public Bitmap screenshot; + @Field(16) + public String unknownString16; + @Field(17) + public boolean unknownBoolean17; + @Field(18) + public long unknownLong18; + @Field(19) + public boolean unknownBool19; + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } + +} diff --git a/play-services-api/src/main/java/com/google/android/gms/feedback/FileTeleporter.java b/play-services-api/src/main/java/com/google/android/gms/feedback/FileTeleporter.java new file mode 100644 index 00000000..445200bf --- /dev/null +++ b/play-services-api/src/main/java/com/google/android/gms/feedback/FileTeleporter.java @@ -0,0 +1,29 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.feedback; + +import android.os.Parcel; + +import androidx.annotation.NonNull; + +import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; + +@SafeParcelable.Class +public class FileTeleporter extends AbstractSafeParcelable { + @Field(3) + public String contentType; + @Field(4) + public String content; + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } + + public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(FileTeleporter.class); +} diff --git a/play-services-api/src/main/java/com/google/android/gms/feedback/LogOptions.java b/play-services-api/src/main/java/com/google/android/gms/feedback/LogOptions.java new file mode 100644 index 00000000..ce1f8201 --- /dev/null +++ b/play-services-api/src/main/java/com/google/android/gms/feedback/LogOptions.java @@ -0,0 +1,37 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.feedback; + +import android.os.Parcel; + +import androidx.annotation.NonNull; + +import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; + +@SafeParcelable.Class +public class LogOptions extends AbstractSafeParcelable { + + @Field(2) + public String options; + @Field(3) + public boolean unknownBool3; + @Field(4) + public boolean unknownBool4; + @Field(5) + public boolean unknownBool5; + @Field(6) + public boolean unknownBool6; + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } + + public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(LogOptions.class); + +} diff --git a/play-services-core/src/main/kotlin/org/microg/gms/feedback/FeedbackService.kt b/play-services-core/src/main/kotlin/org/microg/gms/feedback/FeedbackService.kt new file mode 100644 index 00000000..b28127b5 --- /dev/null +++ b/play-services-core/src/main/kotlin/org/microg/gms/feedback/FeedbackService.kt @@ -0,0 +1,86 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.feedback + +import android.content.Context +import android.os.Bundle +import android.util.Log +import android.widget.Toast +import androidx.lifecycle.LifecycleCoroutineScope +import androidx.lifecycle.lifecycleScope +import com.google.android.gms.common.Feature +import com.google.android.gms.common.api.CommonStatusCodes +import com.google.android.gms.common.internal.ConnectionInfo +import com.google.android.gms.common.internal.GetServiceRequest +import com.google.android.gms.common.internal.IGmsCallbacks +import com.google.android.gms.feedback.ErrorReport +import com.google.android.gms.feedback.FeedbackOptions +import com.google.android.gms.feedback.internal.IFeedbackService +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.microg.gms.BaseService +import org.microg.gms.common.GmsService +import org.microg.gms.profile.ProfileManager + +private const val TAG = "FeedbackService" + +class FeedbackService : BaseService(TAG, GmsService.FEEDBACK) { + + override fun handleServiceRequest(callback: IGmsCallbacks, request: GetServiceRequest, service: GmsService) { + Log.d(TAG, "handleServiceRequest start ") + ProfileManager.ensureInitialized(this) + callback.onPostInitCompleteWithConnectionInfo( + CommonStatusCodes.SUCCESS, + FeedbackServiceImpl(this, lifecycleScope), + ConnectionInfo().apply { + features = arrayOf(Feature("new_send_silent_feedback", 1)) + }) + } + +} + +class FeedbackServiceImpl(private val context: Context, private val lifecycleScope: LifecycleCoroutineScope) : + IFeedbackService.Stub() { + + override fun startFeedbackFlow(errorReport: ErrorReport): Boolean { + Log.d(TAG, "startFeedbackFlow: ") + showFeedbackDisabledToast() + return false + } + + override fun silentSendFeedback(errorReport: ErrorReport): Boolean { + Log.d(TAG, "Not impl silentSendFeedback: ") + return false + } + + override fun saveFeedbackDataAsync(bundle: Bundle, id: Long) { + Log.d(TAG, "Not impl saveFeedbackDataAsync: ") + } + + override fun saveFeedbackDataAsyncWithOption(options: FeedbackOptions, bundle: Bundle, id: Long) { + Log.d(TAG, "Not impl saveFeedbackDataAsyncWithOption: $options") + } + + override fun startFeedbackFlowAsync(errorReport: ErrorReport, id: Long) { + Log.d(TAG, "startFeedbackFlowAsync errorReport:$errorReport") + showFeedbackDisabledToast() + } + + override fun isValidConfiguration(options: FeedbackOptions): Boolean { + Log.d(TAG, "Not impl isValidConfiguration: $options") + return false + } + + private fun showFeedbackDisabledToast() { + lifecycleScope.launchWhenStarted { + withContext(Dispatchers.Main){ + val content = context.resources.getString(com.google.android.gms.R.string.feedback_disabled) + Toast.makeText(context, content, Toast.LENGTH_SHORT).show() + } + } + } + +} diff --git a/play-services-core/src/main/res/values-zh-rCN/strings.xml b/play-services-core/src/main/res/values-zh-rCN/strings.xml index 72062991..beb6f60d 100644 --- a/play-services-core/src/main/res/values-zh-rCN/strings.xml +++ b/play-services-core/src/main/res/values-zh-rCN/strings.xml @@ -111,6 +111,7 @@ 自定义:%s 自动:%s 系统:%s +<<<<<<< HEAD 导入自定义配置 从文件导入设备配置 序列号 @@ -140,3 +141,50 @@ This will open your device settings ! Miscellaneous settings +======= + "测试 SafetyNet 认证" + "Google SafetyNet 是一套设备认证系统,旨在确认设备具有适当安全性,并与 Android CTS 兼容。某些应用会出于安全考虑或是防篡改目的而使用 SafetyNet。 + +microG GmsCore 内置一套自由的 SafetyNet 实现,但是官方服务器要求 SafetyNet 请求使用其专有的 DroidGuard 系统签名。" + Google 服务 + 额外使用 Google 服务 + Google + Wi-Fi + (%1$.7f, %2$.7f) + 预配置 microG 服务 + microG 服务 + 允许 Google 向应用提供验证 + 导入自定义配置信息 + 序列号 + 从文件导入自定义的设备配置信息 + 选择配置信息 + 设备配置信息 + 使用 SafetyNet 的应用 + 清除近期的 SafetyNet 请求 + 最近使用于%1$s + 评估类型 + 响应状态 + 响应数据 + 请求数据 + 十六进制 Nonce + 请求时间 + 请求类型 + 基础数据 + 令牌 + 复制 JSON JWS 数据 + 建议 + 尚未完成 + 无结果 + 无效 JSON + 未通过 CTS 测试 + 进行 reCAPTCHA Enterprise 测试 + 已复制到剪贴板 + 已通过完整性和 CTS 测试 + 未通过完整性测试 + 最近使用 + 选择一个账户 + 添加另一个账户 + + 反馈功能不可用 + +>>>>>>> 64670de2 (Improve dummy for feedback service (#2185)) diff --git a/play-services-core/src/main/res/values/strings.xml b/play-services-core/src/main/res/values/strings.xml index 1c836304..a420ef9c 100755 --- a/play-services-core/src/main/res/values/strings.xml +++ b/play-services-core/src/main/res/values/strings.xml @@ -135,6 +135,7 @@ This can take a couple of minutes." Custom: %s Automatic: %s System: %s +<<<<<<< HEAD Import custom profile Import device profile from file Serial @@ -162,4 +163,48 @@ This can take a couple of minutes." Manage signed in Google accounts This will open your device settings ! Miscellaneous settings +======= + Import custom profile + Serial + Import device profile from file + Select profile + Device profile + + Recent uses + Attestation: %s + ReCaptcha: %s + ReCaptcha Enterprise: %s + Copy JSON JWS data + Advice + Evaluation type + Response status + Response data + Request data + Nonce (Hex) + Time of request + Request type + Basic data + Token + Copied to clipboard! + Integrity and CTS passed + CTS failed + Integrity failed + Not completed yet + No result + Invalid JSON + + OFF + ON / Automatic: %s + ON / Manual: %s + %s seconds + %s minutes + + Licensing off + Licensing on + Answer license verification requests + Some apps require verification that you have purchased them on Google Play. When requested by an app, microG can download a proof of purchase from Google. If disabled, or if no Google account is added, requests for license verification are ignored. + + Feedback currently not possible + +>>>>>>> 64670de2 (Improve dummy for feedback service (#2185)) From 4e9cc0c7f8d99c9f16f97a40419a32eeeb8b402a Mon Sep 17 00:00:00 2001 From: DaVinci9196 <150454414+DaVinci9196@users.noreply.github.com> Date: Tue, 20 Feb 2024 22:17:45 +0800 Subject: [PATCH 059/209] Return proper dummy value in AppSetService.getAppSetIdInfo (#2189) Co-authored-by: Marvin W --- .../src/main/kotlin/org/microg/gms/appset/AppSetService.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/play-services-appset/core/src/main/kotlin/org/microg/gms/appset/AppSetService.kt b/play-services-appset/core/src/main/kotlin/org/microg/gms/appset/AppSetService.kt index a024f498..72e20682 100644 --- a/play-services-appset/core/src/main/kotlin/org/microg/gms/appset/AppSetService.kt +++ b/play-services-appset/core/src/main/kotlin/org/microg/gms/appset/AppSetService.kt @@ -6,7 +6,9 @@ package org.microg.gms.appset import android.util.Log +import com.google.android.gms.appset.AppSetIdInfo import com.google.android.gms.appset.AppSetIdRequestParams +import com.google.android.gms.appset.AppSetInfoParcel import com.google.android.gms.appset.internal.IAppSetIdCallback import com.google.android.gms.appset.internal.IAppSetService import com.google.android.gms.common.ConnectionResult @@ -35,6 +37,6 @@ class AppSetService : BaseService(TAG, GmsService.APP_SET) { class AppSetServiceImpl : IAppSetService.Stub() { override fun getAppSetIdInfo(params: AppSetIdRequestParams?, callback: IAppSetIdCallback?) { Log.d(TAG, "AppSetServiceImp getAppSetIdInfo is called -> ${params?.toString()} ") - callback?.onAppSetInfo(Status.SUCCESS, null) + callback?.onAppSetInfo(Status.SUCCESS, AppSetInfoParcel("", AppSetIdInfo.SCOPE_APP)) } } From f274c13be47192ed42108710c9c983cadc505442 Mon Sep 17 00:00:00 2001 From: Marvin W Date: Tue, 20 Feb 2024 15:18:10 +0100 Subject: [PATCH 060/209] Dynamite: Bump version of measurement API --- .../src/main/java/org/microg/gms/common/api/ApiClient.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/play-services-base/src/main/java/org/microg/gms/common/api/ApiClient.java b/play-services-base/src/main/java/org/microg/gms/common/api/ApiClient.java index 602e150e..fbe23365 100644 --- a/play-services-base/src/main/java/org/microg/gms/common/api/ApiClient.java +++ b/play-services-base/src/main/java/org/microg/gms/common/api/ApiClient.java @@ -16,6 +16,7 @@ package org.microg.gms.common.api; +<<<<<<< HEAD:play-services-base/src/main/java/org/microg/gms/common/api/ApiClient.java public interface ApiClient { void connect(); @@ -24,4 +25,9 @@ public interface ApiClient { boolean isConnected(); boolean isConnecting(); +======= +public class ModuleDescriptor { + public static final String MODULE_ID = "com.google.android.gms.measurement.dynamite"; + public static final int MODULE_VERSION = 98; +>>>>>>> df6f331a (Dynamite: Bump version of measurement API):play-services-core/src/main/java/com/google/android/gms/dynamite/descriptors/com/google/android/gms/measurement/dynamite/ModuleDescriptor.java } From 0dbd07409591e594bf57896f49ee232dc1a898cc Mon Sep 17 00:00:00 2001 From: Marvin W Date: Wed, 21 Feb 2024 13:35:44 +0100 Subject: [PATCH 061/209] Update build tools and dependencies --- build.gradle | 33 + .../gms/firebase/auth/FirebaseAuthService.kt | 647 ++++++++++++++ gradle/wrapper/gradle-wrapper.properties | 5 + gradlew | 18 +- gradlew.bat | 96 +++ .../microg/gms/auth/phone/SmsRetrieverCore.kt | 352 ++++++++ .../gms/auth/phone/SmsRetrieverService.kt | 131 +++ play-services-core/build.gradle | 4 + .../gms/accountsettings/ui/LoaderActivity.kt | 163 ++++ .../gms/accountsettings/ui/WebViewHelper.kt | 130 +++ .../microg/gms/auth/proxy/AuthProxyService.kt | 9 +- .../gms/auth/signin/AuthSignInService.kt | 3 +- .../gms/clearcut/ClearcutLoggerService.kt | 77 ++ .../credential/CredentialManagerService.kt | 89 ++ .../microg/gms/games/GamesConnectService.kt | 94 ++ .../org/microg/gms/games/GamesService.kt | 572 +++++++++++++ .../org/microg/gms/gcm/PushRegisterService.kt | 5 +- .../ui/GoogleHelpRedirectActivity.kt | 38 + .../org/microg/gms/signin/SignInService.kt | 138 +++ .../src/main/res/values-night-v31/themes.xml | 6 + .../src/main/res/values-v27/themes.xml | 12 + .../src/main/res/values-v31/themes.xml | 4 + .../core/privileged/Fido2PrivilegedService.kt | 111 +++ .../GmsDeviceComplianceService.kt | 47 + .../manager/DeviceOrientationManager.kt | 314 +++++++ .../gms/location/manager/LocationManager.kt | 337 ++++++++ .../manager/LocationManagerInstance.kt | 384 +++++++++ .../manager/LocationRequestManager.kt | 4 +- .../ExposureNotificationServiceImpl.kt | 801 ++++++++++++++++++ .../microg/gms/recaptcha/RecaptchaService.kt | 154 ++++ .../microg/gms/recaptcha/RecaptchaWebImpl.kt | 736 ++++++++++++++++ .../gms/safetynet/SafetyNetClientService.kt | 276 ++++++ 32 files changed, 5776 insertions(+), 14 deletions(-) create mode 100644 firebase-auth/core/src/main/kotlin/org/microg/gms/firebase/auth/FirebaseAuthService.kt create mode 100644 play-services-auth-api-phone/core/src/main/kotlin/org/microg/gms/auth/phone/SmsRetrieverCore.kt create mode 100644 play-services-auth-api-phone/core/src/main/kotlin/org/microg/gms/auth/phone/SmsRetrieverService.kt create mode 100644 play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/LoaderActivity.kt create mode 100644 play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/WebViewHelper.kt create mode 100644 play-services-core/src/main/kotlin/org/microg/gms/clearcut/ClearcutLoggerService.kt create mode 100644 play-services-core/src/main/kotlin/org/microg/gms/credential/CredentialManagerService.kt create mode 100644 play-services-core/src/main/kotlin/org/microg/gms/games/GamesConnectService.kt create mode 100644 play-services-core/src/main/kotlin/org/microg/gms/games/GamesService.kt create mode 100644 play-services-core/src/main/kotlin/org/microg/gms/googlehelp/ui/GoogleHelpRedirectActivity.kt create mode 100644 play-services-core/src/main/kotlin/org/microg/gms/signin/SignInService.kt create mode 100644 play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/privileged/Fido2PrivilegedService.kt create mode 100644 play-services-gmscompliance/core/src/main/kotlin/org/microg/gms/gmscompliance/GmsDeviceComplianceService.kt create mode 100644 play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/DeviceOrientationManager.kt create mode 100644 play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationManager.kt create mode 100644 play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationManagerInstance.kt create mode 100644 play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ExposureNotificationServiceImpl.kt create mode 100644 play-services-recaptcha/core/src/main/kotlin/org/microg/gms/recaptcha/RecaptchaService.kt create mode 100644 play-services-recaptcha/core/src/main/kotlin/org/microg/gms/recaptcha/RecaptchaWebImpl.kt create mode 100644 play-services-safetynet/core/src/main/kotlin/org/microg/gms/safetynet/SafetyNetClientService.kt diff --git a/build.gradle b/build.gradle index 0e5cb8fe..55039930 100644 --- a/build.gradle +++ b/build.gradle @@ -4,6 +4,7 @@ */ buildscript { +<<<<<<< HEAD ext.kotlinVersion = '1.8.20' ext.coroutineVersion = '1.6.4' @@ -17,11 +18,34 @@ buildscript { ext.navigationVersion = '2.5.3' ext.preferenceVersion = '1.2.0' ext.webkitVersion = '1.6.0' +======= + ext.cronetVersion = '102.5005.125' + ext.wearableVersion = '0.1.1' + + ext.kotlinVersion = '1.9.22' + ext.coroutineVersion = '1.7.3' + + ext.annotationVersion = '1.7.1' + ext.appcompatVersion = '1.6.1' + ext.biometricVersion = '1.1.0' + ext.coreVersion = '1.12.0' + ext.fragmentVersion = '1.6.2' + ext.lifecycleVersion = '2.7.0' + ext.loaderVersion = '1.1.0' + ext.materialVersion = '1.11.0' + ext.mediarouterVersion = '1.6.0' + ext.multidexVersion = '2.0.1' + ext.navigationVersion = '2.7.7' + ext.preferenceVersion = '1.2.0' + ext.recyclerviewVersion = '1.3.2' + ext.webkitVersion = '1.10.0' +>>>>>>> 48e0b00a (Update build tools and dependencies) ext.circleimageviewVersion = '3.1.0' ext.volleyVersion = '1.2.1' ext.wireVersion = '4.5.5' +<<<<<<< HEAD ext.androidBuildGradleVersion = '8.0.0' ext.androidBuildVersionTools = '33.0.2' @@ -29,6 +53,15 @@ buildscript { ext.androidMinSdk = 23 ext.androidTargetSdk = 30 ext.androidCompileSdk = 33 +======= + ext.androidBuildGradleVersion = '8.2.2' + + ext.androidBuildVersionTools = '34.0.0' + + ext.androidMinSdk = 19 + ext.androidTargetSdk = 29 + ext.androidCompileSdk = 34 +>>>>>>> 48e0b00a (Update build tools and dependencies) repositories { mavenCentral() diff --git a/firebase-auth/core/src/main/kotlin/org/microg/gms/firebase/auth/FirebaseAuthService.kt b/firebase-auth/core/src/main/kotlin/org/microg/gms/firebase/auth/FirebaseAuthService.kt new file mode 100644 index 00000000..3d7f68c4 --- /dev/null +++ b/firebase-auth/core/src/main/kotlin/org/microg/gms/firebase/auth/FirebaseAuthService.kt @@ -0,0 +1,647 @@ +/* + * SPDX-FileCopyrightText: 2020, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.firebase.auth + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.Build.VERSION.SDK_INT +import android.os.Handler +import android.os.Parcel +import android.provider.Telephony +import android.telephony.SmsMessage +import android.util.Log +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import com.google.android.gms.common.api.CommonStatusCodes +import com.google.android.gms.common.api.Status +import com.google.android.gms.common.internal.GetServiceRequest +import com.google.android.gms.common.internal.IGmsCallbacks +import com.google.firebase.auth.ActionCodeSettings +import com.google.firebase.auth.EmailAuthCredential +import com.google.firebase.auth.PhoneAuthCredential +import com.google.firebase.auth.UserProfileChangeRequest +import com.google.firebase.auth.api.internal.* +import org.json.JSONArray +import org.json.JSONObject +import org.microg.gms.BaseService +import org.microg.gms.common.GmsService +import org.microg.gms.common.PackageUtils + +private const val TAG = "GmsFirebaseAuth" + +fun JSONObject.getStringOrNull(key: String) = if (has(key)) getString(key) else null +fun JSONObject.getJSONArrayOrNull(key: String) = if (has(key)) getJSONArray(key) else null +fun JSONArray?.orEmpty() = this ?: JSONArray() +fun JSONObject.getJSONArrayLength(key: String) = getJSONArrayOrNull(key).orEmpty().length() + +private val ActionCodeSettings.requestTypeAsString: String + get() = when (requestType) { + 1 -> "PASSWORD_RESET" + 2 -> "OLD_EMAIL_AGREE" + 3 -> "NEW_EMAIL_ACCEPT" + 4 -> "VERIFY_EMAIL" + 5 -> "RECOVER_EMAIL" + 6 -> "EMAIL_SIGNIN" + 7 -> "VERIFY_AND_CHANGE_EMAIL" + 8 -> "REVERT_SECOND_FACTOR_ADDITION" + else -> "OOB_REQ_TYPE_UNSPECIFIED" + } + +private val UserProfileChangeRequest.deleteAttributeList: List + get() { + val list = arrayListOf() + if (shouldRemoveDisplayName) list.add("DISPLAY_NAME") + if (shouldRemovePhotoUri) list.add("PHOTO_URL") + return list + } + +private fun Intent.getSmsMessages(): Array { + return if (SDK_INT >= 19) { + Telephony.Sms.Intents.getMessagesFromIntent(this) + } else { + (getSerializableExtra("pdus") as? Array)?.map { SmsMessage.createFromPdu(it) }.orEmpty().toTypedArray() + } +} + +class FirebaseAuthService : BaseService(TAG, GmsService.FIREBASE_AUTH) { + override fun handleServiceRequest(callback: IGmsCallbacks, request: GetServiceRequest, service: GmsService?) { + PackageUtils.getAndCheckCallingPackage(this, request.packageName) + val apiKey = request.extras?.getString(Constants.EXTRA_API_KEY) + val libraryVersion = request.extras?.getString(Constants.EXTRA_LIBRARY_VERSION) + if (apiKey == null) { + callback.onPostInitComplete(CommonStatusCodes.DEVELOPER_ERROR, null, null) + } else { + callback.onPostInitComplete(0, FirebaseAuthServiceImpl(this, lifecycle, request.packageName, libraryVersion, apiKey).asBinder(), null) + } + } +} + +class FirebaseAuthServiceImpl(private val context: Context, override val lifecycle: Lifecycle, private val packageName: String, private val libraryVersion: String?, private val apiKey: String) : IFirebaseAuthService.Stub(), LifecycleOwner { + private val client by lazy { IdentityToolkitClient(context, apiKey, packageName, PackageUtils.firstSignatureDigestBytes(context, packageName)) } + private var authorizedDomain: String? = null + + private suspend fun getAuthorizedDomain(): String { + authorizedDomain?.let { return it } + val authorizedDomain = try { + client.getProjectConfig().getJSONArray("authorizedDomains").getString(0) + } catch (e: Exception) { + Log.w(TAG, e) + "localhost" + } + this.authorizedDomain = authorizedDomain + return authorizedDomain + } + + private suspend fun refreshTokenResponse(cachedState: String): GetTokenResponse { + var tokenResponse = GetTokenResponse.parseJson(cachedState) + if (System.currentTimeMillis() + 300000L < tokenResponse.issuedAt + tokenResponse.expiresIn * 1000) { + return tokenResponse + } + return client.getTokenByRefreshToken(tokenResponse.refreshToken).toGetTokenResponse() + } + + private fun JSONObject.toGetTokenResponse() = GetTokenResponse().apply { + refreshToken = getStringOrNull("refresh_token") + accessToken = getStringOrNull("access_token") + expiresIn = getStringOrNull("expires_in")?.toLong() + tokenType = getStringOrNull("token_type") + } + + private fun JSONObject.toGetAccountInfoUser(): GetAccountInfoUser = GetAccountInfoUser().apply { + localId = getStringOrNull("localId") + email = getStringOrNull("email") + isEmailVerified = optBoolean("emailVerified") + displayName = getStringOrNull("displayName") + photoUrl = getStringOrNull("photoUrl") + for (i in 0 until getJSONArrayLength("providerUserInfo")) { + getJSONArray("providerUserInfo").getJSONObject(i).run { + providerInfoList.providerUserInfos.add(ProviderUserInfo().apply { + federatedId = getStringOrNull("federatedId") + displayName = getStringOrNull("displayName") + photoUrl = getStringOrNull("photoUrl") + providerId = getStringOrNull("providerId") + phoneNumber = getStringOrNull("phoneNumber") + email = getStringOrNull("email") + rawUserInfo = this@run.toString() + }) + } + } + password = getStringOrNull("rawPassword") + phoneNumber = getStringOrNull("phoneNumber") + creationTimestamp = getStringOrNull("createdAt")?.toLong() ?: 0L + lastSignInTimestamp = getStringOrNull("lastLoginAt")?.toLong() ?: 0L + } + + private fun JSONObject.toCreateAuthUriResponse(): CreateAuthUriResponse = CreateAuthUriResponse().apply { + authUri = getStringOrNull("authUri") + isRegistered = optBoolean("registered") + providerId = getStringOrNull("providerId") + isForExistingProvider = optBoolean("forExistingProvider") + for (i in 0 until getJSONArrayLength("allProviders")) { + stringList.values.add(getJSONArray("allProviders").getString(i)) + } + for (i in 0 until getJSONArrayLength("signinMethods")) { + signInMethods.add(getJSONArray("signinMethods").getString(i)) + } + } + + override fun applyActionCode(request: ApplyActionCodeAidlRequest, callbacks: IFirebaseAuthCallbacks) { + Log.d(TAG, "Not yet implemented: applyActionCode") + callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported")) + } + + override fun applyActionCodeCompat(code: String?, callbacks: IFirebaseAuthCallbacks) { + Log.d(TAG, "Not yet implemented: applyActionCodeCompat") + callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported")) + } + + override fun changeEmail(request: ChangeEmailAidlRequest, callbacks: IFirebaseAuthCallbacks) { + Log.d(TAG, "Not yet implemented: changeEmail") + callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported")) + } + + override fun changeEmailCompat(cachedState: String?, email: String?, callbacks: IFirebaseAuthCallbacks) { + Log.d(TAG, "Not yet implemented: changeEmailCompat") + callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported")) + } + + override fun changePassword(request: ChangePasswordAidlRequest, callbacks: IFirebaseAuthCallbacks) { + Log.d(TAG, "Not yet implemented: changePassword") + callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported")) + } + + override fun changePasswordCompat(cachedState: String?, password: String?, callbacks: IFirebaseAuthCallbacks) { + Log.d(TAG, "Not yet implemented: changePasswordCompat") + callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported")) + } + + override fun checkActionCode(request: CheckActionCodeAidlRequest, callbacks: IFirebaseAuthCallbacks) { + Log.d(TAG, "Not yet implemented: checkActionCode") + callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported")) + } + + override fun checkActionCodeCompat(code: String?, callbacks: IFirebaseAuthCallbacks) { + Log.d(TAG, "Not yet implemented: checkActionCodeCompat") + callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported")) + } + + override fun confirmPasswordReset(request: ConfirmPasswordResetAidlRequest, callbacks: IFirebaseAuthCallbacks) { + Log.d(TAG, "Not yet implemented: confirmPasswordReset") + callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported")) + } + + override fun confirmPasswordResetCompat(code: String?, newPassword: String?, callbacks: IFirebaseAuthCallbacks) { + Log.d(TAG, "Not yet implemented: confirmPasswordResetCompat") + callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported")) + } + + override fun createUserWithEmailAndPassword(request: CreateUserWithEmailAndPasswordAidlRequest, callbacks: IFirebaseAuthCallbacks) { + lifecycleScope.launchWhenStarted { + Log.d(TAG, "createUserWithEmailAndPassword") + try { + val tokenResult = client.signupNewUser(email = request.email, password = request.password, tenantId = request.tenantId) + val idToken = tokenResult.getString("idToken") + val refreshToken = tokenResult.getString("refreshToken") + val getTokenResponse = client.getTokenByRefreshToken(refreshToken).toGetTokenResponse() + val accountInfoResult = client.getAccountInfo(idToken = idToken).getJSONArray("users").getJSONObject(0).toGetAccountInfoUser().apply { this.isNewUser = true } + Log.d(TAG, "callback: onGetTokenResponseAndUser") + callbacks.onGetTokenResponseAndUser(getTokenResponse, accountInfoResult) + } catch (e: Exception) { + Log.w(TAG, "callback: onFailure", e) + callbacks.onFailure(Status(CommonStatusCodes.INTERNAL_ERROR, e.message)) + } + } + } + + override fun createUserWithEmailAndPasswordCompat(email: String?, password: String?, callbacks: IFirebaseAuthCallbacks) { + createUserWithEmailAndPassword(CreateUserWithEmailAndPasswordAidlRequest().apply { this.email = email; this.password = password }, callbacks) + } + + override fun delete(request: DeleteAidlRequest, callbacks: IFirebaseAuthCallbacks) { + Log.d(TAG, "Not yet implemented: delete") + callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported")) + } + + override fun deleteCompat(cachedState: String?, callbacks: IFirebaseAuthCallbacks) { + Log.d(TAG, "Not yet implemented: deleteCompat") + callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported")) + } + + override fun finalizeMfaEnrollment(request: FinalizeMfaEnrollmentAidlRequest, callbacks: IFirebaseAuthCallbacks) { + Log.d(TAG, "Not yet implemented: finalizeMfaEnrollment") + callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported")) + } + + override fun finalizeMfaSignIn(request: FinalizeMfaSignInAidlRequest, callbacks: IFirebaseAuthCallbacks) { + Log.d(TAG, "Not yet implemented: finalizeMfaSignIn") + callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported")) + } + + override fun getAccessToken(request: GetAccessTokenAidlRequest, callbacks: IFirebaseAuthCallbacks) { + lifecycleScope.launchWhenStarted { + Log.d(TAG, "getAccessToken") + try { + callbacks.onGetTokenResponse(client.getTokenByRefreshToken(request.refreshToken).toGetTokenResponse()) + } catch (e: Exception) { + Log.w(TAG, "callback: onFailure", e) + callbacks.onFailure(Status(CommonStatusCodes.INTERNAL_ERROR, e.message)) + } + } + } + + override fun getAccessTokenCompat(refreshToken: String?, callbacks: IFirebaseAuthCallbacks) { + getAccessToken(GetAccessTokenAidlRequest().apply { this.refreshToken = refreshToken }, callbacks) + } + + override fun getProvidersForEmail(request: GetProvidersForEmailAidlRequest, callbacks: IFirebaseAuthCallbacks) { + lifecycleScope.launchWhenStarted { + Log.d(TAG, "getProvidersForEmail") + try { + callbacks.onCreateAuthUriResponse(client.createAuthUri(identifier = request.email, tenantId = request.tenantId).toCreateAuthUriResponse()) + } catch (e: Exception) { + Log.w(TAG, "callback: onFailure", e) + callbacks.onFailure(Status(CommonStatusCodes.INTERNAL_ERROR, e.message)) + } + } + } + + override fun getProvidersForEmailCompat(email: String?, callbacks: IFirebaseAuthCallbacks) { + getProvidersForEmail(GetProvidersForEmailAidlRequest().apply { this.email = email }, callbacks) + } + + override fun linkEmailAuthCredential(request: LinkEmailAuthCredentialAidlRequest, callbacks: IFirebaseAuthCallbacks) { + lifecycleScope.launchWhenStarted { + Log.d(TAG, "linkEmailAuthCredential") + try { + val getTokenResponse = refreshTokenResponse(request.cachedState) + val accountInfoResult = client.getAccountInfo(idToken = getTokenResponse.accessToken).getJSONArray("users").getJSONObject(0).toGetAccountInfoUser() + val setAccountInfo = client.setAccountInfo(idToken = getTokenResponse.accessToken, localId = accountInfoResult.localId, email = request.email, password = request.password).toGetAccountInfoUser() + accountInfoResult.email = setAccountInfo.email + accountInfoResult.isEmailVerified = setAccountInfo.isEmailVerified + accountInfoResult.providerInfoList = setAccountInfo.providerInfoList + callbacks.onGetTokenResponseAndUser(getTokenResponse, accountInfoResult) + } catch (e: Exception) { + Log.w(TAG, "callback: onFailure", e) + callbacks.onFailure(Status(CommonStatusCodes.INTERNAL_ERROR, e.message)) + } + } + } + + override fun linkEmailAuthCredentialCompat(email: String?, password: String?, cachedState: String?, callbacks: IFirebaseAuthCallbacks) { + linkEmailAuthCredential(LinkEmailAuthCredentialAidlRequest().apply { this.email = email; this.password = password; this.cachedState = cachedState }, callbacks) + } + + override fun linkFederatedCredential(request: LinkFederatedCredentialAidlRequest, callbacks: IFirebaseAuthCallbacks) { + Log.d(TAG, "Not yet implemented: linkFederatedCredential") + callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported")) + } + + override fun linkFederatedCredentialCompat(cachedState: String?, verifyAssertionRequest: VerifyAssertionRequest, callbacks: IFirebaseAuthCallbacks) { + Log.d(TAG, "Not yet implemented: linkFederatedCredentialCompat") + callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported")) + } + + override fun linkPhoneAuthCredential(request: LinkPhoneAuthCredentialAidlRequest, callbacks: IFirebaseAuthCallbacks) { + Log.d(TAG, "Not yet implemented: linkPhoneAuthCredential") + callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported")) + } + + override fun linkPhoneAuthCredentialCompat(cachedState: String?, credential: PhoneAuthCredential?, callbacks: IFirebaseAuthCallbacks) { + Log.d(TAG, "Not yet implemented: linkPhoneAuthCredentialCompat") + callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported")) + } + + override fun reload(request: ReloadAidlRequest, callbacks: IFirebaseAuthCallbacks) { + lifecycleScope.launchWhenStarted { + try { + Log.d(TAG, "reload") + val getTokenResponse = refreshTokenResponse(request.cachedState) + val accountInfoResult = client.getAccountInfo(idToken = getTokenResponse.accessToken).getJSONArray("users").getJSONObject(0).toGetAccountInfoUser() + Log.d(TAG, "callback: onGetTokenResponseAndUser") + callbacks.onGetTokenResponseAndUser(getTokenResponse, accountInfoResult) + } catch (e: Exception) { + Log.w(TAG, "callback: onFailure", e) + callbacks.onFailure(Status(CommonStatusCodes.INTERNAL_ERROR, e.message)) + } + } + } + + override fun reloadCompat(cachedState: String?, callbacks: IFirebaseAuthCallbacks) { + reload(ReloadAidlRequest().apply { this.cachedState = cachedState }, callbacks) + } + + override fun sendEmailVerification(request: SendEmailVerificationWithSettingsAidlRequest, callbacks: IFirebaseAuthCallbacks) { + lifecycleScope.launchWhenStarted { + try { + Log.d(TAG, "sendEmailVerification") + client.getOobConfirmationCode( + requestType = "VERIFY_EMAIL", + idToken = request.token, + iOSBundleId = request.settings?.iOSBundle, + iOSAppStoreId = request.settings?.iOSAppStoreId, + continueUrl = request.settings?.url, + androidInstallApp = request.settings?.androidInstallApp, + androidMinimumVersion = request.settings?.androidMinimumVersion, + androidPackageName = request.settings?.androidPackageName, + canHandleCodeInApp = request.settings?.handleCodeInApp + ) + callbacks.onEmailVerificationResponse() + } catch (e: Exception) { + Log.w(TAG, "callback: onFailure", e) + callbacks.onFailure(Status(CommonStatusCodes.INTERNAL_ERROR, e.message)) + } + } + } + + override fun sendEmailVerificationCompat(token: String?, actionCodeSettings: ActionCodeSettings?, callbacks: IFirebaseAuthCallbacks) { + sendEmailVerification(SendEmailVerificationWithSettingsAidlRequest().apply { this.token = token; this.settings = actionCodeSettings }, callbacks) + } + + override fun sendVerificationCode(request: SendVerificationCodeAidlRequest, callbacks: IFirebaseAuthCallbacks) { + lifecycleScope.launchWhenStarted { + try { + Log.d(TAG, "sendVerificationCode") + val reCaptchaToken = when { + request.request.recaptchaToken != null -> request.request.recaptchaToken + ReCaptchaOverlay.isSupported(context) -> ReCaptchaOverlay.awaitToken(context, apiKey, getAuthorizedDomain()) + ReCaptchaActivity.isSupported(context) -> ReCaptchaActivity.awaitToken(context, apiKey, getAuthorizedDomain()) + else -> throw RuntimeException("No recaptcha token available") + } + var sessionInfo: String? = null + var registered = true + val receiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + var smsCode: String? = null + for (message in intent.getSmsMessages()) { + smsCode = Regex("\\b([0-9]{6})\\b").find(message.messageBody)?.groups?.get(1)?.value + ?: continue + Log.d(TAG, "Received SMS verification code: $smsCode") + break + } + if (smsCode == null) return + registered = false + context.unregisterReceiver(this) + try { + callbacks.onVerificationCompletedResponse(PhoneAuthCredential().apply { + this.phoneNumber = request.request.phoneNumber + this.sessionInfo = sessionInfo + this.smsCode = smsCode + }) + Log.d(TAG, "callback: onVerificationCompletedResponse") + } catch (e: Exception) { + Log.w(TAG, e) + } + } + } + context.registerReceiver(receiver, IntentFilter("android.provider.Telephony.SMS_RECEIVED")) + var timeout = request.request.timeoutInSeconds * 1000L + if (timeout <= 0L) timeout = 120000L + Handler().postDelayed({ + if (registered) { + Log.d(TAG, "Waited ${timeout}ms for verification code SMS, timeout.") + context.unregisterReceiver(receiver) + callbacks.onVerificationAutoTimeOut(sessionInfo) + Log.d(TAG, "callback: onVerificationAutoTimeOut") + } + }, timeout) + sessionInfo = client.sendVerificationCode(phoneNumber = request.request.phoneNumber, reCaptchaToken = reCaptchaToken).getString("sessionInfo") + callbacks.onSendVerificationCodeResponse(sessionInfo) + Log.d(TAG, "callback: onSendVerificationCodeResponse") + } catch (e: Exception) { + Log.w(TAG, "callback: onFailure", e) + callbacks.onFailure(Status(CommonStatusCodes.INTERNAL_ERROR, e.message)) + } + } + } + + override fun sendVerificationCodeCompat(request: SendVerificationCodeRequest, callbacks: IFirebaseAuthCallbacks) { + sendVerificationCode(SendVerificationCodeAidlRequest().apply { this.request = request }, callbacks) + } + + override fun sendGetOobConfirmationCodeEmail(request: SendGetOobConfirmationCodeEmailAidlRequest, callbacks: IFirebaseAuthCallbacks) { + lifecycleScope.launchWhenStarted { + try { + Log.d(TAG, "sendGetOobConfirmationCodeEmail") + client.getOobConfirmationCode( + requestType = request.settings?.requestTypeAsString ?: "OOB_REQ_TYPE_UNSPECIFIED", + email = request.email, + iOSBundleId = request.settings?.iOSBundle, + iOSAppStoreId = request.settings?.iOSAppStoreId, + continueUrl = request.settings?.url, + androidInstallApp = request.settings?.androidInstallApp, + androidMinimumVersion = request.settings?.androidMinimumVersion, + androidPackageName = request.settings?.androidPackageName, + canHandleCodeInApp = request.settings?.handleCodeInApp + ) + Log.d(TAG, "callback: onResetPasswordResponse") + callbacks.onResetPasswordResponse(null) + } catch (e: Exception) { + Log.w(TAG, "callback: onFailure", e) + callbacks.onFailure(Status(CommonStatusCodes.INTERNAL_ERROR, e.message)) + } + } + } + + override fun sendGetOobConfirmationCodeEmailCompat(email: String?, actionCodeSettings: ActionCodeSettings?, callbacks: IFirebaseAuthCallbacks) { + sendGetOobConfirmationCodeEmail(SendGetOobConfirmationCodeEmailAidlRequest().apply { this.email = email; this.settings = actionCodeSettings }, callbacks) + } + + override fun setFirebaseUiVersion(request: SetFirebaseUiVersionAidlRequest, callbacks: IFirebaseAuthCallbacks) { + Log.d(TAG, "Not yet implemented: setFirebaseUiVersion") + callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported")) + } + + override fun setFirebaseUIVersionCompat(firebaseUiVersion: String?, callbacks: IFirebaseAuthCallbacks) { + Log.d(TAG, "Not yet implemented: setFirebaseUIVersionCompat") + callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported")) + } + + override fun signInAnonymously(request: SignInAnonymouslyAidlRequest, callbacks: IFirebaseAuthCallbacks) { + lifecycleScope.launchWhenStarted { + Log.d(TAG, "signInAnonymously") + try { + val tokenResult = client.signupNewUser(tenantId = request.tenantId) + val idToken = tokenResult.getString("idToken") + val refreshToken = tokenResult.getString("refreshToken") + val getTokenResponse = client.getTokenByRefreshToken(refreshToken).toGetTokenResponse() + val accountInfoResult = client.getAccountInfo(idToken = idToken).getJSONArray("users").getJSONObject(0).toGetAccountInfoUser().apply { this.isNewUser = true } + Log.d(TAG, "callback: onGetTokenResponseAndUser") + callbacks.onGetTokenResponseAndUser(getTokenResponse, accountInfoResult) + } catch (e: Exception) { + Log.w(TAG, "callback: onFailure", e) + callbacks.onFailure(Status(CommonStatusCodes.INTERNAL_ERROR, e.message)) + } + } + } + + override fun signInAnonymouslyCompat(callbacks: IFirebaseAuthCallbacks) { + signInAnonymously(SignInAnonymouslyAidlRequest(), callbacks) + } + + override fun signInWithCredential(request: SignInWithCredentialAidlRequest, callbacks: IFirebaseAuthCallbacks) { + Log.d(TAG, "Not yet implemented: signInWithCredential") + callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported")) + } + + override fun signInWithCredentialCompat(verifyAssertionRequest: VerifyAssertionRequest, callbacks: IFirebaseAuthCallbacks) { + Log.d(TAG, "Not yet implemented: signInWithCredentialCompat") + callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported")) + } + + override fun signInWithCustomToken(request: SignInWithCustomTokenAidlRequest, callbacks: IFirebaseAuthCallbacks) { + lifecycleScope.launchWhenStarted { + Log.d(TAG, "signInWithCustomToken") + try { + val tokenResult = client.verifyCustomToken(token = request.token) + val idToken = tokenResult.getString("idToken") + val refreshToken = tokenResult.getString("refreshToken") + val isNewUser = tokenResult.optBoolean("isNewUser") + val getTokenResponse = client.getTokenByRefreshToken(refreshToken).toGetTokenResponse() + val accountInfoResult = client.getAccountInfo(idToken = idToken).getJSONArray("users").getJSONObject(0).toGetAccountInfoUser().apply { this.isNewUser = isNewUser } + Log.d(TAG, "callback: onGetTokenResponseAndUser") + callbacks.onGetTokenResponseAndUser(getTokenResponse, accountInfoResult) + } catch (e: Exception) { + Log.w(TAG, "callback: onFailure", e) + callbacks.onFailure(Status(CommonStatusCodes.INTERNAL_ERROR, e.message)) + } + } + } + + override fun signInWithCustomTokenCompat(token: String, callbacks: IFirebaseAuthCallbacks) { + signInWithCustomToken(SignInWithCustomTokenAidlRequest().apply { this.token = token }, callbacks) + } + + override fun signInWithEmailAndPassword(request: SignInWithEmailAndPasswordAidlRequest, callbacks: IFirebaseAuthCallbacks) { + lifecycleScope.launchWhenStarted { + Log.d(TAG, "signInWithEmailAndPassword") + try { + val tokenResult = client.verifyPassword(email = request.email, password = request.password, tenantId = request.tenantId) + val idToken = tokenResult.getString("idToken") + val refreshToken = tokenResult.getString("refreshToken") + val getTokenResponse = client.getTokenByRefreshToken(refreshToken).toGetTokenResponse() + val accountInfoResult = client.getAccountInfo(idToken = idToken).getJSONArray("users").getJSONObject(0).toGetAccountInfoUser() + Log.d(TAG, "callback: onGetTokenResponseAndUser") + callbacks.onGetTokenResponseAndUser(getTokenResponse, accountInfoResult) + } catch (e: Exception) { + Log.w(TAG, "callback: onFailure", e) + callbacks.onFailure(Status(CommonStatusCodes.INTERNAL_ERROR, e.message)) + } + } + } + + override fun signInWithEmailAndPasswordCompat(email: String?, password: String?, callbacks: IFirebaseAuthCallbacks) { + signInWithEmailAndPassword(SignInWithEmailAndPasswordAidlRequest().apply { this.email = email; this.password = password }, callbacks) + } + + override fun signInWithEmailLink(request: SignInWithEmailLinkAidlRequest, callbacks: IFirebaseAuthCallbacks) { + Log.d(TAG, "Not yet implemented: signInWithEmailLink") + callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported")) + } + + override fun signInWithEmailLinkCompat(credential: EmailAuthCredential?, callbacks: IFirebaseAuthCallbacks) { + Log.d(TAG, "Not yet implemented: signInWithEmailLinkCompat") + callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported")) + } + + override fun signInWithPhoneNumber(request: SignInWithPhoneNumberAidlRequest, callbacks: IFirebaseAuthCallbacks) { + lifecycleScope.launchWhenStarted { + Log.d(TAG, "signInWithPhoneNumber") + try { + val tokenResult = client.verifyPhoneNumber( + phoneNumber = request.credential.phoneNumber, + temporaryProof = request.credential.temporaryProof, + sessionInfo = request.credential.sessionInfo, + code = request.credential.smsCode + ) + val idToken = tokenResult.getString("idToken") + val refreshToken = tokenResult.getString("refreshToken") + val isNewUser = tokenResult.optBoolean("isNewUser") + val getTokenResponse = client.getTokenByRefreshToken(refreshToken).toGetTokenResponse() + val accountInfoResult = client.getAccountInfo(idToken).getJSONArray("users").getJSONObject(0).toGetAccountInfoUser().apply { this.isNewUser = isNewUser } + Log.d(TAG, "callback: onGetTokenResponseAndUser") + callbacks.onGetTokenResponseAndUser(getTokenResponse, accountInfoResult) + } catch (e: Exception) { + Log.w(TAG, "callback: onFailure", e) + callbacks.onFailure(Status(CommonStatusCodes.INTERNAL_ERROR, e.message)) + } + } + } + + override fun signInWithPhoneNumberCompat(credential: PhoneAuthCredential?, callbacks: IFirebaseAuthCallbacks) { + signInWithPhoneNumber(SignInWithPhoneNumberAidlRequest().apply { this.credential = credential }, callbacks) + } + + override fun startMfaEnrollmentWithPhoneNumber(request: StartMfaPhoneNumberEnrollmentAidlRequest, callbacks: IFirebaseAuthCallbacks) { + Log.d(TAG, "Not yet implemented: startMfaEnrollmentWithPhoneNumber") + callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported")) + } + + override fun startMfaSignInWithPhoneNumber(request: StartMfaPhoneNumberSignInAidlRequest, callbacks: IFirebaseAuthCallbacks) { + Log.d(TAG, "Not yet implemented: startMfaSignInWithPhoneNumber") + callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported")) + } + + override fun unenrollMfa(request: UnenrollMfaAidlRequest, callbacks: IFirebaseAuthCallbacks) { + Log.d(TAG, "Not yet implemented: unenrollMfa") + callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported")) + } + + override fun unlinkEmailCredential(request: UnlinkEmailCredentialAidlRequest, callbacks: IFirebaseAuthCallbacks) { + Log.d(TAG, "Not yet implemented: unlinkEmailCredential") + callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported")) + } + + override fun unlinkEmailCredentialCompat(cachedState: String?, callbacks: IFirebaseAuthCallbacks) { + Log.d(TAG, "Not yet implemented: unlinkEmailCredentialCompat") + callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported")) + } + + override fun unlinkFederatedCredential(request: UnlinkFederatedCredentialAidlRequest, callbacks: IFirebaseAuthCallbacks) { + Log.d(TAG, "Not yet implemented: unlinkFederatedCredential") + callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported")) + } + + override fun unlinkFederatedCredentialCompat(provider: String?, cachedState: String?, callbacks: IFirebaseAuthCallbacks) { + Log.d(TAG, "Not yet implemented: unlinkFederatedCredentialCompat") + callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported")) + } + + override fun updateProfile(request: UpdateProfileAidlRequest, callbacks: IFirebaseAuthCallbacks) { + lifecycleScope.launchWhenStarted { + Log.d(TAG, "updateProfile") + try { + val getTokenResponse = refreshTokenResponse(request.cachedState) + val accountInfoResult = client.getAccountInfo(idToken = getTokenResponse.accessToken).getJSONArray("users").getJSONObject(0).toGetAccountInfoUser() + val setAccountInfo = client.setAccountInfo(idToken = getTokenResponse.accessToken, localId = accountInfoResult.localId, displayName = request.request.displayName, photoUrl = request.request.photoUrl, deleteAttribute = request.request.deleteAttributeList).toGetAccountInfoUser() + accountInfoResult.photoUrl = setAccountInfo.photoUrl + accountInfoResult.displayName = setAccountInfo.displayName + callbacks.onGetTokenResponseAndUser(getTokenResponse, accountInfoResult) + } catch (e: Exception) { + Log.w(TAG, "callback: onFailure", e) + callbacks.onFailure(Status(CommonStatusCodes.INTERNAL_ERROR, e.message)) + } + } + } + + override fun updateProfileCompat(cachedState: String?, userProfileChangeRequest: UserProfileChangeRequest, callbacks: IFirebaseAuthCallbacks) { + updateProfile(UpdateProfileAidlRequest().apply { this.cachedState = cachedState; this.request = userProfileChangeRequest}, callbacks) + } + + override fun verifyBeforeUpdateEmail(request: VerifyBeforeUpdateEmailAidlRequest, callbacks: IFirebaseAuthCallbacks) { + Log.d(TAG, "Not yet implemented: verifyBeforeUpdateEmail") + callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported")) + } + + + override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean { + if (super.onTransact(code, data, reply, flags)) return true + Log.d(TAG, "onTransact: $code, $data, $flags") + return false + } +} diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e736c04d..ab7cda7a 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,11 @@ #Thu Jun 29 16:45:22 ICT 2023 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +<<<<<<< HEAD distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-rc-3-bin.zip +======= +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip +networkTimeout=10000 +>>>>>>> 48e0b00a (Update build tools and dependencies) zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 60cf9e8c..64544521 100755 --- a/gradlew +++ b/gradlew @@ -56,7 +56,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -81,10 +81,10 @@ do esac done -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" +# This is normally unused +# shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' @@ -142,12 +142,16 @@ fi if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -204,6 +208,12 @@ set -- \ org.gradle.wrapper.GradleWrapperMain \ "$@" +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. diff --git a/gradlew.bat b/gradlew.bat index 5093609d..d0412fa3 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,3 +1,4 @@ +<<<<<<< HEAD @rem @rem Copyright 2015 the original author or authors. @rem @@ -102,3 +103,98 @@ exit /b 1 if "%OS%"=="Windows_NT" endlocal :omega +======= +@rem SPDX-License-Identifier: Apache-2.0 +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega +>>>>>>> 48e0b00a (Update build tools and dependencies) diff --git a/play-services-auth-api-phone/core/src/main/kotlin/org/microg/gms/auth/phone/SmsRetrieverCore.kt b/play-services-auth-api-phone/core/src/main/kotlin/org/microg/gms/auth/phone/SmsRetrieverCore.kt new file mode 100644 index 00000000..c0c70b49 --- /dev/null +++ b/play-services-auth-api-phone/core/src/main/kotlin/org/microg/gms/auth/phone/SmsRetrieverCore.kt @@ -0,0 +1,352 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.auth.phone + +import android.Manifest.permission.READ_CONTACTS +import android.Manifest.permission.RECEIVE_SMS +import android.annotation.TargetApi +import android.app.Activity +import android.app.AlarmManager +import android.app.PendingIntent +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.pm.PackageManager.PERMISSION_GRANTED +import android.os.* +import android.os.Build.VERSION.SDK_INT +import android.provider.ContactsContract +import android.provider.ContactsContract.CommonDataKinds.Phone +import android.provider.Telephony +import android.telephony.SmsMessage +import android.text.TextUtils +import android.util.Base64 +import android.util.Log +import androidx.core.content.ContextCompat +import androidx.core.os.bundleOf +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import com.google.android.gms.auth.api.phone.SmsRetriever +import com.google.android.gms.common.api.CommonStatusCodes +import com.google.android.gms.common.api.Status +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import org.microg.gms.auth.phone.SmsRetrieverRequestType.RETRIEVER +import org.microg.gms.auth.phone.SmsRetrieverRequestType.USER_CONSENT +import org.microg.gms.common.Constants +import org.microg.gms.utils.getSignatures +import java.nio.charset.StandardCharsets +import java.security.MessageDigest +import java.util.concurrent.atomic.AtomicInteger + + +private const val TAG = "SmsRetrieverCore" + +private const val ACTION_SMS_RETRIEVE_TIMEOUT = "org.microg.gms.auth.phone.ACTION_SMS_RETRIEVE_TIMEOUT" +private const val EXTRA_REQUEST_ID = "requestId" +private const val TIMEOUT = 1000 * 60 * 5 // 5 minutes +private const val MESSAGE_MAX_LEN = 140 + +class SmsRetrieverCore(private val context: Context, override val lifecycle: Lifecycle) : LifecycleOwner, DefaultLifecycleObserver { + private val requests: HashMap = hashMapOf() + private val requestIdCounter = AtomicInteger(0) + private lateinit var timeoutBroadcastReceiver: BroadcastReceiver + private lateinit var smsBroadcastReceiver: BroadcastReceiver + private var requestCode = 0 + private val alarmManager: AlarmManager + get() = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager + + init { + lifecycle.addObserver(this) + } + + @TargetApi(19) + private fun configureBroadcastListenersIfNeeded() { + synchronized(this) { + if (!this::timeoutBroadcastReceiver.isInitialized) { + val intentFilter = IntentFilter(ACTION_SMS_RETRIEVE_TIMEOUT) + timeoutBroadcastReceiver = TimeoutReceiver() + ContextCompat.registerReceiver(context, timeoutBroadcastReceiver, intentFilter, ContextCompat.RECEIVER_NOT_EXPORTED) + } + if (!this::smsBroadcastReceiver.isInitialized) { + val intentFilter = IntentFilter(Telephony.Sms.Intents.SMS_RECEIVED_ACTION) + intentFilter.priority = 999 + smsBroadcastReceiver = SmsReceiver() + context.registerReceiver(smsBroadcastReceiver, intentFilter) + } + } + } + + private suspend fun ensureReady(permissions: Array): Boolean { + if (SDK_INT < 19) throw RuntimeException("Version not supported") + if (!ensurePermission(permissions)) return false + configureBroadcastListenersIfNeeded() + return true + } + + suspend fun startSmsRetriever(packageName: String) { + val appHashString = getHashString(packageName) + + if (!ensureReady(arrayOf(RECEIVE_SMS))) + throw RuntimeException("Initialization failed") + if (anyOtherPackageHasHashString(packageName, appHashString)) + throw RuntimeException("Collision in hash string, can't use SMS Retriever API") + if (requests.values.any { it.packageName == packageName && it.appHashString == appHashString && it.type == RETRIEVER }) + throw RuntimeException("App already listening") + + val request = SmsRetrieverRequest( + id = requestIdCounter.incrementAndGet(), + type = RETRIEVER, + packageName = packageName, + appHashString = appHashString, + timeoutPendingIntent = getTimeoutPendingIntent(context, packageName) + ) + requests[request.id] = request + alarmManager.set(AlarmManager.RTC, request.creation + TIMEOUT, request.timeoutPendingIntent) + } + + suspend fun startWithConsentPrompt(packageName: String, senderPhoneNumber: String?) { + if (!ensureReady(arrayOf(RECEIVE_SMS, READ_CONTACTS))) + throw RuntimeException("Initialization failed") + if (requests.values.any { it.packageName == packageName && it.senderPhoneNumber == senderPhoneNumber && it.type == USER_CONSENT }) + throw RuntimeException("App already listening") + + val request = SmsRetrieverRequest( + id = requestIdCounter.incrementAndGet(), + type = USER_CONSENT, + packageName = packageName, + senderPhoneNumber = senderPhoneNumber, + timeoutPendingIntent = getTimeoutPendingIntent(context, packageName) + ) + requests[request.id] = request + alarmManager.set(AlarmManager.RTC, request.creation + TIMEOUT, request.timeoutPendingIntent) + } + + fun hasOngoingUserConsentRequest(): Boolean { + return requests.values.any { it.type == USER_CONSENT } + } + + private fun sendRetrieverBroadcast(request: SmsRetrieverRequest, message: SmsMessage) { + sendReply(request, Status.SUCCESS, bundleOf(SmsRetriever.EXTRA_SMS_MESSAGE to message.messageBody)) + } + + private fun sendUserConsentBroadcast(request: SmsRetrieverRequest, message: SmsMessage) { + val userConsentIntent = Intent(context, UserConsentPromptActivity::class.java) + userConsentIntent.setPackage(Constants.GMS_PACKAGE_NAME) + userConsentIntent.putExtra(EXTRA_MESSENGER, Messenger(object : Handler(Looper.getMainLooper()) { + override fun handleMessage(msg: Message) { + if (Binder.getCallingUid() == Process.myUid()) { + if (msg.what == MSG_REQUEST_MESSAGE_BODY) { + msg.replyTo?.send(Message.obtain().apply { + what = 1 + data = bundleOf("message" to message.messageBody) + }) + } else if (msg.what == MSG_CONSUME_MESSAGE) { + finishRequest(request) + } + } + } + })) + + sendReply(request, Status.SUCCESS, bundleOf(SmsRetriever.EXTRA_CONSENT_INTENT to userConsentIntent), false) + } + + private fun getTimeoutPendingIntent(context: Context, packageName: String): PendingIntent { + val intent = Intent(ACTION_SMS_RETRIEVE_TIMEOUT) + intent.setPackage(packageName) + return PendingIntent.getBroadcast(context, ++requestCode, intent, PendingIntent.FLAG_IMMUTABLE) + } + + private fun tryHandleIncomingMessageAsRetrieverMessage(message: SmsMessage): Boolean { + for (request in requests.values) { + if (request.type == RETRIEVER) { + // 11-digit hash code that uniquely identifies your app + if (request.appHashString.isNullOrBlank() || !message.messageBody.contains(request.appHashString)) continue + + sendRetrieverBroadcast(request, message) + return true + } + } + return false + } + + private fun tryHandleIncomingMessageAsUserConsentMessage(message: SmsMessage): Boolean { + val senderPhoneNumber = message.originatingAddress ?: return false + + // 4-10 digit alphanumeric code containing at least one number + if (message.messageBody.split("[^A-Za-z0-9]".toRegex()).none { it.length in 4..10 && it.any(Char::isDigit) }) return false + + // Sender cannot be in the user's Contacts list + if (isPhoneNumberInContacts(context, senderPhoneNumber)) return false + + for (request in requests.values) { + if (request.type == USER_CONSENT) { + if (!request.senderPhoneNumber.isNullOrBlank() && request.senderPhoneNumber != senderPhoneNumber) continue + + sendUserConsentBroadcast(request, message) + return true + } + } + return false + } + + private fun handleIncomingSmsMessage(message: SmsMessage) { + if (message.messageBody.isNullOrBlank()) return + if (message.messageBody.length > MESSAGE_MAX_LEN) return + + if (tryHandleIncomingMessageAsRetrieverMessage(message)) return + if (tryHandleIncomingMessageAsUserConsentMessage(message)) return + } + + fun handleTimeout(requestId: Int) { + val request = requests[requestId] ?: return + sendReply(request, Status(CommonStatusCodes.TIMEOUT)) + } + + private fun sendReply(request: SmsRetrieverRequest, status: Status, extras: Bundle = Bundle.EMPTY, finish: Boolean = true) { + Log.d(TAG, "Send reply to ${request.packageName} ${CommonStatusCodes.getStatusCodeString(status.statusCode)}") + + val intent = Intent(SmsRetriever.SMS_RETRIEVED_ACTION) + intent.setPackage(request.packageName) + intent.putExtras(extras) + intent.putExtra(SmsRetriever.EXTRA_STATUS, status) + context.sendBroadcast(intent) + + if (finish) finishRequest(request) + } + + fun finishRequest(request: SmsRetrieverRequest) { + alarmManager.cancel(request.timeoutPendingIntent) + requests.remove(request.id) + } + + override fun onDestroy(owner: LifecycleOwner) { + super.onDestroy(owner) + + if (this::smsBroadcastReceiver.isInitialized) context.unregisterReceiver(smsBroadcastReceiver) + if (this::timeoutBroadcastReceiver.isInitialized) context.unregisterReceiver(timeoutBroadcastReceiver) + + for (request in requests.values) { + sendReply(request, Status(CommonStatusCodes.TIMEOUT)) + } + + requests.clear() + } + + @TargetApi(19) + private inner class SmsReceiver : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + if (Telephony.Sms.Intents.SMS_RECEIVED_ACTION == intent.action) { + val messages = Telephony.Sms.Intents.getMessagesFromIntent(intent) + for (message in messages) { + try { + handleIncomingSmsMessage(message) + } catch (e: Exception) { + Log.w(TAG, "Error handling incoming SMS", e) + } + } + } + } + } + + private inner class TimeoutReceiver : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + val requestId = intent.getIntExtra(EXTRA_REQUEST_ID, -1) + if (requestId != -1) { + handleTimeout(requestId) + } + } + } + + @TargetApi(19) + fun getHashString(packageName: String): String { + val signature = + context.packageManager.getSignatures(packageName).firstOrNull()?.toCharsString() ?: throw RuntimeException("No signature found for $packageName") + val appInfo = "$packageName $signature" + val messageDigest = MessageDigest.getInstance("SHA-256") + messageDigest.update(appInfo.toByteArray(StandardCharsets.UTF_8)) + return Base64.encodeToString(messageDigest.digest(), Base64.NO_PADDING or Base64.NO_WRAP).substring(0, 11) + } + + private fun anyOtherPackageHasHashString(packageName: String, hashString: String): Boolean { + val collision = context.packageManager.getInstalledPackages(0) + .firstOrNull { it.packageName != packageName && getHashString(it.packageName) == hashString } ?: return false + + Log.w(TAG, "Hash string collision between $packageName and ${collision.packageName} (both are $hashString)") + return true + } + + private fun isPhoneNumberInContacts(context: Context, phoneNumber: String): Boolean { + fun normalizePhoneNumber(input: String): String { + var output = "" + if (!TextUtils.isEmpty(input)) { + // only keep digits + val temp = input.replace("[^0-9]".toRegex(), "") + // trim leading zeroes + output = temp.replaceFirst("^0*".toRegex(), "") + } + return output + } + + val normalizePhoneNumber = normalizePhoneNumber(phoneNumber) + val cursor = context.contentResolver.query(Phone.CONTENT_URI, arrayOf(Phone.NUMBER), null, null, null) ?: return false + try { + while (cursor.moveToNext()) { + val addressIndex = cursor.getColumnIndex(Phone.NUMBER) + val contactPhoneNumber = normalizePhoneNumber(cursor.getString(addressIndex)) + if (normalizePhoneNumber == contactPhoneNumber) { + return true + } + } + } catch (e: Exception) { + Log.w(TAG, e) + } finally { + cursor.close() + } + return false + } + + private val activePermissionRequestLock = Mutex() + private var activePermissionRequest: Deferred? = null + + private suspend fun ensurePermission(permissions: Array): Boolean { + if (SDK_INT < 23) + return true + + if (permissions.all { ContextCompat.checkSelfPermission(context, it) == PERMISSION_GRANTED }) + return true + + val (completable, deferred) = activePermissionRequestLock.withLock { + if (activePermissionRequest == null) { + val completable = CompletableDeferred() + activePermissionRequest = completable + completable to activePermissionRequest!! + } else { + null to activePermissionRequest!! + } + } + if (completable != null) { + val intent = Intent(context, AskPermissionActivity::class.java) + intent.putExtra(EXTRA_MESSENGER, Messenger(object : Handler(Looper.getMainLooper()) { + override fun handleMessage(msg: Message) { + if (msg.what == Activity.RESULT_OK) { + val grantResults = msg.data?.getIntArray(EXTRA_GRANT_RESULTS) ?: IntArray(0) + completable.complete(grantResults.size == permissions.size && grantResults.all { it == PERMISSION_GRANTED }) + } else { + completable.complete(false) + } + } + })) + intent.putExtra(EXTRA_PERMISSIONS, permissions) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + context.startActivity(intent) + } + return deferred.await() + } +} \ No newline at end of file diff --git a/play-services-auth-api-phone/core/src/main/kotlin/org/microg/gms/auth/phone/SmsRetrieverService.kt b/play-services-auth-api-phone/core/src/main/kotlin/org/microg/gms/auth/phone/SmsRetrieverService.kt new file mode 100644 index 00000000..76fe5359 --- /dev/null +++ b/play-services-auth-api-phone/core/src/main/kotlin/org/microg/gms/auth/phone/SmsRetrieverService.kt @@ -0,0 +1,131 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.auth.phone + +import android.util.Log +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import com.google.android.gms.auth.api.phone.SmsRetrieverStatusCodes +import com.google.android.gms.auth.api.phone.internal.IAutofillPermissionStateCallback +import com.google.android.gms.auth.api.phone.internal.IOngoingSmsRequestCallback +import com.google.android.gms.auth.api.phone.internal.ISmsRetrieverApiService +import com.google.android.gms.auth.api.phone.internal.ISmsRetrieverResultCallback +import com.google.android.gms.common.Feature +import com.google.android.gms.common.api.CommonStatusCodes +import com.google.android.gms.common.api.Status +import com.google.android.gms.common.api.internal.IStatusCallback +import com.google.android.gms.common.internal.ConnectionInfo +import com.google.android.gms.common.internal.GetServiceRequest +import com.google.android.gms.common.internal.IGmsCallbacks +import org.microg.gms.BaseService +import org.microg.gms.common.GmsService +import org.microg.gms.common.PackageUtils + + +private const val TAG = "SmsRetrieverService" +private val FEATURES = arrayOf( + Feature("sms_retrieve", 1), + Feature("user_consent", 3) +) + +class SmsRetrieverService : BaseService(TAG, GmsService.SMS_RETRIEVER) { + private val smsRetriever = SmsRetrieverCore(this, lifecycle) + + override fun handleServiceRequest(callback: IGmsCallbacks, request: GetServiceRequest, service: GmsService) { + val packageName = PackageUtils.getAndCheckCallingPackage(this, request.packageName) + ?: throw IllegalArgumentException("Missing package name") + callback.onPostInitCompleteWithConnectionInfo( + CommonStatusCodes.SUCCESS, + SmsRetrieverServiceImpl(smsRetriever, packageName, lifecycle), + ConnectionInfo().apply { features = FEATURES } + ) + } +} + + +class SmsRetrieverServiceImpl(private val smsRetriever: SmsRetrieverCore, private val packageName: String, override val lifecycle: Lifecycle) : + ISmsRetrieverApiService.Stub(), LifecycleOwner { + + override fun startSmsRetriever(callback: ISmsRetrieverResultCallback) { + Log.d(TAG, "startSmsRetriever()") + lifecycleScope.launchWhenStarted { + val status = try { + smsRetriever.startSmsRetriever(packageName) + Status.SUCCESS + } catch (e: Exception) { + Status(CommonStatusCodes.INTERNAL_ERROR, e.message) + } + try { + callback.onResult(status) + } catch (e: Exception) { + Log.w(TAG, "Failed delivering $status for startSmsRetriever()", e) + } + } + } + + override fun startWithConsentPrompt(senderPhoneNumber: String?, callback: ISmsRetrieverResultCallback) { + Log.d(TAG, "startWithConsentPrompt($senderPhoneNumber)") + lifecycleScope.launchWhenStarted { + val status = try { + smsRetriever.startWithConsentPrompt(packageName, senderPhoneNumber) + Status.SUCCESS + } catch (e: Exception) { + Status(CommonStatusCodes.INTERNAL_ERROR, e.message) + } + try { + callback.onResult(status) + } catch (e: Exception) { + Log.w(TAG, "Failed delivering $status for startWithConsentPrompt()", e) + } + } + } + + override fun startSmsCodeAutofill(callback: IStatusCallback) { + Log.d(TAG, "startSmsCodeAutofill()") + try { + callback.onResult(Status(SmsRetrieverStatusCodes.API_NOT_AVAILABLE)) + } catch (e: Exception) { + Log.w(TAG, "Failed delivering result for startSmsCodeAutofill()", e) + } + } + + override fun checkAutofillPermissionState(callback: IAutofillPermissionStateCallback) { + Log.d(TAG, "checkAutofillPermissionState()") + try { + callback.onCheckPermissionStateResult(Status.SUCCESS, 1) + } catch (e: Exception) { + Log.w(TAG, "Failed delivering result for checkAutofillPermissionState()", e) + } + } + + override fun checkOngoingSmsRequest(packageName: String?, callback: IOngoingSmsRequestCallback) { + Log.d(TAG, "checkOngoingSmsRequest($packageName)") + lifecycleScope.launchWhenStarted { + val result = try { + smsRetriever.hasOngoingUserConsentRequest() + } catch (e: Exception) { + true + } + + try { + callback.onHasOngoingSmsRequestResult(Status.SUCCESS, result) + } catch (e: Exception) { + Log.w(TAG, "Failed delivering $result for checkOngoingSmsRequest()", e) + } + } + } + + + override fun startSmsCodeBrowser(callback: IStatusCallback) { + Log.d(TAG, "startSmsCodeBrowser()") + try { + callback.onResult(Status(SmsRetrieverStatusCodes.API_NOT_AVAILABLE)) + } catch (e: Exception) { + Log.w(TAG, "Failed delivering result for startSmsCodeBrowser()", e) + } + } +} \ No newline at end of file diff --git a/play-services-core/build.gradle b/play-services-core/build.gradle index 6c7ded13..6b496940 100644 --- a/play-services-core/build.gradle +++ b/play-services-core/build.gradle @@ -87,6 +87,7 @@ dependencies { >>>>>>> 1b34bbd3 (Add dummy service for PANORAMA(3) (#2173)) implementation "androidx.webkit:webkit:$webkitVersion" +<<<<<<< HEAD implementation project(':play-services-base-core') implementation project(':play-services-core-proto') implementation project(':play-services-core:microg-ui-tools') // deprecated @@ -98,6 +99,9 @@ dependencies { implementation "androidx.preference:preference-ktx:$preferenceVersion" implementation 'org.microg.gms:conscrypt-gmscore:2.5.2' +======= + // Material Components +>>>>>>> 48e0b00a (Update build tools and dependencies) implementation "com.google.android.material:material:$materialVersion" // Navigation diff --git a/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/LoaderActivity.kt b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/LoaderActivity.kt new file mode 100644 index 00000000..5d165f4e --- /dev/null +++ b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/LoaderActivity.kt @@ -0,0 +1,163 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.accountsettings.ui + +import android.accounts.AccountManager +import android.app.Activity +import android.content.Intent +import android.content.Intent.* +import android.net.Uri +import android.os.Bundle +import android.util.Log +import androidx.appcompat.app.AppCompatActivity +import org.microg.gms.auth.AuthConstants.DEFAULT_ACCOUNT_TYPE +import org.microg.gms.common.PackageUtils +import org.microg.tools.AccountPickerActivity + +private const val TAG = "AccountSettingsLoader" + +private val ALLOWED_FALLBACK_PREFIXES = setOf("https://myaccount.google.com/", "https://takeout.google.com/") +private val BROWSABLE_SCREEN_IDS = setOf(1, 200, 400, 502, 527, 10003, 10050, 12700, 12701) +private val ACCOUNT_CHOOSER_URI = Uri.parse("https://accounts.google.com/AccountChooser") + +private const val QUERY_PARAM_CONTINUE = "continue" +private const val QUERY_PARAM_LANG = "hl" +private const val QUERY_PARAM_EMAIL = "Email" + +private const val EXTRA_ALLOWABLE_ACCOUNT_TYPES = "allowableAccountTypes" + +private const val REQUEST_ACCOUNT_PICKER = 1 + +class LoaderActivity : AppCompatActivity() { + private var canAskForAccount = false + + private fun launchFallback() { + val fallbackUrl = intent?.getStringExtra(EXTRA_FALLBACK_URL) + + if (fallbackUrl == null) { + Log.d(TAG, "No fallback") + finishResult(RESULT_CANCELED) + } else if (fallbackUrl in ALLOWED_FALLBACK_PREFIXES) { + // TODO: Error screen? + Log.d(TAG, "Illegal fallback url") + finishResult(RESULT_CANCELED) + } else { + val fallbackAuth = intent?.getBooleanExtra(EXTRA_FALLBACK_AUTH, false) ?: false + val uri = if (fallbackAuth) { + val builder = ACCOUNT_CHOOSER_URI.buildUpon().appendQueryParameter(QUERY_PARAM_CONTINUE, fallbackUrl) + val accountName = intent?.getStringExtra(EXTRA_ACCOUNT_NAME) + if (!accountName.isNullOrBlank()) { + builder.appendQueryParameter(QUERY_PARAM_EMAIL, accountName) + } + val lang = Uri.parse(fallbackUrl).getQueryParameter(QUERY_PARAM_LANG) + if (lang != null) { + builder.appendQueryParameter(QUERY_PARAM_LANG, lang) + } + builder.build() + } else { + Uri.parse(fallbackUrl) + } + Log.d(TAG, "Opening fallback $fallbackUrl") + // noinspection UnsafeImplicitIntentLaunch + val intent = Intent(ACTION_VIEW, uri).apply { addCategory(CATEGORY_BROWSABLE) } + startActivity(intent) + finishResult(RESULT_OK) + } + } + + private fun launchMain() { + val requestedAccountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME) + val ignoreAccount = intent?.getBooleanExtra(EXTRA_IGNORE_ACCOUNT, false) ?: false + val accounts = AccountManager.get(this).getAccountsByType(DEFAULT_ACCOUNT_TYPE) + val account = if (requestedAccountName != null) { + val account = accounts.find { it.name == requestedAccountName } + if (account == null) { + // TODO: Error screen? + Log.d(TAG, "Account not found: $requestedAccountName") + return finishResult(RESULT_CANCELED) + } + account + } else if (accounts.isEmpty()) { + if (intent?.getStringExtra(EXTRA_FALLBACK_URL) != null) { + return launchFallback() + } else { + // TODO: Error screen? + Log.d(TAG, "No account configured") + return finishResult(RESULT_CANCELED) + } + } else if (accounts.size > 1) { + if (canAskForAccount) { + val intent = Intent(this, AccountPickerActivity::class.java) + intent.putExtra(EXTRA_ALLOWABLE_ACCOUNT_TYPES, arrayOf(DEFAULT_ACCOUNT_TYPE)) + startActivityForResult(intent, REQUEST_ACCOUNT_PICKER) + canAskForAccount = false + return + } else { + return finishResult(RESULT_CANCELED) + } + } else { + accounts.first() + } + + val intent = Intent(this, MainActivity::class.java).apply { + action = intent.action + if (intent.hasExtra(EXTRA_THEME_CHOICE)) putExtra(EXTRA_THEME_CHOICE, intent.getIntExtra(EXTRA_THEME_CHOICE, 0)) + putExtra(EXTRA_ACCOUNT_NAME, account.name) + if (ignoreAccount) putExtra(EXTRA_IGNORE_ACCOUNT, true) + putExtra(EXTRA_SCREEN_ID, intent.getIntExtra(EXTRA_SCREEN_ID, 1)) + for (it in intent.extras?.keySet().orEmpty()) { + if (it.startsWith(EXTRA_SCREEN_OPTIONS_PREFIX)) putExtra(it, intent.getStringExtra(it)) + } + putExtra(EXTRA_CALLING_PACKAGE_NAME, callingActivity?.packageName) + if (intent.action != ACTION_BROWSE_SETTINGS) { + addFlags(FLAG_ACTIVITY_FORWARD_RESULT) + } + } + + startActivity(intent) + + if (!isFinishing && !isChangingConfigurations) { + finishResult(RESULT_OK) + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (requestCode == REQUEST_ACCOUNT_PICKER) { + if (resultCode == RESULT_OK && data?.hasExtra(AccountManager.KEY_ACCOUNT_NAME) == true) { + intent.putExtra(EXTRA_ACCOUNT_NAME, data.getStringExtra(AccountManager.KEY_ACCOUNT_NAME)) + launchMain() + } else { + finishResult(resultCode) + } + } else { + super.onActivityResult(requestCode, resultCode, data) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + canAskForAccount = true + val extras = intent?.extras?.also { it.keySet() } + Log.d(TAG, "Invoked with ${intent.action} and extras $extras") + + super.onCreate(savedInstanceState) + + val isMainAllowed = if (intent == null || intent.action != ACTION_BROWSE_SETTINGS) + PackageUtils.isGooglePackage(this, callingActivity?.packageName) + else + extras?.getInt(EXTRA_SCREEN_ID, -1) in BROWSABLE_SCREEN_IDS + + if (!isMainAllowed) { + launchFallback() + } else if (!isFinishing && !isChangingConfigurations){ + launchMain() + } + } + + private fun finishResult(resultCode: Int) { + setResult(resultCode) + finish() + } +} \ No newline at end of file diff --git a/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/WebViewHelper.kt b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/WebViewHelper.kt new file mode 100644 index 00000000..a65b7a0d --- /dev/null +++ b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/WebViewHelper.kt @@ -0,0 +1,130 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.accountsettings.ui + +import android.content.Intent +import android.content.Intent.URI_INTENT_SCHEME +import android.net.Uri +import android.util.Log +import android.view.View +import android.webkit.WebResourceRequest +import android.webkit.WebSettings +import android.webkit.WebView +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.lifecycleScope +import androidx.webkit.WebResourceErrorCompat +import androidx.webkit.WebViewClientCompat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.microg.gms.auth.AuthManager +import org.microg.gms.common.Constants.GMS_PACKAGE_NAME +import org.microg.gms.common.PackageUtils +import java.net.URLEncoder +import java.util.* + +private const val TAG = "AccountSettingsWebView" + +class WebViewHelper(private val activity: AppCompatActivity, private val webView: WebView, private val allowedPrefixes: Set = emptySet()) { + fun openWebView(url: String?, accountName: String?) { + prepareWebViewSettings(webView.settings) + webView.webViewClient = object : WebViewClientCompat() { + override fun onReceivedError(view: WebView, request: WebResourceRequest, error: WebResourceErrorCompat) { + Log.w(TAG, "Error loading: $error") + } + + override fun onPageFinished(view: WebView?, url: String?) { + super.onPageFinished(view, url) + webView.visibility = View.VISIBLE + } + + override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean { + Log.d(TAG, "Navigating to $url") + if (url.startsWith("intent:")) { + try { + val intent = Intent.parseUri(url, URI_INTENT_SCHEME) + if (intent.`package` == GMS_PACKAGE_NAME || PackageUtils.isGooglePackage(activity, intent.`package`)) { + // Only allow to start Google packages + activity.startActivity(intent) + } else { + Log.w(TAG, "Ignoring request to start non-Google app") + } + } catch (e: Exception) { + Log.w(TAG, "Error invoking intent", e) + } + return false + } + if (allowedPrefixes.isNotEmpty() && allowedPrefixes.none { url.startsWith(it) }) { + try { + // noinspection UnsafeImplicitIntentLaunch + activity.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)).apply { addCategory(Intent.CATEGORY_BROWSABLE) }) + } catch (e: Exception) { + Log.w(TAG, "Error forwarding to browser", e) + } + return true + } + return false + } + } + + val urlWithLanguage: String? = addLanguageParam(url) + if (accountName != null) { + activity.lifecycleScope.launch { + withContext(Dispatchers.IO) { + openWebWithAccount(accountName, urlWithLanguage) + } + } + } else { + loadWebViewUrl(urlWithLanguage) + } + } + + private fun loadWebViewUrl(url: String?) { + if (url != null) { + webView.loadUrl(url) + } else { + activity.finish() + } + } + + private fun addLanguageParam(url: String?): String? { + val language = Locale.getDefault().language + return if (language.isNotEmpty()) { + Uri.parse(url).buildUpon().appendQueryParameter("hl", language).toString() + } else { + url + } + } + + private fun openWebWithAccount(accountName: String, url: String?) { + try { + val service = "weblogin:continue=" + URLEncoder.encode(url, "utf-8") + val authManager = AuthManager(activity, accountName, GMS_PACKAGE_NAME, service) + val authUrl = authManager.requestAuth(false)?.auth + if (authUrl?.contains("WILL_NOT_SIGN_IN") == true) { + throw RuntimeException("Would not sign in") + } + Log.d(TAG, "Opening $authUrl") + webView.post { + loadWebViewUrl(authUrl) + } + } catch (e: Exception) { + Log.w(TAG, "Failed to get weblogin auth.", e) + activity.finish() + } + } + + private fun prepareWebViewSettings(settings: WebSettings) { + settings.javaScriptEnabled = true + settings.setSupportMultipleWindows(false) + settings.allowFileAccess = false + settings.databaseEnabled = false + settings.setNeedInitialFocus(false) + settings.useWideViewPort = false + settings.setSupportZoom(false) + settings.javaScriptCanOpenWindowsAutomatically = false + } +} \ No newline at end of file diff --git a/play-services-core/src/main/kotlin/org/microg/gms/auth/proxy/AuthProxyService.kt b/play-services-core/src/main/kotlin/org/microg/gms/auth/proxy/AuthProxyService.kt index fe3d5e5a..6ec6c5d8 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/auth/proxy/AuthProxyService.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/auth/proxy/AuthProxyService.kt @@ -40,7 +40,7 @@ class AuthProxyService : BaseService(TAG, GmsService.AUTH_PROXY) { } } -class AuthServiceImpl(private val context: Context, private val lifecycle: Lifecycle, private val packageName: String) : IAuthService.Stub(), LifecycleOwner { +class AuthServiceImpl(private val context: Context, override val lifecycle: Lifecycle, private val packageName: String) : IAuthService.Stub(), LifecycleOwner { override fun performProxyRequest(callbacks: IAuthCallbacks, request: ProxyRequest) { Log.d(TAG, "performProxyRequest($packageName, $request)") lifecycleScope.launchWhenStarted { @@ -57,7 +57,12 @@ class AuthServiceImpl(private val context: Context, private val lifecycle: Lifec } } +<<<<<<< HEAD override fun getLifecycle(): Lifecycle = lifecycle override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean = warnOnTransactionIssues(code, reply, flags) { super.onTransact(code, data, reply, flags) } -} \ No newline at end of file +} +======= + override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean = warnOnTransactionIssues(code, reply, flags, TAG) { super.onTransact(code, data, reply, flags) } +} +>>>>>>> 48e0b00a (Update build tools and dependencies) diff --git a/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/AuthSignInService.kt b/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/AuthSignInService.kt index 88cac7e3..f852a7ea 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/AuthSignInService.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/AuthSignInService.kt @@ -61,14 +61,13 @@ class AuthSignInService : BaseService(TAG, GmsService.AUTH_SIGN_IN) { class AuthSignInServiceImpl( private val context: Context, - private val lifecycle: Lifecycle, + override val lifecycle: Lifecycle, private val packageName: String, private val account: Account?, private val scopes: List, private val extras: Bundle ) : ISignInService.Stub(), LifecycleOwner { private val queue = singleInstanceOf { Volley.newRequestQueue(context.applicationContext) } - override fun getLifecycle(): Lifecycle = lifecycle override fun silentSignIn(callbacks: ISignInCallbacks, options: GoogleSignInOptions?) { Log.d(TAG, "$packageName:silentSignIn($options)") diff --git a/play-services-core/src/main/kotlin/org/microg/gms/clearcut/ClearcutLoggerService.kt b/play-services-core/src/main/kotlin/org/microg/gms/clearcut/ClearcutLoggerService.kt new file mode 100644 index 00000000..e149a026 --- /dev/null +++ b/play-services-core/src/main/kotlin/org/microg/gms/clearcut/ClearcutLoggerService.kt @@ -0,0 +1,77 @@ +/* + * SPDX-FileCopyrightText: 2022 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ +package org.microg.gms.clearcut + +import android.os.Parcel +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import com.google.android.gms.clearcut.LogEventParcelable +import com.google.android.gms.clearcut.internal.IClearcutLoggerCallbacks +import com.google.android.gms.clearcut.internal.IClearcutLoggerService +import com.google.android.gms.common.api.CommonStatusCodes +import com.google.android.gms.common.api.Status +import com.google.android.gms.common.data.DataHolder +import com.google.android.gms.common.internal.GetServiceRequest +import com.google.android.gms.common.internal.IGmsCallbacks +import org.microg.gms.BaseService +import org.microg.gms.common.GmsService +import org.microg.gms.utils.warnOnTransactionIssues + +private const val TAG = "ClearcutLoggerService" +private const val COLLECT_FOR_DEBUG_DURATION = 24L * 60 * 60 * 1000 + +class ClearcutLoggerService : BaseService(TAG, GmsService.CLEARCUT_LOGGER) { + override fun handleServiceRequest(callback: IGmsCallbacks, request: GetServiceRequest, service: GmsService) { + callback.onPostInitComplete(0, ClearcutLoggerServiceImpl(lifecycle), null) + } +} + +class ClearcutLoggerServiceImpl(override val lifecycle: Lifecycle) : IClearcutLoggerService.Stub(), LifecycleOwner { + private var collectForDebugExpiryTime: Long = 0 + + override fun log(callbacks: IClearcutLoggerCallbacks, event: LogEventParcelable) { + lifecycleScope.launchWhenStarted { + runCatching { callbacks.onLogResult(Status.SUCCESS) } + } + } + + override fun forceUpload(callbacks: IClearcutLoggerCallbacks) { + lifecycleScope.launchWhenStarted { + runCatching { callbacks.onLogResult(Status.SUCCESS) } + } + } + + override fun startCollectForDebug(callbacks: IClearcutLoggerCallbacks) { + lifecycleScope.launchWhenStarted { + collectForDebugExpiryTime = System.currentTimeMillis() + COLLECT_FOR_DEBUG_DURATION + runCatching { callbacks.onStartCollectForDebugResult(Status.SUCCESS, collectForDebugExpiryTime) } + } + } + + override fun stopCollectForDebug(callbacks: IClearcutLoggerCallbacks) { + lifecycleScope.launchWhenStarted { + runCatching { callbacks.onStopCollectForDebugResult(Status.SUCCESS) } + } + } + + override fun getCollectForDebugExpiryTime(callbacks: IClearcutLoggerCallbacks) { + lifecycleScope.launchWhenStarted { + runCatching { callbacks.onCollectForDebugExpiryTime(Status.SUCCESS, collectForDebugExpiryTime) } + } + } + + override fun getLogEventParcelablesLegacy(callbacks: IClearcutLoggerCallbacks) { + getLogEventParcelables(callbacks) + } + + override fun getLogEventParcelables(callbacks: IClearcutLoggerCallbacks) { + lifecycleScope.launchWhenStarted { + runCatching { callbacks.onLogEventParcelables(DataHolder.empty(CommonStatusCodes.SUCCESS)) } + } + } + + override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean = warnOnTransactionIssues(code, reply, flags, TAG) { super.onTransact(code, data, reply, flags) } +} diff --git a/play-services-core/src/main/kotlin/org/microg/gms/credential/CredentialManagerService.kt b/play-services-core/src/main/kotlin/org/microg/gms/credential/CredentialManagerService.kt new file mode 100644 index 00000000..f2b7d692 --- /dev/null +++ b/play-services-core/src/main/kotlin/org/microg/gms/credential/CredentialManagerService.kt @@ -0,0 +1,89 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.credential + +import android.content.Context +import android.os.Bundle +import android.os.Parcel +import android.util.Base64 +import android.util.Log +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import com.google.android.gms.common.ConnectionResult +import com.google.android.gms.common.Feature +import com.google.android.gms.common.api.Status +import com.google.android.gms.common.api.internal.IStatusCallback +import com.google.android.gms.common.internal.ConnectionInfo +import com.google.android.gms.common.internal.GetServiceRequest +import com.google.android.gms.common.internal.IGmsCallbacks +import com.google.android.gms.credential.manager.common.IPendingIntentCallback +import com.google.android.gms.credential.manager.common.ISettingsCallback +import com.google.android.gms.credential.manager.firstparty.internal.ICredentialManagerService +import com.google.android.gms.credential.manager.invocationparams.CredentialManagerInvocationParams +import org.microg.gms.BaseService +import org.microg.gms.common.GmsService +import org.microg.gms.common.PackageUtils +import org.microg.gms.utils.toBase64 +import org.microg.gms.utils.warnOnTransactionIssues + +private const val TAG = "CredentialManager" + +class CredentialManagerService : BaseService(TAG, GmsService.CREDENTIAL_MANAGER) { + override fun handleServiceRequest(callback: IGmsCallbacks, request: GetServiceRequest, service: GmsService) { + if (!PackageUtils.callerHasExtendedAccess(this)) { + Log.d(TAG, "No access to ${request.packageName}, lacks permission.") + callback.onPostInitComplete(ConnectionResult.API_DISABLED_FOR_CONNECTION, null, null) + return + } + callback.onPostInitCompleteWithConnectionInfo(ConnectionResult.SUCCESS, CredentialManagerServiceImpl(this, lifecycle), ConnectionInfo().apply { + features = arrayOf( + Feature("credential_manager_first_party_api", 1), + Feature("password_checkup_first_party_api", 1), + Feature("user_service_security", 1), + ) + }) + } + +} + +private class CredentialManagerServiceImpl(private val context: Context, override val lifecycle: Lifecycle) : ICredentialManagerService.Stub(), LifecycleOwner { + + override fun getCredentialManagerIntent(callback: IPendingIntentCallback?, params: CredentialManagerInvocationParams?) { + Log.d(TAG, "Not yet implemented: getCredentialManagerIntent $params") + lifecycleScope.launchWhenStarted { + try { + callback?.onPendingIntent(Status.INTERNAL_ERROR, null) + } catch (e: Exception) { + Log.w(TAG, e) + } + } + } + + override fun getSetting(callback: ISettingsCallback?, key: String?) { + Log.d(TAG, "Not yet implemented: getSetting $key") + lifecycleScope.launchWhenStarted { + try { + callback?.onSetting(Status.INTERNAL_ERROR, null) + } catch (e: Exception) { + Log.w(TAG, e) + } + } + } + + override fun setSetting(callback: IStatusCallback?, key: String?, value: ByteArray?) { + Log.d(TAG, "Not yet implemented: setSetting $key ${value?.toBase64(Base64.NO_WRAP)}") + lifecycleScope.launchWhenStarted { + try { + callback?.onResult(Status.INTERNAL_ERROR) + } catch (e: Exception) { + Log.w(TAG, e) + } + } + } + + override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean = warnOnTransactionIssues(code, reply, flags, TAG) { super.onTransact(code, data, reply, flags) } +} \ No newline at end of file diff --git a/play-services-core/src/main/kotlin/org/microg/gms/games/GamesConnectService.kt b/play-services-core/src/main/kotlin/org/microg/gms/games/GamesConnectService.kt new file mode 100644 index 00000000..e5f7486a --- /dev/null +++ b/play-services-core/src/main/kotlin/org/microg/gms/games/GamesConnectService.kt @@ -0,0 +1,94 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.games + +import android.accounts.Account +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.os.Parcel +import android.util.Log +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import com.google.android.gms.common.Scopes +import com.google.android.gms.common.api.CommonStatusCodes +import com.google.android.gms.common.api.Status +import com.google.android.gms.common.internal.ConnectionInfo +import com.google.android.gms.common.internal.GetServiceRequest +import com.google.android.gms.common.internal.IGmsCallbacks +import com.google.android.gms.games.internal.connect.GamesSignInRequest +import com.google.android.gms.games.internal.connect.GamesSignInResponse +import com.google.android.gms.games.internal.connect.IGamesConnectCallbacks +import com.google.android.gms.games.internal.connect.IGamesConnectService +import org.microg.gms.BaseService +import org.microg.gms.auth.AuthManager +import org.microg.gms.auth.AuthPrefs +import org.microg.gms.common.GmsService +import org.microg.gms.common.PackageUtils +import org.microg.gms.utils.warnOnTransactionIssues +import java.util.UUID + +private const val TAG = "GamesConnectService" + +class GamesConnectService : BaseService(TAG, GmsService.GAMES) { + override fun handleServiceRequest(callback: IGmsCallbacks, request: GetServiceRequest, service: GmsService) { + val packageName = PackageUtils.getAndCheckCallingPackage(this, request.packageName) + ?: throw IllegalArgumentException("Missing package name") + callback.onPostInitCompleteWithConnectionInfo( + CommonStatusCodes.SUCCESS, + GamesConnectServiceImpl(this, lifecycle, packageName), + ConnectionInfo() + ) + } +} + +class GamesConnectServiceImpl(val context: Context, override val lifecycle: Lifecycle, val packageName: String) : IGamesConnectService.Stub(), LifecycleOwner { + + override fun signIn(callback: IGamesConnectCallbacks?, request: GamesSignInRequest?) { + Log.d(TAG, "signIn($request)") + fun sendSignInRequired() { + val resolution = PendingIntent.getActivity(context, packageName.hashCode(), Intent(context, GamesSignInActivity::class.java).apply { + putExtra(EXTRA_GAME_PACKAGE_NAME, packageName) + putExtra(EXTRA_SCOPES, arrayOf(Scopes.GAMES_LITE)) + }, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + when (request?.signInType) { + 0 -> { // Manual sign in, provide resolution + callback?.onSignIn(Status(CommonStatusCodes.SIGN_IN_REQUIRED, null, resolution), null) + } + + 1 -> { // Auto sign-in on start, don't provide resolution if not + callback?.onSignIn(Status(CommonStatusCodes.SIGN_IN_REQUIRED), null) + } + + else -> { + callback?.onSignIn(Status(CommonStatusCodes.SIGN_IN_REQUIRED), null) + } + } + } + lifecycleScope.launchWhenStarted { + try { + val account = request?.previousStepResolutionResult?.resultData?.getParcelableExtra(EXTRA_ACCOUNT) + ?: GamesConfigurationService.getDefaultAccount(context, packageName) + ?: return@launchWhenStarted sendSignInRequired() + val authManager = AuthManager(context, account.name, packageName, "oauth2:${Scopes.GAMES_LITE}") + if (!authManager.isPermitted && !AuthPrefs.isTrustGooglePermitted(context)) return@launchWhenStarted sendSignInRequired() + val result = performGamesSignIn(context, packageName, account) + if (result) { + callback?.onSignIn(Status.SUCCESS, GamesSignInResponse().apply { gameRunToken = UUID.randomUUID().toString() }) + } else { + sendSignInRequired() + } + } catch (e: Exception) { + Log.w(TAG, e) + return@launchWhenStarted sendSignInRequired() + } + } + } + + override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean = + warnOnTransactionIssues(code, reply, flags, TAG) { super.onTransact(code, data, reply, flags) } +} \ No newline at end of file diff --git a/play-services-core/src/main/kotlin/org/microg/gms/games/GamesService.kt b/play-services-core/src/main/kotlin/org/microg/gms/games/GamesService.kt new file mode 100644 index 00000000..41ee931c --- /dev/null +++ b/play-services-core/src/main/kotlin/org/microg/gms/games/GamesService.kt @@ -0,0 +1,572 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.games + +import android.accounts.Account +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.os.IBinder +import android.os.Parcel +import android.util.Log +import androidx.core.os.bundleOf +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import com.google.android.gms.common.ConnectionResult +import com.google.android.gms.common.Scopes +import com.google.android.gms.common.api.CommonStatusCodes +import com.google.android.gms.common.api.Scope +import com.google.android.gms.common.api.Status +import com.google.android.gms.common.data.DataHolder +import com.google.android.gms.common.internal.ConnectionInfo +import com.google.android.gms.common.internal.GetServiceRequest +import com.google.android.gms.common.internal.IGmsCallbacks +import com.google.android.gms.games.Player +import com.google.android.gms.games.PlayerColumns +import com.google.android.gms.games.PlayerEntity +import com.google.android.gms.games.internal.IGamesCallbacks +import com.google.android.gms.games.internal.IGamesClient +import com.google.android.gms.games.internal.IGamesService +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.json.JSONObject +import org.microg.gms.BaseService +import org.microg.gms.auth.AuthConstants +import org.microg.gms.auth.AuthManager +import org.microg.gms.auth.AuthPrefs +import org.microg.gms.common.GmsService +import org.microg.gms.common.PackageUtils +import org.microg.gms.utils.warnOnTransactionIssues + +private const val TAG = "GamesService" + +class GamesService : BaseService(TAG, GmsService.GAMES) { + override fun handleServiceRequest(callback: IGmsCallbacks, request: GetServiceRequest, service: GmsService) { + val packageName = PackageUtils.getAndCheckCallingPackageOrExtendedAccess(this, request.packageName) + ?: throw IllegalArgumentException("Missing package name") + + fun sendSignInRequired() { + Log.d(TAG, "Sending SIGN_IN_REQUIRED to $packageName") + callback.onPostInitCompleteWithConnectionInfo(ConnectionResult.SIGN_IN_REQUIRED, null, ConnectionInfo().apply { + params = bundleOf( + "pendingIntent" to PendingIntent.getActivity( + this@GamesService, + packageName.hashCode(), + Intent(this@GamesService, GamesSignInActivity::class.java).apply { + putExtra(EXTRA_GAME_PACKAGE_NAME, request.packageName) + putExtra(EXTRA_ACCOUNT, request.account) + putExtra(EXTRA_SCOPES, request.scopes) + }, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + ) + }) + } + + lifecycleScope.launchWhenStarted { + try { + val account = request.account?.takeIf { it.name != AuthConstants.DEFAULT_ACCOUNT } + ?: GamesConfigurationService.getDefaultAccount(this@GamesService, packageName) + ?: return@launchWhenStarted sendSignInRequired() + + val scopes = (request.scopes.toSet() + Scope(Scopes.GAMES_LITE)).toList().sortedBy { it.scopeUri } + val authManager = AuthManager(this@GamesService, account.name, packageName, "oauth2:${scopes.joinToString(" ")}") + if (!authManager.isPermitted && !AuthPrefs.isTrustGooglePermitted(this@GamesService)) { + Log.d(TAG, "Not permitted to use $account for ${scopes.toList()}, sign in required") + return@launchWhenStarted sendSignInRequired() + } + + if (!performGamesSignIn(this@GamesService, packageName, account, scopes = scopes)) { + return@launchWhenStarted sendSignInRequired() + } + + val player = JSONObject(GamesConfigurationService.getPlayer(this@GamesService, packageName, account)).toPlayer() + + callback.onPostInitCompleteWithConnectionInfo( + CommonStatusCodes.SUCCESS, + GamesServiceImpl(this@GamesService, lifecycle, packageName, account, player), + ConnectionInfo() + ) + } catch (e: Exception) { + Log.w(TAG, e) + runCatching { callback.onPostInitCompleteWithConnectionInfo(ConnectionResult.INTERNAL_ERROR, null, null) } + } + } + } +} + +class GamesServiceImpl(val context: Context, override val lifecycle: Lifecycle, val packageName: String, val account: Account, val player: Player) : + IGamesService.Stub(), LifecycleOwner { + + override fun clientDisconnecting(clientId: Long) { + Log.d(TAG, "Not yet implemented: clientDisconnecting($clientId)") + } + + override fun signOut(callbacks: IGamesCallbacks?) { + Log.d(TAG, "Not yet implemented: signOut") + lifecycleScope.launchWhenStarted { + GamesConfigurationService.setDefaultAccount(context, packageName, null) + callbacks?.onSignOutComplete() + } + } + + override fun getAppId(): String? { + Log.d(TAG, "Not yet implemented: getAppId") + return null + } + + override fun getConnectionHint(): Bundle? { + return null + } + + override fun showWelcomePopup(windowToken: IBinder?, extraArgs: Bundle?) { + runCatching { extraArgs?.keySet() } + Log.d(TAG, "Not yet implemented: showWelcomePopup($windowToken, $extraArgs)") + } + + override fun cancelPopups() { + Log.d(TAG, "Not yet implemented: cancelPopups") + } + + override fun getCurrentAccountName(): String? { + Log.d(TAG, "Not yet implemented: getCurrentAccountName") + return null + } + + override fun loadGameplayAclInternal(callbacks: IGamesCallbacks?, gameId: String?) { + Log.d(TAG, "Not yet implemented: loadGameplayAclInternal($gameId)") + } + + override fun updateGameplayAclInternal(callbacks: IGamesCallbacks?, gameId: String?, aclData: String?) { + Log.d(TAG, "Not yet implemented: updateGameplayAclInternal($gameId, $aclData)") + } + + override fun loadFAclInternal(callbacks: IGamesCallbacks?, gameId: String?) { + Log.d(TAG, "Not yet implemented: loadFAclInternal($gameId)") + } + + override fun updateFAclInternal(callbacks: IGamesCallbacks?, gameId: String?, allCirclesVisible: Boolean, circleIds: LongArray?) { + Log.d(TAG, "Not yet implemented: updateFAclInternal($gameId, $allCirclesVisible, $circleIds)") + } + + override fun getCurrentPlayerId(): String? { + Log.d(TAG, "Not yet implemented: getCurrentPlayerId") + return null + } + + override fun getCurrentPlayer(): DataHolder? { + return if (player is PlayerEntity) { + DataHolder.builder(PlayerColumns.CURRENT_PLAYER_COLUMNS.toTypedArray()).withRow(player.toContentValues()).build(CommonStatusCodes.SUCCESS) + } else { + DataHolder.builder(PlayerColumns.CURRENT_PLAYER_COLUMNS.toTypedArray()).build(CommonStatusCodes.SIGN_IN_REQUIRED) + } + } + + override fun loadPlayer(callbacks: IGamesCallbacks?, playerId: String?) { + Log.d(TAG, "Not yet implemented: loadPlayer($playerId)") + } + + override fun loadInvitablePlayers(callbacks: IGamesCallbacks?, pageSize: Int, expandCachedData: Boolean, forceReload: Boolean) { + Log.d(TAG, "Not yet implemented: loadInvitablePlayers($pageSize, $expandCachedData, $forceReload)") + } + + override fun submitScore(callbacks: IGamesCallbacks?, leaderboardId: String?, score: Long) { + Log.d(TAG, "Not yet implemented: submitScore($leaderboardId, $score)") + } + + override fun loadLeaderboards(callbacks: IGamesCallbacks?) { + Log.d(TAG, "Not yet implemented: loadLeaderboards") + } + + override fun loadLeaderboard(callbacks: IGamesCallbacks?, leaderboardId: String?) { + Log.d(TAG, "Not yet implemented: loadLeaderboard($leaderboardId)") + } + + override fun loadTopScores( + callbacks: IGamesCallbacks?, + leaderboardId: String?, + span: Int, + leaderboardCollection: Int, + maxResults: Int, + forceReload: Boolean + ) { + Log.d(TAG, "Not yet implemented: loadTopScores($leaderboardId, $span, $leaderboardCollection, $maxResults, $forceReload)") + } + + override fun loadPlayerCenteredScores( + callbacks: IGamesCallbacks?, + leaderboardId: String?, + span: Int, + leaderboardCollection: Int, + maxResults: Int, + forceReload: Boolean + ) { + Log.d(TAG, "Not yet implemented: loadPlayerCenteredScores($leaderboardId, $span, $leaderboardCollection, $maxResults, $forceReload)") + } + + override fun loadMoreScores(callbacks: IGamesCallbacks?, previousheader: Bundle?, maxResults: Int, pageDirection: Int) { + runCatching { previousheader?.keySet() } + Log.d(TAG, "Not yet implemented: loadMoreScores($previousheader, $maxResults, $pageDirection)") + } + + override fun loadAchievements(callbacks: IGamesCallbacks?) { + loadAchievementsV2(callbacks, false) + } + + override fun revealAchievement(callbacks: IGamesCallbacks?, achievementId: String?, windowToken: IBinder?, extraArgs: Bundle?) { + runCatching { extraArgs?.keySet() } + Log.d(TAG, "Not yet implemented: revealAchievement($achievementId, $windowToken, $extraArgs)") + } + + override fun unlockAchievement(callbacks: IGamesCallbacks?, achievementId: String?, windowToken: IBinder?, extraArgs: Bundle?) { + runCatching { extraArgs?.keySet() } + Log.d(TAG, "Not yet implemented: unlockAchievement($achievementId, $windowToken, $extraArgs") + } + + override fun incrementAchievement(callbacks: IGamesCallbacks?, achievementId: String?, numSteps: Int, windowToken: IBinder?, extraArgs: Bundle?) { + runCatching { extraArgs?.keySet() } + Log.d(TAG, "Not yet implemented: incrementAchievement($achievementId, $numSteps, $windowToken, $extraArgs)") + } + + override fun loadGame(callbacks: IGamesCallbacks?) { + Log.d(TAG, "Not yet implemented: loadGame") + } + + override fun loadInvitations(callbacks: IGamesCallbacks?) { + Log.d(TAG, "Not yet implemented: loadInvitations") + } + + override fun declineInvitation(invitationId: String?, invitationType: Int) { + Log.d(TAG, "Not yet implemented: declineInvitation($invitationId, $invitationType)") + } + + override fun dismissInvitation(invitationId: String?, invitationType: Int) { + Log.d(TAG, "Not yet implemented: dismissInvitation($invitationId, $invitationType)") + } + + override fun createRoom( + callbacks: IGamesCallbacks?, + processBinder: IBinder?, + variant: Int, + invitedPlayerIds: Array?, + autoMatchCriteria: Bundle?, + enableSockets: Boolean, + clientId: Long + ) { + Log.d(TAG, "Not yet implemented: createRoom($variant, $invitedPlayerIds, $autoMatchCriteria, $enableSockets, $clientId)") + } + + override fun joinRoom(callbacks: IGamesCallbacks?, processBinder: IBinder?, matchId: String?, enableSockets: Boolean, clientId: Long) { + Log.d(TAG, "Not yet implemented: joinRoom($matchId, $enableSockets, $clientId)") + } + + override fun leaveRoom(callbacks: IGamesCallbacks?, matchId: String?) { + Log.d(TAG, "Not yet implemented: leaveRoom($matchId)") + } + + override fun sendReliableMessage(callbacks: IGamesCallbacks?, messageData: ByteArray?, matchId: String?, recipientParticipantId: String?): Int { + Log.d(TAG, "Not yet implemented: sendReliableMessage($messageData, $matchId, $recipientParticipantId)") + return 0 + } + + override fun sendUnreliableMessage(messageData: ByteArray?, matchId: String?, recipientParticipantIds: Array?): Int { + Log.d(TAG, "Not yet implemented: sendUnreliableMessage($messageData, $matchId, $recipientParticipantIds)") + return 0 + } + + override fun createSocketConnection(participantId: String?): String? { + Log.d(TAG, "Not yet implemented: createSocketConnection($participantId)") + return null + } + + override fun clearNotifications(notificationTypes: Int) { + Log.d(TAG, "Not yet implemented: clearNotifications($notificationTypes)") + } + + override fun loadLeaderboardsFirstParty(callbacks: IGamesCallbacks?, gameId: String?) { + Log.d(TAG, "Not yet implemented: loadLeaderboardsFirstParty($gameId)") + } + + override fun loadLeaderboardFirstParty(callbacks: IGamesCallbacks?, gameId: String?, leaderboardId: String?) { + Log.d(TAG, "Not yet implemented: loadLeaderboardFirstParty($gameId, $leaderboardId)") + } + + override fun loadTopScoresFirstParty( + callbacks: IGamesCallbacks?, + gameId: String?, + leaderboardId: String?, + span: Int, + leaderboardCollection: Int, + maxResults: Int, + forceReload: Boolean + ) { + Log.d(TAG, "Not yet implemented: loadTopScoresFirstParty($gameId, $leaderboardId, $span, $leaderboardCollection, $maxResults, $forceReload)") + } + + override fun loadPlayerCenteredScoresFirstParty( + callbacks: IGamesCallbacks?, + gameId: String?, + leaderboardId: String?, + span: Int, + leaderboardCollection: Int, + maxResults: Int, + forceReload: Boolean + ) { + Log.d(TAG, "Not yet implemented: loadPlayerCenteredScoresFirstParty($gameId, $leaderboardId, $span, $leaderboardCollection, $maxResults, $forceReload)") + } + + override fun loadAchievementsFirstParty(callbacks: IGamesCallbacks?, playerId: String?, gameId: String?) { + Log.d(TAG, "Not yet implemented: loadAchievementsFirstParty($playerId, $gameId)") + } + + override fun loadGameFirstParty(callbacks: IGamesCallbacks?, gameId: String?) { + Log.d(TAG, "Not yet implemented: loadGameFirstParty($gameId)") + } + + override fun loadGameInstancesFirstParty(callbacks: IGamesCallbacks?, gameId: String?) { + Log.d(TAG, "Not yet implemented: loadGameInstancesFirstParty($gameId)") + } + + override fun loadGameCollectionFirstParty( + callbacks: IGamesCallbacks?, + pageSize: Int, + collectionType: Int, + expandCachedData: Boolean, + forceReload: Boolean + ) { + Log.d(TAG, "Not yet implemented: loadGameCollectionFirstParty($pageSize, $collectionType, $expandCachedData, $forceReload)") + } + + override fun loadRecentlyPlayedGamesFirstParty( + callbacks: IGamesCallbacks?, + externalPlayerId: String?, + pageSize: Int, + expandCachedData: Boolean, + forceReload: Boolean + ) { + Log.d(TAG, "Not yet implemented: loadRecentlyPlayedGamesFirstParty($externalPlayerId, $pageSize, $expandCachedData, $forceReload)") + } + + override fun loadInvitablePlayersFirstParty(callbacks: IGamesCallbacks?, pageSize: Int, expandCachedData: Boolean, forceReload: Boolean) { + Log.d(TAG, "Not yet implemented: loadInvitablePlayersFirstParty($pageSize, $expandCachedData, $forceReload)") + } + + override fun loadRecentPlayersFirstParty(callbacks: IGamesCallbacks?) { + Log.d(TAG, "Not yet implemented: loadRecentPlayersFirstParty") + } + + override fun loadCircledPlayersFirstParty(callbacks: IGamesCallbacks?, pageSize: Int, expandCachedData: Boolean, forceReload: Boolean) { + Log.d(TAG, "Not yet implemented: loadCircledPlayersFirstParty($pageSize, $expandCachedData, $forceReload)") + } + + override fun loadSuggestedPlayersFirstParty(callbacks: IGamesCallbacks?) { + Log.d(TAG, "Not yet implemented: loadSuggestedPlayersFirstParty") + } + + override fun dismissPlayerSuggestionFirstParty(playerIdToDismiss: String?) { + Log.d(TAG, "Not yet implemented: dismissPlayerSuggestionFirstParty($playerIdToDismiss)") + } + + override fun declineInvitationFirstParty(gameId: String?, invitationId: String?, invitationType: Int) { + Log.d(TAG, "Not yet implemented: declineInvitationFirstParty($gameId, $invitationId, $invitationType)") + } + + override fun loadInvitationsFirstParty(callbacks: IGamesCallbacks?, gameId: String?) { + Log.d(TAG, "Not yet implemented: loadInvitationsFirstParty($gameId)") + } + + override fun registerWaitingRoomListenerRestricted(callbacks: IGamesCallbacks?, roomId: String?): Int { + Log.d(TAG, "Not yet implemented: registerWaitingRoomListenerRestricted($roomId)") + return 0 + } + + override fun setGameMuteStatusInternal(callbacks: IGamesCallbacks?, gameId: String?, muted: Boolean) { + Log.d(TAG, "Not yet implemented: setGameMuteStatusInternal($gameId, $muted)") + } + + override fun clearNotificationsFirstParty(gameId: String?, notificationTypes: Int) { + Log.d(TAG, "Not yet implemented: clearNotificationsFirstParty($gameId, $notificationTypes)") + } + + override fun loadNotifyAclInternal(callbacks: IGamesCallbacks?) { + Log.d(TAG, "Not yet implemented: loadNotifyAclInternal") + } + + override fun updateNotifyAclInternal(callbacks: IGamesCallbacks?, aclData: String?) { + Log.d(TAG, "Not yet implemented: updateNotifyAclInternal($aclData)") + } + + override fun registerInvitationListener(callbacks: IGamesCallbacks?, clientId: Long) { + Log.d(TAG, "Not yet implemented: registerInvitationListener($clientId)") + } + + override fun unregisterInvitationListener(clientId: Long) { + Log.d(TAG, "Not yet implemented: unregisterInvitationListener($clientId)") + } + + override fun unregisterWaitingRoomListenerRestricted(roomId: String?): Int { + Log.d(TAG, "Not yet implemented: unregisterWaitingRoomListenerRestricted($roomId)") + return 0 + } + + override fun isGameMutedInternal(callbacks: IGamesCallbacks?, gameId: String?) { + Log.d(TAG, "Not yet implemented: isGameMutedInternal($gameId)") + } + + override fun loadContactSettingsInternal(callbacks: IGamesCallbacks?) { + Log.d(TAG, "Not yet implemented: loadContactSettingsInternal") + } + + override fun updateContactSettingsInternal(callbacks: IGamesCallbacks?, enableMobileNotifications: Boolean) { + Log.d(TAG, "Not yet implemented: updateContactSettingsInternal($enableMobileNotifications)") + } + + override fun getSelectedAccountForGameFirstParty(gamePackageName: String?): String? { + Log.d(TAG, "Not yet implemented: getSelectedAccountForGameFirstParty($gamePackageName)") + return null + } + + override fun updateSelectedAccountForGameFirstParty(gamePackageName: String?, accountName: String?) { + Log.d(TAG, "Not yet implemented: updateSelectedAccountForGameFirstParty($gamePackageName, $accountName)") + } + + override fun getGamesContentUriRestricted(gameId: String?): Uri? { + Log.d(TAG, "Not yet implemented: getGamesContentUriRestricted($gameId)") + return null + } + + override fun shouldUseNewPlayerNotificationsFirstParty(): Boolean { + Log.d(TAG, "Not yet implemented: shouldUseNewPlayerNotificationsFirstParty") + return false + } + + override fun setUseNewPlayerNotificationsFirstParty(newPlayerStyle: Boolean) { + Log.d(TAG, "Not yet implemented: setUseNewPlayerNotificationsFirstParty($newPlayerStyle)") + } + + override fun searchForPlayersFirstParty(callbacks: IGamesCallbacks?, query: String?, pageSize: Int, expandCachedData: Boolean, forceReload: Boolean) { + Log.d(TAG, "Not yet implemented: searchForPlayersFirstParty($query, $pageSize, $expandCachedData, $forceReload)") + } + + override fun getCurrentGame(): DataHolder? { + Log.d(TAG, "Not yet implemented: getCurrentGame") + return null + } + + override fun loadAchievementsV2(callbacks: IGamesCallbacks?, forceReload: Boolean) { + Log.d(TAG, "Not yet implemented: loadAchievementsV2($forceReload)") + callbacks?.onAchievementsLoaded(DataHolder.empty(CommonStatusCodes.SUCCESS)) + } + + override fun submitLeaderboardScore(callbacks: IGamesCallbacks?, leaderboardId: String?, score: Long, scoreTag: String?) { + Log.d(TAG, "Not yet implemented: submitLeaderboardScore($leaderboardId, $score, $scoreTag)") + } + + override fun setAchievementSteps(callbacks: IGamesCallbacks?, id: String?, numSteps: Int, windowToken: IBinder?, extras: Bundle?) { + runCatching { extras?.keySet() } + Log.d(TAG, "Not yet implemented: setAchievementSteps($id, $numSteps, $windowToken, $extras)") + } + + private fun getGamesIntent(action: String, block: Intent.() -> Unit = {}) = Intent(action).apply { + setPackage(GAMES_PACKAGE_NAME) + putExtra(EXTRA_GAME_PACKAGE_NAME, packageName) + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + block() + } + + override fun getAllLeaderboardsIntent(): Intent = getGamesIntent(ACTION_VIEW_LEADERBOARDS) + + override fun getAchievementsIntent(): Intent = getGamesIntent(ACTION_VIEW_ACHIEVEMENTS) + + override fun getPlayerSearchIntent(): Intent = getGamesIntent(ACTION_PLAYER_SEARCH) + + override fun loadEvents(callbacks: IGamesCallbacks?, forceReload: Boolean) { + Log.d(TAG, "Not yet implemented: loadEvents($forceReload)") + } + + override fun incrementEvent(eventId: String?, incrementAmount: Int) { + Log.d(TAG, "Not yet implemented: incrementEvent($eventId, $incrementAmount)") + } + + override fun loadEventsById(callbacks: IGamesCallbacks?, forceReload: Boolean, eventsIds: Array?) { + Log.d(TAG, "Not yet implemented: loadEventsById($forceReload, $eventsIds)") + } + + override fun getMaxDataSize(): Int { + return 3 * 1024 * 1024 + } + + override fun getMaxCoverImageSize(): Int { + return 800 * 1024 + } + + override fun registerEventClient(callback: IGamesClient?, l: Long) { + Log.d(TAG, "Not yet implemented: registerEventClient($l)") + } + + private fun getCompareProfileIntent(playerId: String, block: Intent.() -> Unit = {}): Intent = getGamesIntent(ACTION_VIEW_PROFILE) { + putExtra(EXTRA_IS_SELF, playerId == currentPlayerId) + putExtra(EXTRA_ACCOUNT, currentAccount) + block() + } + + override fun getCompareProfileIntentForPlayer(player: PlayerEntity): Intent = getCompareProfileIntent(player.playerId) { + putExtra(EXTRA_PLAYER, player) + } + + override fun loadPlayerStats(callbacks: IGamesCallbacks?, forceReload: Boolean) { + Log.d(TAG, "Not yet implemented: loadPlayerStats($forceReload)") + } + + override fun getCurrentAccount(): Account? { + Log.d(TAG, "Not yet implemented: getCurrentAccount") + return account + } + + override fun isTelevision(): Boolean { + Log.d(TAG, "Not yet implemented: isTelevision") + return false + } + + override fun getCompareProfileIntentWithAlternativeNameHints( + otherPlayerId: String, + otherPlayerInGameName: String?, + currentPlayerInGameName: String? + ): Intent = getCompareProfileIntent(otherPlayerId) { + putExtra(EXTRA_PLAYER_ID, otherPlayerId) + putExtra(EXTRA_OTHER_PLAYER_IN_GAME_NAME, otherPlayerInGameName) + putExtra(EXTRA_SELF_IN_GAME_NAME, currentPlayerInGameName) + } + + override fun requestServerSideAccess(callbacks: IGamesCallbacks, serverClientId: String, forceRefreshToken: Boolean) { + lifecycleScope.launchWhenStarted { + try { + val serverAuthTokenResponse = withContext(Dispatchers.IO) { + val serverAuthTokenManager = AuthManager(context, account.name, packageName, "oauth2:server:client_id:${serverClientId}:api_scope:${Scopes.GAMES_LITE}") + serverAuthTokenManager.setOauth2Prompt(if (forceRefreshToken) "consent" else "auto") + serverAuthTokenManager.setItCaveatTypes("2") + serverAuthTokenManager.isPermitted = true + serverAuthTokenManager.invalidateAuthToken() + serverAuthTokenManager.requestAuth(true) + } + if (serverAuthTokenResponse.auth != null) { + callbacks.onServerAuthCode(Status(CommonStatusCodes.SUCCESS), serverAuthTokenResponse.auth) + } else { + callbacks.onServerAuthCode(Status(CommonStatusCodes.SIGN_IN_REQUIRED), null) + } + } catch (e: Exception) { + Log.w(TAG, e) + runCatching { callbacks.onServerAuthCode(Status(CommonStatusCodes.INTERNAL_ERROR), null) } + } + } + } + + override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean = + warnOnTransactionIssues(code, reply, flags, TAG) { super.onTransact(code, data, reply, flags) } +} \ No newline at end of file diff --git a/play-services-core/src/main/kotlin/org/microg/gms/gcm/PushRegisterService.kt b/play-services-core/src/main/kotlin/org/microg/gms/gcm/PushRegisterService.kt index 4b7b44bc..3f942d62 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/gcm/PushRegisterService.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/gcm/PushRegisterService.kt @@ -191,8 +191,7 @@ class PushRegisterService : LifecycleService() { } } -internal class PushRegisterHandler(private val context: Context, private val database: GcmDatabase, private val lifecycle: Lifecycle) : Handler(), LifecycleOwner { - override fun getLifecycle(): Lifecycle = lifecycle +internal class PushRegisterHandler(private val context: Context, private val database: GcmDatabase, override val lifecycle: Lifecycle) : Handler(), LifecycleOwner { private var callingUid = 0 override fun sendMessageAtTime(msg: Message, uptimeMillis: Long): Boolean { @@ -339,7 +338,7 @@ internal class PushRegisterHandler(private val context: Context, private val dat class PushRegisterReceiver : WakefulBroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { val intent2 = Intent(context, PushRegisterService::class.java) - if (intent.extras!!["delete"] != null) { + if (intent.extras!!.get("delete") != null) { intent2.action = ACTION_C2DM_UNREGISTER } else { intent2.action = ACTION_C2DM_REGISTER diff --git a/play-services-core/src/main/kotlin/org/microg/gms/googlehelp/ui/GoogleHelpRedirectActivity.kt b/play-services-core/src/main/kotlin/org/microg/gms/googlehelp/ui/GoogleHelpRedirectActivity.kt new file mode 100644 index 00000000..27f5667e --- /dev/null +++ b/play-services-core/src/main/kotlin/org/microg/gms/googlehelp/ui/GoogleHelpRedirectActivity.kt @@ -0,0 +1,38 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.googlehelp.ui + +import android.content.Intent +import android.os.Bundle +import android.util.Log +import androidx.appcompat.app.AppCompatActivity +import com.google.android.gms.googlehelp.GoogleHelp + +private const val TAG = "GoogleHelpRedirect" + +class GoogleHelpRedirectActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val callingPackage = callingActivity?.packageName ?: return finish() + val intent = intent ?: return finish() + val googleHelp = intent.getParcelableExtra(EXTRA_GOOGLE_HELP) ?: return finish() + Log.d(TAG, "Using GoogleHelp: $googleHelp") + val uri = googleHelp.uri ?: return finish() + // TODO: Not all Google apps send proper URI values, as these are in fact not used by Google's original implementation. + // As a work-around we should get the proper URL by retrieving top_level_topic_url:$callingPackage + // from https://www.google.com/tools/feedback/mobile/get-configurations endpoint. + // Long-term best is to not redirect to web but instead implement the thing properly, allowing us also to show + // option items, do proper theming for better integration, etc. + Log.d(TAG, "Open $uri for $callingPackage/${googleHelp.appContext} in Browser") + // noinspection UnsafeImplicitIntentLaunch + startActivity(Intent(Intent.ACTION_VIEW, uri)) + finish() + } + + companion object { + const val EXTRA_GOOGLE_HELP = "EXTRA_GOOGLE_HELP" + } +} \ No newline at end of file diff --git a/play-services-core/src/main/kotlin/org/microg/gms/signin/SignInService.kt b/play-services-core/src/main/kotlin/org/microg/gms/signin/SignInService.kt new file mode 100644 index 00000000..14bad51a --- /dev/null +++ b/play-services-core/src/main/kotlin/org/microg/gms/signin/SignInService.kt @@ -0,0 +1,138 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.signin + +import android.accounts.Account +import android.accounts.AccountManager +import android.content.Context +import android.os.Bundle +import android.os.Parcel +import android.util.Log +import androidx.core.content.getSystemService +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import com.google.android.gms.common.ConnectionResult +import com.google.android.gms.common.api.CommonStatusCodes +import com.google.android.gms.common.api.Scope +import com.google.android.gms.common.internal.* +import com.google.android.gms.signin.internal.* +import org.microg.gms.BaseService +import org.microg.gms.common.GmsService +import org.microg.gms.common.PackageUtils +import org.microg.gms.utils.warnOnTransactionIssues + +private const val TAG = "SignInService" + +class SignInService : BaseService(TAG, GmsService.SIGN_IN) { + override fun handleServiceRequest(callback: IGmsCallbacks, request: GetServiceRequest, service: GmsService) { + val packageName = PackageUtils.getAndCheckCallingPackage(this, request.packageName) + ?: throw IllegalArgumentException("Missing package name") + val binder = SignInServiceImpl(this, lifecycle, packageName, request.scopes).asBinder() + callback.onPostInitComplete(CommonStatusCodes.SUCCESS, binder, Bundle()) + } +} + +class SignInServiceImpl(val context: Context, override val lifecycle: Lifecycle, val packageName: String, val scopes: Array) : ISignInService.Stub(), + LifecycleOwner { + + override fun clearAccountFromSessionStore(sessionId: Int) { + Log.d(TAG, "Not yet implemented: clearAccountFromSessionStore $sessionId") + } + + override fun putAccount(sessionId: Int, account: Account?, callbacks: ISignInCallbacks?) { + Log.d(TAG, "Not yet implemented: putAccount") + } + + override fun saveDefaultAccount(accountAccessor: IAccountAccessor?, sessionId: Int, crossClient: Boolean) { + Log.d(TAG, "Not yet implemented: saveDefaultAccount $sessionId $crossClient") + } + + override fun saveConsent(request: RecordConsentRequest?, callbacks: ISignInCallbacks?) { + Log.d(TAG, "Not yet implemented: saveConsent") + } + + override fun getCurrentAccount(callbacks: ISignInCallbacks?) { + Log.d(TAG, "Not yet implemented: getCurrentAccount") + } + + override fun signIn(request: SignInRequest?, callbacks: ISignInCallbacks?) { + Log.d(TAG, "signIn($request)") + val account = request?.request?.account + val result = if (account == null || context.getSystemService()?.getAccountsByType(account.type)?.contains(account) != true) + ConnectionResult(ConnectionResult.SIGN_IN_REQUIRED) else ConnectionResult(ConnectionResult.SUCCESS) + runCatching { + callbacks?.onSignIn(SignInResponse().apply { + connectionResult = result + response = ResolveAccountResponse().apply { + connectionResult = result + if (account != null) { + accountAccessor = object : IAccountAccessor.Stub() { + override fun getAccount(): Account { + return account + } + } + } + } + }) + } +// fun sendError() { +// runCatching { +// callbacks?.onSignIn(SignInResponse().apply { +// connectionResult = ConnectionResult(ConnectionResult.INTERNAL_ERROR) +// response = ResolveAccountResponse().apply { +// connectionResult = ConnectionResult(ConnectionResult.INTERNAL_ERROR) +// } +// }) +// } +// } +// Log.d(TAG, "Not yet implemented: signIn $request with $scopes") +// val account = request?.request?.account ?: return sendError() +// val authManager = AuthManager(context, account.name, packageName, "oauth2:${scopes.joinToString(" ") { it.scopeUri }}") +// authManager.setItCaveatTypes("2") +// if (!authManager.isPermitted && !AuthPrefs.isTrustGooglePermitted(context)) return sendError() +// lifecycleScope.launchWhenStarted { +// val authResponse = withContext(Dispatchers.IO) { +// authManager.requestAuth(true) +// } +// if (authResponse.auths == null) return@launchWhenStarted sendError() +// runCatching { +// callbacks?.onSignIn(SignInResponse().apply { +// connectionResult = ConnectionResult(ConnectionResult.SUCCESS) +// response = ResolveAccountResponse().apply { +// connectionResult = ConnectionResult(ConnectionResult.SUCCESS) +// } +// }) +// } +// } + } + + override fun setGamesHasBeenGreeted(hasGreeted: Boolean) { + Log.d(TAG, "Not yet implemented: setGamesHasBeenGreeted") + } + + override fun recordConsentByConsentResult(request: RecordConsentByConsentResultRequest?, callbacks: ISignInCallbacks?) { + Log.d(TAG, "Not yet implemented: recordConsentByConsentResult") + } + + override fun authAccount(request: AuthAccountRequest?, callbacks: ISignInCallbacks?) { + Log.d(TAG, "Not yet implemented: authAccount") + } + + override fun onCheckServerAuthorization(result: CheckServerAuthResult?) { + Log.d(TAG, "Not yet implemented: onCheckServerAuthorization") + } + + override fun onUploadServerAuthCode(sessionId: Int) { + Log.d(TAG, "Not yet implemented: onUploadServerAuthCode") + } + + override fun resolveAccount(request: ResolveAccountRequest?, callbacks: IResolveAccountCallbacks?) { + Log.d(TAG, "Not yet implemented: resolveAccount") + } + + override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean = + warnOnTransactionIssues(code, reply, flags, TAG) { super.onTransact(code, data, reply, flags) } +} \ No newline at end of file diff --git a/play-services-core/src/main/res/values-night-v31/themes.xml b/play-services-core/src/main/res/values-night-v31/themes.xml index de5386e8..00b8181a 100644 --- a/play-services-core/src/main/res/values-night-v31/themes.xml +++ b/play-services-core/src/main/res/values-night-v31/themes.xml @@ -1,3 +1,4 @@ +<<<<<<< HEAD +======= + + + + diff --git a/play-services-core/src/main/res/values-v31/themes.xml b/play-services-core/src/main/res/values-v31/themes.xml index 009a86db..5aa4f6ac 100644 --- a/play-services-core/src/main/res/values-v31/themes.xml +++ b/play-services-core/src/main/res/values-v31/themes.xml @@ -1,4 +1,5 @@ +<<<<<<< HEAD <<<<<<< HEAD +======= + + + + + + diff --git a/play-services-core/src/main/res/values/themes.xml b/play-services-core/src/main/res/values/themes.xml index c8ce1496..56bb40bc 100755 --- a/play-services-core/src/main/res/values/themes.xml +++ b/play-services-core/src/main/res/values/themes.xml @@ -39,21 +39,12 @@ - - + \ No newline at end of file diff --git a/play-services-location/src/main/aidl/com/google/android/gms/location/internal/IGoogleLocationManagerService.aidl b/play-services-location/src/main/aidl/com/google/android/gms/location/internal/IGoogleLocationManagerService.aidl new file mode 100644 index 00000000..d848199f --- /dev/null +++ b/play-services-location/src/main/aidl/com/google/android/gms/location/internal/IGoogleLocationManagerService.aidl @@ -0,0 +1,138 @@ +package com.google.android.gms.location.internal; + +import android.app.PendingIntent; +import android.location.Location; +import android.os.Bundle; + +import com.google.android.gms.common.api.Status; +import com.google.android.gms.common.api.internal.IStatusCallback; +import com.google.android.gms.common.internal.ICancelToken; +import com.google.android.gms.location.internal.DeviceOrientationRequestUpdateData; +import com.google.android.gms.location.internal.IFusedLocationProviderCallback; +import com.google.android.gms.location.internal.IGeofencerCallbacks; +import com.google.android.gms.location.internal.ILocationStatusCallback; +import com.google.android.gms.location.internal.ISettingsCallbacks; +import com.google.android.gms.location.internal.LocationReceiver; +import com.google.android.gms.location.internal.LocationRequestInternal; +import com.google.android.gms.location.internal.LocationRequestUpdateData; +import com.google.android.gms.location.internal.ParcelableGeofence; +import com.google.android.gms.location.ActivityRecognitionResult; +import com.google.android.gms.location.ActivityRecognitionRequest; +import com.google.android.gms.location.ActivityTransitionRequest; +import com.google.android.gms.location.CurrentLocationRequest; +import com.google.android.gms.location.GeofencingRequest; +import com.google.android.gms.location.GestureRequest; +import com.google.android.gms.location.ILocationListener; +import com.google.android.gms.location.LastLocationRequest; +import com.google.android.gms.location.LocationAvailability; +import com.google.android.gms.location.LocationAvailabilityRequest; +import com.google.android.gms.location.LocationRequest; +import com.google.android.gms.location.LocationSettingsRequest; +import com.google.android.gms.location.LocationStatus; +import com.google.android.gms.location.SleepSegmentRequest; + +interface IGoogleLocationManagerService { + + // current + + void addGeofences(in GeofencingRequest geofencingRequest, in PendingIntent pendingIntent, IGeofencerCallbacks callbacks) = 56; + void removeGeofencesByIntent(in PendingIntent pendingIntent, IGeofencerCallbacks callbacks, String packageName) = 1; + void removeGeofencesById(in String[] geofenceRequestIds, IGeofencerCallbacks callbacks, String packageName) = 2; + + void removeActivityTransitionUpdates(in PendingIntent pendingIntent, IStatusCallback callback) = 72; + void removeActivityUpdates(in PendingIntent callbackIntent) = 5; + void requestActivityTransitionUpdates(in ActivityTransitionRequest request, in PendingIntent pendingIntent, IStatusCallback callback) = 71; + void requestActivityUpdates(long detectionIntervalMillis, boolean triggerUpdates, in PendingIntent callbackIntent) = 4; + void requestActivityUpdatesWithCallback(in ActivityRecognitionRequest request, in PendingIntent pendingIntent, IStatusCallback callback) = 69; + + Location getLastLocation() = 6; + void getLastLocationWithRequest(in LastLocationRequest request, ILocationStatusCallback callback) = 81; + void getLastLocationWithReceiver(in LastLocationRequest request, in LocationReceiver receiver) = 89; + + ICancelToken getCurrentLocation(in CurrentLocationRequest request, ILocationStatusCallback callback) = 86; + ICancelToken getCurrentLocationWithReceiver(in CurrentLocationRequest request, in LocationReceiver receiver) = 91; + + void requestLocationUpdatesWithCallback(in LocationReceiver receiver, in LocationRequest request, IStatusCallback callback) = 87; + void removeLocationUpdatesWithCallback(in LocationReceiver receiver, IStatusCallback callback) = 88; + void updateLocationRequest(in LocationRequestUpdateData locationRequestUpdateData) = 58; + + void flushLocations(IFusedLocationProviderCallback callback) = 66; + + LocationAvailability getLocationAvailabilityWithPackage(String packageName) = 33; + void getLocationAvailabilityWithReceiver(in LocationAvailabilityRequest request, in LocationReceiver receiver) = 90; + + void setMockMode(boolean mockMode) = 11; + void setMockLocation(in Location mockLocation) = 12; + void setMockModeWithCallback(boolean mockMode, IStatusCallback callback) = 83; + void setMockLocationWithCallback(in Location mockLocation, IStatusCallback callback) = 84; + + void removeSleepSegmentUpdates(in PendingIntent pendingIntent, IStatusCallback callback) = 68; + void requestSleepSegmentUpdates(in PendingIntent pendingIntent, in SleepSegmentRequest request, IStatusCallback callback) = 78; + + void requestLocationSettingsDialog(in LocationSettingsRequest settingsRequest, ISettingsCallbacks callback, String packageName) = 62; + + void updateDeviceOrientationRequest(in DeviceOrientationRequestUpdateData request) = 74; + + // deprecated + + void addGeofencesList(in List geofences, in PendingIntent pendingIntent, IGeofencerCallbacks callbacks, String packageName) = 0; + void removeAllGeofences(IGeofencerCallbacks callbacks, String packageName) = 3; + + ActivityRecognitionResult getLastActivity(String packageName) = 63; + + Location getLastLocationWithPackage(String packageName) = 20; + Location getLastLocationWith(String s) = 79; + + void requestLocationUpdatesWithListener(in LocationRequest request, ILocationListener listener) = 7; + void requestLocationUpdatesWithPackage(in LocationRequest request, ILocationListener listener, String packageName) = 19; + void requestLocationUpdatesWithIntent(in LocationRequest request, in PendingIntent callbackIntent) = 8; + void requestLocationUpdatesInternalWithListener(in LocationRequestInternal request, ILocationListener listener) = 51; + void requestLocationUpdatesInternalWithIntent(in LocationRequestInternal request, in PendingIntent callbackIntent) = 52; + void removeLocationUpdatesWithListener(ILocationListener listener) = 9; + void removeLocationUpdatesWithIntent(in PendingIntent callbackIntent) = 10; + + // unsupported + +// void iglms14(in LatLngBounds var1, int var2, in PlaceFilter var3, in PlacesParams var4, IPlacesCallbacks var5) = 13; +// void iglms15(String var1, in PlacesParams var2, IPlacesCallbacks var3) = 14; +// void iglms16(in LatLng var1, in PlaceFilter var2, in PlacesParams var3, IPlacesCallbacks var4) = 15; +// void iglms17(in PlaceFilter var1, in PlacesParams var2, IPlacesCallbacks var3) = 16; +// void iglms18(in PlaceRequest var1, in PlacesParams var2, in PendingIntent var3) = 17; +// void iglms19(in PlacesParams var1, in PendingIntent var2) = 18; + +// void iglms25(in PlaceReport var1, in PlacesParams var2) = 24; +// void injectLocation(in Location mockLocation, int injectionType) = 25; + +// void iglms42(String var1, in PlacesParams var2, IPlacesCallbacks var3) = 41; + +// void iglms46(in UserAddedPlace var1, in PlacesParams var2, IPlacesCallbacks var3) = 45; +// void iglms47(in LatLngBounds var1, int var2, String var3, in PlaceFilter var4, in PlacesParams var5, IPlacesCallbacks var6) = 46; +// void iglms48(in NearbyAlertRequest var1, in PlacesParams var2, in PendingIntent var3) = 47; +// void iglms49(in PlacesParams var1, in PendingIntent var2) = 48; +// void iglms50(in UserDataType var1, in LatLngBounds var2, in List var3, in PlacesParams var4, IPlacesCallbacks var5) = 49; +// IBinder iglms51() = 50; + +// IBinder iglms54() = 53; +// void iglms55(String var1, in LatLngBounds var2, in AutocompleteFilter var3, in PlacesParams var4, IPlacesCallbacks var5) = 54; + +// void iglms58(in List var1, in PlacesParams var2, IPlacesCallbacks var3) = 57; + +// Status requestGestureUpdates(in GestureRequest request, in PendingIntent pendingIntent) = 59; +// Status iglms61(in PendingIntent pendingIntent) = 60; + +// void iglms65(in PendingIntent pendingIntent, IStatusCallback callback) = 64; +// void iglms66(in PendingIntent pendingIntent, IStatusCallback callback) = 65; + +// void iglms68(in PendingIntent pendingIntent, IStatusCallback callback) = 67; + +// void iglms71(IStatusCallback callback) = 70; + +// void removeGeofences(in RemoveGeofencingRequest request, IGeofencerCallbacks callback) = 73; +// void iglms76(in PendingIntent pendingIntent) = 75; +// boolean setActivityRecognitionMode(int mode) = 76; +// int getActivityRecognitionMode() = 77; + +// void injectLocatinWithCallback(in Location mockLocation, int injectionType, IStatusCallback callback) = 85; + +// void isGoogleLocationAccuracyEnabled(in IBooleanStatusCallback callback) = 94; +} diff --git a/play-services-location/src/main/java/com/google/android/gms/location/LocationSettingsRequest.java b/play-services-location/src/main/java/com/google/android/gms/location/LocationSettingsRequest.java new file mode 100644 index 00000000..786d6cb4 --- /dev/null +++ b/play-services-location/src/main/java/com/google/android/gms/location/LocationSettingsRequest.java @@ -0,0 +1,135 @@ +/* + * SPDX-FileCopyrightText: 2015 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. + */ + +package com.google.android.gms.location; + +import android.os.Parcel; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; +import org.microg.gms.common.Hide; +import org.microg.gms.common.PublicApi; +import org.microg.gms.utils.ToStringHelper; +import org.microg.safeparcel.AutoSafeParcelable; +import org.microg.safeparcel.SafeParceled; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * Specifies the types of location services the client is interested in using. Settings will be checked for optimal functionality + * of all requested services. Use {@link LocationSettingsRequest.Builder} to construct this object. + */ +@PublicApi +@SafeParcelable.Class +public class LocationSettingsRequest extends AbstractSafeParcelable { + @Field(1000) + private int versionCode = 2; + + @Field(value = 1, subClass = LocationRequest.class) + @Hide + public final List requests; + + @Field(2) + @Hide + public final boolean alwaysShow; + + @Field(3) + @Hide + public final boolean needBle; + + @Field(5) + @Hide + @Nullable + public final LocationSettingsConfiguration configuration; + + @Constructor + LocationSettingsRequest(@Param(1) List requests, @Param(2) boolean alwaysShow, @Param(3) boolean needBle, @Param(5) @Nullable LocationSettingsConfiguration configuration) { + this.requests = requests; + this.alwaysShow = alwaysShow; + this.needBle = needBle; + this.configuration = configuration; + } + + /** + * A builder that builds {@link LocationSettingsRequest}. + */ + public static class Builder { + private List requests = new ArrayList(); + private boolean alwaysShow = false; + private boolean needBle = false; + + /** + * Adds a collection of {@link LocationRequest}s that the client is interested in. Settings + * will be checked for optimal performance of all {@link LocationRequest}s. + */ + public Builder addAllLocationRequests(Collection requests) { + this.requests.addAll(requests); + return this; + } + + /** + * Adds one {@link LocationRequest} that the client is interested in. Settings will be + * checked for optimal performance of all {@link LocationRequest}s. + */ + public Builder addLocationRequest(LocationRequest request) { + requests.add(request); + return this; + } + + /** + * Creates a LocationSettingsRequest that can be used with SettingsApi. + */ + public LocationSettingsRequest build() { + return new LocationSettingsRequest(requests, alwaysShow, needBle, null); + } + + /** + * Whether or not location is required by the calling app in order to continue. Set this to + * true if location is required to continue and false if having location provides better + * results, but is not required. This changes the wording/appearance of the dialog + * accordingly. + */ + public Builder setAlwaysShow(boolean show) { + alwaysShow = show; + return this; + } + + /** + * Sets whether the client wants BLE scan to be enabled. When this flag is set to true, if + * the platform supports BLE scan mode and Bluetooth is off, the dialog will prompt the + * user to enable BLE scan. If the platform doesn't support BLE scan mode, the dialog will + * prompt to enable Bluetooth. + */ + public Builder setNeedBle(boolean needBle) { + this.needBle = needBle; + return this; + } + } + + @Hide + @NonNull + @Override + public String toString() { + return ToStringHelper.name("LocationSettingsRequest") + .value(requests) + .field("alwaysShow", alwaysShow) + .field("needBle", needBle) + .end(); + } + + public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(LocationSettingsRequest.class); + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } +} diff --git a/play-services-location/src/main/java/com/google/android/gms/location/LocationSettingsResponse.java b/play-services-location/src/main/java/com/google/android/gms/location/LocationSettingsResponse.java new file mode 100644 index 00000000..dbe22a08 --- /dev/null +++ b/play-services-location/src/main/java/com/google/android/gms/location/LocationSettingsResponse.java @@ -0,0 +1,41 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. + */ + +package com.google.android.gms.location; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.ResolvableApiException; +import com.google.android.gms.common.api.Response; +import com.google.android.gms.tasks.Task; +import org.microg.gms.common.Hide; +import org.microg.gms.common.PublicApi; + +/** + * Successful response of checking settings via {@link SettingsApi#checkLocationSettings(GoogleApiClient, LocationSettingsRequest)}. + *

+ * If a {@link Task} with this response type fails, it will receive a {@link ResolvableApiException} which may be able to resolve the failure. + * See {@link SettingsClient} for more details. + *

+ * The current location settings states can be accessed via {@link #getLocationSettingsStates()}. See {@link LocationSettingsStates} for more details. + */ +public class LocationSettingsResponse extends Response { + /** + * Retrieves the location settings states. + */ + @Nullable + public LocationSettingsStates getLocationSettingsStates() { + return getResult().getLocationSettingsStates(); + } + + @Hide + public LocationSettingsResponse(@NonNull LocationSettingsResult result) { + super(result); + } +} diff --git a/play-services-location/src/main/java/com/google/android/gms/location/LocationSettingsResult.java b/play-services-location/src/main/java/com/google/android/gms/location/LocationSettingsResult.java new file mode 100644 index 00000000..62e00c26 --- /dev/null +++ b/play-services-location/src/main/java/com/google/android/gms/location/LocationSettingsResult.java @@ -0,0 +1,86 @@ +/* + * SPDX-FileCopyrightText: 2015 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. + */ + +package com.google.android.gms.location; + +import android.app.Activity; +import android.os.Parcel; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.Result; +import com.google.android.gms.common.api.Status; + +import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; +import org.microg.gms.common.Hide; +import org.microg.gms.common.PublicApi; + +/** + * Result of checking settings via {@link SettingsApi#checkLocationSettings(GoogleApiClient, LocationSettingsRequest)}, + * indicates whether a dialog should be shown to ask the user's consent to change their + * settings. + *

+ * The method {@link #getStatus()} can be used to confirm if the request was successful. If the current location settings don't + * satisfy the app's requirements and the user has permission to change the settings, the app could use + * {@link Status#startResolutionForResult(Activity, int)} to start an intent to show a dialog, asking for user's consent to + * change the settings. + *

+ * The current location settings states can be accessed via {@link #getLocationSettingsStates()}. See + * {@link LocationSettingsStates} for more details. + */ +@PublicApi +@SafeParcelable.Class +public class LocationSettingsResult extends AbstractSafeParcelable implements Result { + + @Field(1000) + int versionCode = 1; + + @Field(1) + @NonNull + private final Status status; + + @Field(2) + @Nullable + private final LocationSettingsStates settings; + + + /** + * Retrieves the location settings states. + */ + @Nullable + public LocationSettingsStates getLocationSettingsStates() { + return settings; + } + + @Override + @NonNull + public Status getStatus() { + return status; + } + + @Hide + @Constructor + public LocationSettingsResult(@Param(1) @NonNull Status status, @Param(2) @Nullable LocationSettingsStates settings) { + this.settings = settings; + this.status = status; + } + + @Hide + public LocationSettingsResult(@NonNull Status status) { + this(status, null); + } + + public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(LocationSettingsResult.class); + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } +} diff --git a/play-services-location/src/main/java/com/google/android/gms/location/LocationSettingsStates.java b/play-services-location/src/main/java/com/google/android/gms/location/LocationSettingsStates.java new file mode 100644 index 00000000..e2f6d02b --- /dev/null +++ b/play-services-location/src/main/java/com/google/android/gms/location/LocationSettingsStates.java @@ -0,0 +1,138 @@ +/* + * SPDX-FileCopyrightText: 2015 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. + */ + +package com.google.android.gms.location; + +import android.app.Activity; +import android.content.Intent; +import android.os.Parcel; +import androidx.annotation.NonNull; +import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableSerializer; +import org.microg.gms.common.PublicApi; + +/** + * Stores the current states of all location-related settings. + */ +@PublicApi +@SafeParcelable.Class +public class LocationSettingsStates extends AbstractSafeParcelable { + + @Field(1000) + int versionCode = 2; + + @Field(1) + private final boolean gpsUsable; + + @Field(2) + private final boolean networkLocationUsable; + + @Field(3) + private final boolean bleUsable; + + @Field(4) + private final boolean gpsPresent; + + @Field(5) + private final boolean networkLocationPresent; + + @Field(6) + private final boolean blePresent; + + /** + * Whether BLE is present on the device. + */ + public boolean isBlePresent() { + return blePresent; + } + + /** + * Whether BLE is enabled and is usable by the app. + */ + public boolean isBleUsable() { + return bleUsable; + } + + /** + * Whether GPS provider is present on the device. + */ + public boolean isGpsPresent() { + return gpsPresent; + } + + /** + * Whether GPS provider is enabled and is usable by the app. + */ + public boolean isGpsUsable() { + return gpsUsable; + } + + /** + * Whether location is present on the device. + *

+ * This method returns true when either GPS or network location provider is present. + */ + public boolean isLocationPresent() { + return isGpsPresent() || isNetworkLocationPresent(); + } + + /** + * Whether location is enabled and is usable by the app. + *

+ * This method returns true when either GPS or network location provider is usable. + */ + public boolean isLocationUsable() { + return isGpsUsable() || isNetworkLocationUsable(); + } + + /** + * Whether network location provider is present on the device. + */ + public boolean isNetworkLocationPresent() { + return networkLocationPresent; + } + + /** + * Whether network location provider is enabled and usable by the app. + */ + public boolean isNetworkLocationUsable() { + return networkLocationUsable; + } + + @Constructor + public LocationSettingsStates(@Param(1) boolean gpsUsable, @Param(2) boolean networkLocationUsable, @Param(3) boolean bleUsable, @Param(4) boolean gpsPresent, @Param(5) boolean networkLocationPresent, @Param(6) boolean blePresent) { + this.gpsUsable = gpsUsable; + this.networkLocationUsable = networkLocationUsable; + this.bleUsable = bleUsable; + this.gpsPresent = gpsPresent; + this.networkLocationPresent = networkLocationPresent; + this.blePresent = blePresent; + } + + /** + * Retrieves the location settings states from the intent extras. When the location settings dialog finishes, you can use this + * method to retrieve the current location settings states from the intent in your + * {@link Activity#onActivityResult(int, int, Intent)}. + */ + public static LocationSettingsStates fromIntent(Intent intent) { + byte[] bytes = intent.getByteArrayExtra(EXTRA_NAME); + if (bytes == null) return null; + return SafeParcelableSerializer.deserializeFromBytes(bytes, CREATOR); + } + + public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(LocationSettingsStates.class); + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } + + private static final String EXTRA_NAME = "com.google.android.gms.location.LOCATION_SETTINGS_STATES"; +} diff --git a/play-services-location/src/main/java/com/google/android/gms/location/SettingsClient.java b/play-services-location/src/main/java/com/google/android/gms/location/SettingsClient.java new file mode 100644 index 00000000..4c485d72 --- /dev/null +++ b/play-services-location/src/main/java/com/google/android/gms/location/SettingsClient.java @@ -0,0 +1,44 @@ +/* + * SPDX-FileCopyrightText: 2022 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. + */ + +package com.google.android.gms.location; + +import android.location.LocationManager; +import android.provider.Settings; +import com.google.android.gms.common.api.Api; +import com.google.android.gms.common.api.HasApiKey; +import com.google.android.gms.tasks.Task; + +/** + * The main entry point for interacting with the location settings-enabler APIs. + *

+ * This API makes it easy for an app to ensure that the device's system settings are properly configured for the app's + * location needs. + */ +public interface SettingsClient extends HasApiKey { + /** + * Checks if the relevant system settings are enabled on the device to carry out the desired location requests. + * + * @param locationSettingsRequest an object that contains all the location requirements that the client is interested in. + */ + Task checkLocationSettings(LocationSettingsRequest locationSettingsRequest); + + /** + * Returns true if the Google Location Accuracy setting is currently enabled. This setting is required for Fused Location + * Provider APIs to be able to generate network (wifi, cell, etc) based locations. If Google Play services is chosen as the + * platform {@link LocationManager#NETWORK_PROVIDER} (this is the case on all GMS compliant devices, which constitute the + * vast majority of the Android ecosystem), then this setting is also required for the platform + * {@link LocationManager#NETWORK_PROVIDER} to be enabled. + *

+ * On Android P and above devices, the Google Location Accuracy setting may be found under location settings. Below + * Android P, Google Location Accuracy is tied to the device location mode - it will be enabled if the device is in + * {@link Settings.Secure#LOCATION_MODE_BATTERY_SAVING} or {@link Settings.Secure#LOCATION_MODE_HIGH_ACCURACY}, and + * disabled in {@link Settings.Secure#LOCATION_MODE_SENSORS_ONLY}. + */ + Task isGoogleLocationAccuracyEnabled(); +} diff --git a/play-services-location/src/main/java/org/microg/gms/location/SettingsClientImpl.java b/play-services-location/src/main/java/org/microg/gms/location/SettingsClientImpl.java new file mode 100644 index 00000000..f8d89125 --- /dev/null +++ b/play-services-location/src/main/java/org/microg/gms/location/SettingsClientImpl.java @@ -0,0 +1,39 @@ +/* + * SPDX-FileCopyrightText: 2022 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location; + +import android.content.Context; + +import android.os.RemoteException; +import com.google.android.gms.common.api.Api; +import com.google.android.gms.common.api.GoogleApi; +import com.google.android.gms.location.*; +import com.google.android.gms.location.internal.ISettingsCallbacks; +import com.google.android.gms.tasks.Task; +import com.google.android.gms.tasks.TaskCompletionSource; +import com.google.android.gms.tasks.Tasks; +import org.microg.gms.common.api.PendingGoogleApiCall; + +public class SettingsClientImpl extends GoogleApi implements SettingsClient { + public SettingsClientImpl(Context context) { + super(context, LocationServices.API); + } + + @Override + public Task checkLocationSettings(LocationSettingsRequest locationSettingsRequest) { + return scheduleTask((PendingGoogleApiCall) (client, completionSource) -> client.getServiceInterface().requestLocationSettingsDialog(locationSettingsRequest, new ISettingsCallbacks.Stub() { + @Override + public void onLocationSettingsResult(LocationSettingsResult result) { + completionSource.setResult(new LocationSettingsResponse(result)); + } + }, null)); + } + + @Override + public Task isGoogleLocationAccuracyEnabled() { + return Tasks.forResult(true); // TODO + } +} From b2193b74520a76b539c9afcc292f5065bdfd0df5 Mon Sep 17 00:00:00 2001 From: DaVinci9196 <150454414+DaVinci9196@users.noreply.github.com> Date: Thu, 21 Mar 2024 05:56:07 +0800 Subject: [PATCH 116/209] Phenotype: Fixed crashing when sharing photos (#2155) --- .../main/kotlin/org/microg/gms/phenotype/PhenotypeService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/play-services-core/src/main/kotlin/org/microg/gms/phenotype/PhenotypeService.kt b/play-services-core/src/main/kotlin/org/microg/gms/phenotype/PhenotypeService.kt index fff8154e..1720f62a 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/phenotype/PhenotypeService.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/phenotype/PhenotypeService.kt @@ -31,7 +31,7 @@ class PhenotypeService : BaseService(TAG, GmsService.PHENOTYPE) { class PhenotypeServiceImpl(val packageName: String?) : IPhenotypeService.Stub() { override fun register(callbacks: IPhenotypeCallbacks, packageName: String?, version: Int, p3: Array?, p4: ByteArray?) { Log.d(TAG, "register($packageName, $version, $p3, $p4)") - callbacks.onRegistered(Status.SUCCESS) + callbacks.onRegistered(if (version != 0) Status.SUCCESS else Status.CANCELED) } override fun weakRegister(callbacks: IPhenotypeCallbacks, packageName: String?, version: Int, p3: Array?, p4: IntArray?, p5: ByteArray?) { From fbd56e1d04cb4ea71ea5acf0151e2f5414519cb7 Mon Sep 17 00:00:00 2001 From: ale5000 <15793015+ale5000-git@users.noreply.github.com> Date: Thu, 21 Mar 2024 01:29:00 +0100 Subject: [PATCH 117/209] Bump GMS version to 24.09.13 (#2191) --- build.gradle | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/build.gradle b/build.gradle index 55039930..b0173646 100644 --- a/build.gradle +++ b/build.gradle @@ -76,7 +76,11 @@ buildscript { } +<<<<<<< HEAD def gmsVersion = "24.10.17" +======= +def gmsVersion = "24.09.13" +>>>>>>> ef113281 (Bump GMS version to 24.09.13 (#2191)) def gmsVersionCode = Integer.parseInt(gmsVersion.replaceAll('\\.', '')) def ourVersionBase = "0.3.0" From ceed0a170dd7f84fad1e1261c44c3bae929a5474 Mon Sep 17 00:00:00 2001 From: DaVinci9196 <150454414+DaVinci9196@users.noreply.github.com> Date: Thu, 21 Mar 2024 09:34:43 +0800 Subject: [PATCH 118/209] Translation permission related (Chinese (Simplified)) (#2212) --- .../src/huawei/res/values-zh-rCN/strings.xml | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 play-services-location/core/src/huawei/res/values-zh-rCN/strings.xml diff --git a/play-services-location/core/src/huawei/res/values-zh-rCN/strings.xml b/play-services-location/core/src/huawei/res/values-zh-rCN/strings.xml new file mode 100644 index 00000000..29e2fa37 --- /dev/null +++ b/play-services-location/core/src/huawei/res/values-zh-rCN/strings.xml @@ -0,0 +1,23 @@ + + + %s 需要位置权限 + 请点击并选择 \"%s\" 去允许 %s 访问此设备的位置。 + 始终允许 + + 建议给microG服务授予"始终允许"权限,缺少此权限可能将导致部分应用在后台运行时无法获取当前位置的问题,如Microsoft Teams的位置分享功能可能会受影响。 + 提示信息: + 只需两步: + 打开设置 + 不再提醒 + + + 请允许 %1$s 访问此设备的位置。 + \n需要这些权限才能在支持应用程序的地图功能中启用位置。 + + + 请选择“始终允许”以允许 %1$s 访问此设备的位置。 + + + 请点击“权限管理器”并选择“始终允许”。 + + \ No newline at end of file From f606eab1e32e0fa3f6d63fbdb4f056cd0ad98847 Mon Sep 17 00:00:00 2001 From: Aayush Gupta Date: Tue, 5 Dec 2023 14:39:14 +0530 Subject: [PATCH 119/209] Introduce a new AccountsFragment to store accounts related prefs Add a new fragment to host preferences that concern accounts or data related to them. * Show currently logged in accounts as a simple list on top * Relocate add account preference below. * Add a new preference to quickly open account settings page to manage all accounts. Issue: calyxos#712 Signed-off-by: Aayush Gupta Change-Id: Ifd00fe1295cc42077e835dd9afa829c444a5af97 --- .../org/microg/gms/ui/AccountsFragment.kt | 53 +++++++++++++++++++ .../org/microg/gms/ui/SettingsFragment.kt | 13 +++++ .../src/main/res/navigation/nav_settings.xml | 19 +++++++ .../src/main/res/values/strings.xml | 10 ++++ .../src/main/res/xml/preferences_accounts.xml | 27 ++++++++++ .../src/main/res/xml/preferences_start.xml | 13 +++++ 6 files changed, 135 insertions(+) create mode 100644 play-services-core/src/main/java/org/microg/gms/ui/AccountsFragment.kt create mode 100644 play-services-core/src/main/res/xml/preferences_accounts.xml diff --git a/play-services-core/src/main/java/org/microg/gms/ui/AccountsFragment.kt b/play-services-core/src/main/java/org/microg/gms/ui/AccountsFragment.kt new file mode 100644 index 00000000..b9dc737c --- /dev/null +++ b/play-services-core/src/main/java/org/microg/gms/ui/AccountsFragment.kt @@ -0,0 +1,53 @@ +package org.microg.gms.ui + +import android.accounts.AccountManager +import android.content.ActivityNotFoundException +import android.content.Intent +import android.os.Bundle +import android.provider.Settings +import android.util.Log +import android.view.View +import androidx.core.content.ContextCompat +import androidx.preference.Preference +import androidx.preference.PreferenceCategory +import androidx.preference.PreferenceFragmentCompat +import com.google.android.gms.R +import org.microg.gms.auth.AuthConstants + +class AccountsFragment : PreferenceFragmentCompat() { + + private val TAG = AccountsFragment::class.java.simpleName + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + addPreferencesFromResource(R.xml.preferences_accounts) + + val accountManager = AccountManager.get(requireContext()) + val accounts = accountManager.getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE) + + findPreference("pref_current_accounts_none")?.isVisible = accounts.isEmpty() + val preferenceCategory = findPreference("prefcat_current_accounts") + accounts.forEach { + Preference(requireContext()).apply { + title = it.name + icon = ContextCompat.getDrawable( + requireContext(), + R.drawable.proprietary_auth_gls_ic_google_minitab_selected + ) + preferenceCategory?.addPreference(this) + } + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + findPreference("pref_manage_accounts")?.setOnPreferenceClickListener { + try { + startActivity(Intent(Settings.ACTION_SYNC_SETTINGS)) + } catch (activityNotFoundException: ActivityNotFoundException) { + Log.e(TAG, "Failed to launch sync settings", activityNotFoundException) + } + true + } + } +} diff --git a/play-services-core/src/main/kotlin/org/microg/gms/ui/SettingsFragment.kt b/play-services-core/src/main/kotlin/org/microg/gms/ui/SettingsFragment.kt index 8d503a68..724b5b42 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/ui/SettingsFragment.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/ui/SettingsFragment.kt @@ -27,7 +27,15 @@ class SettingsFragment : ResourceSettingsFragment() { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { super.onCreatePreferences(savedInstanceState, rootKey) +<<<<<<< HEAD findPreference(PREF_CHECKIN)?.setOnPreferenceClickListener { +======= + findPreference(PREF_ACCOUNTS)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { + findNavController().navigate(requireContext(), R.id.accountManagerFragment) + true + } + findPreference(PREF_CHECKIN)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { +>>>>>>> 8c9aea55 (Introduce a new AccountsFragment to store accounts related prefs) findNavController().navigate(requireContext(), R.id.openCheckinSettings) true } @@ -76,6 +84,11 @@ class SettingsFragment : ResourceSettingsFragment() { const val PREF_ABOUT = "pref_about" const val PREF_GCM = "pref_gcm" const val PREF_CHECKIN = "pref_checkin" +<<<<<<< HEAD +======= + const val PREF_VENDING = "pref_vending" + const val PREF_ACCOUNTS = "pref_accounts" +>>>>>>> 8c9aea55 (Introduce a new AccountsFragment to store accounts related prefs) } init { diff --git a/play-services-core/src/main/res/navigation/nav_settings.xml b/play-services-core/src/main/res/navigation/nav_settings.xml index fb1cfa00..0349a61e 100755 --- a/play-services-core/src/main/res/navigation/nav_settings.xml +++ b/play-services-core/src/main/res/navigation/nav_settings.xml @@ -13,6 +13,25 @@ +<<<<<<< HEAD +======= + + + + + + + + + + +>>>>>>> 8c9aea55 (Introduce a new AccountsFragment to store accounts related prefs) More >>>>>>> 9028d03a (Auth: Allow stripping device name from requests) + Accounts + Add and manage Google accounts + Settings + Currently signed-in accounts + Manage accounts + Manage existing accounts settings Account Add Google account +<<<<<<< HEAD Receive push notifications +======= + +>>>>>>> 8c9aea55 (Introduce a new AccountsFragment to store accounts related prefs) Cloud Messaging is a push notification provider used by many third-party applications. To use it you must enable device registration. About Vanced+ microG diff --git a/play-services-core/src/main/res/xml/preferences_accounts.xml b/play-services-core/src/main/res/xml/preferences_accounts.xml new file mode 100644 index 00000000..e22aa90a --- /dev/null +++ b/play-services-core/src/main/res/xml/preferences_accounts.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + diff --git a/play-services-core/src/main/res/xml/preferences_start.xml b/play-services-core/src/main/res/xml/preferences_start.xml index 7893924b..53a9e7db 100755 --- a/play-services-core/src/main/res/xml/preferences_start.xml +++ b/play-services-core/src/main/res/xml/preferences_start.xml @@ -22,9 +22,22 @@ android:title="@string/account_manager_title" android:summary="@string/manage_google_account_summary"> +======= + android:targetClass="org.microg.gms.ui.SelfCheckFragment$AsActivity" + android:targetPackage="com.google.android.gms" /> + + + + +>>>>>>> 8c9aea55 (Introduce a new AccountsFragment to store accounts related prefs) Date: Wed, 20 Mar 2024 22:32:22 +0100 Subject: [PATCH 120/209] Fake signature: Add additional packages --- .../diff/AppListDatabaseOpenHelper.java | 75 +++++++++++++++++++ fake-signature/src/main/res/values/arrays.xml | 4 + 2 files changed, 79 insertions(+) create mode 100644 fake-signature/src/huawei/java/com/huawei/signature/diff/AppListDatabaseOpenHelper.java diff --git a/fake-signature/src/huawei/java/com/huawei/signature/diff/AppListDatabaseOpenHelper.java b/fake-signature/src/huawei/java/com/huawei/signature/diff/AppListDatabaseOpenHelper.java new file mode 100644 index 00000000..f7e2d26e --- /dev/null +++ b/fake-signature/src/huawei/java/com/huawei/signature/diff/AppListDatabaseOpenHelper.java @@ -0,0 +1,75 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.huawei.signature.diff; + +import android.content.ContentValues; +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.util.Log; +import org.microg.signature.fake.R; + +public class AppListDatabaseOpenHelper extends SQLiteOpenHelper { + private static final String TAG = AppListDatabaseOpenHelper.class.getSimpleName(); + private static final String DATABASE_NAME = "app_list.db"; + public static final String TABLE_APPLIST = "applist"; + public static final String COLUMN_NAME = "name"; + public static final String COLUMN_FAKE = "fake"; + private static final int DATABASE_VERSION = 3; + private static final String DROP_APP_LIST_TABLE = "DROP TABLE IF EXISTS " + TABLE_APPLIST; + private static final String CREATE_APP_LIST_TABLE = "CREATE TABLE IF NOT EXISTS " + TABLE_APPLIST + "(" + + COLUMN_NAME + " VARCHAR(255) PRIMARY KEY, " + + COLUMN_FAKE + " INTEGER CHECK(" + COLUMN_FAKE + " >= 0 and " + COLUMN_FAKE + " <= 1)" + + ")"; + private final Context context; + + public AppListDatabaseOpenHelper(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + this.context = context; + } + + @Override + public void onCreate(SQLiteDatabase db) { + Log.d(TAG, "onCreate"); + db.execSQL(DROP_APP_LIST_TABLE); + db.execSQL(CREATE_APP_LIST_TABLE); + initData(db); + } + + @Override + public void onOpen(SQLiteDatabase db) { + super.onOpen(db); + if (!db.isReadOnly()) { + initData(db); + } + } + + private void initData(SQLiteDatabase db) { + String[] wantFakeApps = context.getResources().getStringArray(R.array.signature_want_fake); + String[] neverFakeApps = context.getResources().getStringArray(R.array.signature_never_fake); + if (wantFakeApps.length == 0 && neverFakeApps.length == 0) { + return; + } + for (String app : wantFakeApps) { + db.insertWithOnConflict(TABLE_APPLIST, null, generateValues(app, true), SQLiteDatabase.CONFLICT_REPLACE); + } + for (String app : neverFakeApps) { + db.insertWithOnConflict(TABLE_APPLIST, null, generateValues(app, false), SQLiteDatabase.CONFLICT_REPLACE); + } + } + + private ContentValues generateValues(String packageName, boolean fake) { + ContentValues contentValues = new ContentValues(); + contentValues.put(COLUMN_NAME, packageName); + contentValues.put(COLUMN_FAKE, fake ? 1 : 0); + return contentValues; + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + onCreate(db); + } +} \ No newline at end of file diff --git a/fake-signature/src/main/res/values/arrays.xml b/fake-signature/src/main/res/values/arrays.xml index 86a670a0..4f3118db 100644 --- a/fake-signature/src/main/res/values/arrays.xml +++ b/fake-signature/src/main/res/values/arrays.xml @@ -28,6 +28,10 @@ com.mobisoft.morhipo com.zzkko com.didilabs.kaavefali + com.airbnb.android + com.mobillium.papara + co.com.fincaraiz.app + com.frisby.frisby com.truecaller From 00bb5826147dc58eb12ef6e1f1811ce2dd18acca Mon Sep 17 00:00:00 2001 From: Marvin W Date: Wed, 20 Mar 2024 15:11:57 +0100 Subject: [PATCH 121/209] Use PendingIntentCompat for mutable flag --- .../microg/gms/auth/phone/SmsRetrieverCore.kt | 3 +- play-services-base/core/build.gradle | 61 ++++ .../microg/gms/utils/IntentCacheManager.kt | 149 ++++++++++ .../microg/gms/games/GamesConnectService.kt | 5 +- .../org/microg/gms/games/GamesService.kt | 6 +- .../core/privileged/Fido2PrivilegedService.kt | 7 +- .../core/transport/nfc/NfcTransportHandler.kt | 113 ++++++++ .../usb/UsbDevicePermissionManager.kt | 84 ++++++ .../core/provider/build.gradle | 57 ++++ .../NetworkLocationProviderPreTiramisu.kt | 124 ++++++++ .../exposurenotification/AdvertiserService.kt | 268 ++++++++++++++++++ .../exposurenotification/CleanupService.kt | 64 +++++ .../ExposureNotificationServiceImpl.kt | 3 +- .../exposurenotification/NotifyService.kt | 140 +++++++++ .../exposurenotification/ScannerService.kt | 197 +++++++++++++ .../com/google/android/gms/tasks/Task.java | 2 +- 16 files changed, 1272 insertions(+), 11 deletions(-) create mode 100644 play-services-base/core/build.gradle create mode 100644 play-services-base/core/src/main/kotlin/org/microg/gms/utils/IntentCacheManager.kt create mode 100644 play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/nfc/NfcTransportHandler.kt create mode 100644 play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/usb/UsbDevicePermissionManager.kt create mode 100644 play-services-location/core/provider/build.gradle create mode 100644 play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/NetworkLocationProviderPreTiramisu.kt create mode 100644 play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/AdvertiserService.kt create mode 100644 play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/CleanupService.kt create mode 100644 play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/NotifyService.kt create mode 100644 play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ScannerService.kt diff --git a/play-services-auth-api-phone/core/src/main/kotlin/org/microg/gms/auth/phone/SmsRetrieverCore.kt b/play-services-auth-api-phone/core/src/main/kotlin/org/microg/gms/auth/phone/SmsRetrieverCore.kt index c0c70b49..14096e4e 100644 --- a/play-services-auth-api-phone/core/src/main/kotlin/org/microg/gms/auth/phone/SmsRetrieverCore.kt +++ b/play-services-auth-api-phone/core/src/main/kotlin/org/microg/gms/auth/phone/SmsRetrieverCore.kt @@ -25,6 +25,7 @@ import android.telephony.SmsMessage import android.text.TextUtils import android.util.Base64 import android.util.Log +import androidx.core.app.PendingIntentCompat import androidx.core.content.ContextCompat import androidx.core.os.bundleOf import androidx.lifecycle.DefaultLifecycleObserver @@ -160,7 +161,7 @@ class SmsRetrieverCore(private val context: Context, override val lifecycle: Lif private fun getTimeoutPendingIntent(context: Context, packageName: String): PendingIntent { val intent = Intent(ACTION_SMS_RETRIEVE_TIMEOUT) intent.setPackage(packageName) - return PendingIntent.getBroadcast(context, ++requestCode, intent, PendingIntent.FLAG_IMMUTABLE) + return PendingIntentCompat.getBroadcast(context, ++requestCode, intent, 0, false)!! } private fun tryHandleIncomingMessageAsRetrieverMessage(message: SmsMessage): Boolean { diff --git a/play-services-base/core/build.gradle b/play-services-base/core/build.gradle new file mode 100644 index 00000000..96ace60a --- /dev/null +++ b/play-services-base/core/build.gradle @@ -0,0 +1,61 @@ +/* + * SPDX-FileCopyrightText: 2020, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'maven-publish' +apply plugin: 'signing' + +dependencies { + api project(':play-services-basement-ktx') + + implementation "androidx.annotation:annotation:$annotationVersion" + implementation "androidx.appcompat:appcompat:$appcompatVersion" + implementation "androidx.core:core-ktx:$coreVersion" + implementation "androidx.lifecycle:lifecycle-service:$lifecycleVersion" + implementation "androidx.navigation:navigation-fragment-ktx:$navigationVersion" + implementation "androidx.navigation:navigation-ui-ktx:$navigationVersion" + implementation "androidx.preference:preference-ktx:$preferenceVersion" + + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" +} + +android { + namespace "org.microg.gms.base.core" + + compileSdkVersion androidCompileSdk + buildToolsVersion "$androidBuildVersionTools" + + defaultConfig { + versionName version + minSdkVersion androidMinSdk + targetSdkVersion androidTargetSdk + } + + buildFeatures { + dataBinding = true + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + compileOptions { + sourceCompatibility = 1.8 + targetCompatibility = 1.8 + } + + kotlinOptions { + jvmTarget = 1.8 + } + + lintOptions { + disable 'MissingTranslation' + } +} + +apply from: '../../gradle/publish-android.gradle' + +description = 'microG service implementation for play-services-base' diff --git a/play-services-base/core/src/main/kotlin/org/microg/gms/utils/IntentCacheManager.kt b/play-services-base/core/src/main/kotlin/org/microg/gms/utils/IntentCacheManager.kt new file mode 100644 index 00000000..4a5e55be --- /dev/null +++ b/play-services-base/core/src/main/kotlin/org/microg/gms/utils/IntentCacheManager.kt @@ -0,0 +1,149 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.utils + +import android.app.AlarmManager +import android.app.AlarmManager.ELAPSED_REALTIME_WAKEUP +import android.app.PendingIntent.FLAG_NO_CREATE +import android.app.PendingIntent.FLAG_UPDATE_CURRENT +import android.app.Service +import android.content.Context +import android.content.Intent +import android.os.Build.VERSION.SDK_INT +import android.os.Parcelable +import android.os.SystemClock +import android.util.Log +import androidx.core.app.PendingIntentCompat +import androidx.core.content.getSystemService +import java.util.UUID + +class IntentCacheManager(private val context: Context, private val clazz: Class, private val type: Int) { + private val lock = Any() + private lateinit var content: ArrayList + private lateinit var id: String + private var isReady: Boolean = false + private val pendingActions: MutableList<() -> Unit> = arrayListOf() + + init { + val pendingIntent = PendingIntentCompat.getService(context, type, getIntent(), 0, true)!! + val alarmManager = context.getSystemService() + if (SDK_INT >= 19) { + alarmManager?.setWindow(ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + TEN_YEARS, -1, pendingIntent) + } else { + alarmManager?.set(ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + TEN_YEARS, pendingIntent) + } + pendingIntent.send() + } + + private fun getIntent() = Intent(context, clazz).apply { + action = ACTION + putExtra(EXTRA_IS_CACHE, true) + putExtra(EXTRA_CACHE_TYPE, this@IntentCacheManager.type) + } + + fun add(entry: T, check: (T) -> Boolean = { false }) = runIfReady { + val iterator = content.iterator() + while (iterator.hasNext()) { + if (check(iterator.next())) { + iterator.remove() + } + } + content.add(entry) + updateIntent() + } + + fun remove(entry: T) = runIfReady { + if (content.remove(entry)) updateIntent() + } + + fun removeIf(check: (T) -> Boolean) = runIfReady { + var removed = false + val iterator = content.iterator() + while (iterator.hasNext()) { + if (check(iterator.next())) { + iterator.remove() + removed = true + } + } + if (removed) updateIntent() + } + + fun clear() = runIfReady { + content.clear() + updateIntent() + } + + fun getId(): String? = if (this::id.isInitialized) id else null + + fun getEntries(): List = if (this::content.isInitialized) content else emptyList() + + fun processIntent(intent: Intent) { + if (isCache(intent) && getType(intent) == type) { + synchronized(lock) { + content = intent.getParcelableArrayListExtra(EXTRA_DATA) ?: arrayListOf() + id = intent.getStringExtra(EXTRA_ID) ?: UUID.randomUUID().toString() + if (!intent.hasExtra(EXTRA_ID)) { + Log.d(TAG, "Created new intent cache with id $id") + } else if (intent.hasExtra(EXTRA_DATA)) { + Log.d(TAG, "Recovered data from intent cache with id $id") + } + pendingActions.forEach { it() } + pendingActions.clear() + isReady = true + updateIntent() + } + } + } + + private fun runIfReady(action: () -> Unit) { + synchronized(lock) { + if (isReady) { + action() + } else { + pendingActions.add(action) + } + } + } + + private fun updateIntent() { + synchronized(lock) { + if (isReady) { + val intent = getIntent().apply { + putExtra(EXTRA_ID, id) + putParcelableArrayListExtra(EXTRA_DATA, content) + } + val pendingIntent = PendingIntentCompat.getService(context, type, intent, FLAG_NO_CREATE or FLAG_UPDATE_CURRENT, true) + if (pendingIntent == null) { + Log.w(TAG, "Failed to update existing pending intent, will likely have a loss of information") + } + } + } + } + + companion object { + private const val TAG = "IntentCacheManager" + private const val TEN_YEARS = 315360000000L + private const val ACTION = "org.microg.gms.ACTION_INTENT_CACHE_MANAGER" + private const val EXTRA_IS_CACHE = "org.microg.gms.IntentCacheManager.is_cache" + private const val EXTRA_CACHE_TYPE = "org.microg.gms.IntentCacheManager.cache_type" + private const val EXTRA_ID = "org.microg.gms.IntentCacheManager.id" + private const val EXTRA_DATA = "org.microg.gms.IntentCacheManager.data" + + inline fun create(context: Context, type: Int) = IntentCacheManager(context, S::class.java, type) + + fun isCache(intent: Intent): Boolean = try { + intent.getBooleanExtra(EXTRA_IS_CACHE, false) + } catch (e: Exception) { + false + } + + fun getType(intent: Intent): Int { + val ret = intent.getIntExtra(EXTRA_CACHE_TYPE, -1) + if (ret == -1) throw IllegalArgumentException() + return ret + } + } +} \ No newline at end of file diff --git a/play-services-core/src/main/kotlin/org/microg/gms/games/GamesConnectService.kt b/play-services-core/src/main/kotlin/org/microg/gms/games/GamesConnectService.kt index e5f7486a..34810ef7 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/games/GamesConnectService.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/games/GamesConnectService.kt @@ -11,6 +11,7 @@ import android.content.Context import android.content.Intent import android.os.Parcel import android.util.Log +import androidx.core.app.PendingIntentCompat import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope @@ -51,10 +52,10 @@ class GamesConnectServiceImpl(val context: Context, override val lifecycle: Life override fun signIn(callback: IGamesConnectCallbacks?, request: GamesSignInRequest?) { Log.d(TAG, "signIn($request)") fun sendSignInRequired() { - val resolution = PendingIntent.getActivity(context, packageName.hashCode(), Intent(context, GamesSignInActivity::class.java).apply { + val resolution = PendingIntentCompat.getActivity(context, packageName.hashCode(), Intent(context, GamesSignInActivity::class.java).apply { putExtra(EXTRA_GAME_PACKAGE_NAME, packageName) putExtra(EXTRA_SCOPES, arrayOf(Scopes.GAMES_LITE)) - }, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + }, PendingIntent.FLAG_UPDATE_CURRENT, false) when (request?.signInType) { 0 -> { // Manual sign in, provide resolution callback?.onSignIn(Status(CommonStatusCodes.SIGN_IN_REQUIRED, null, resolution), null) diff --git a/play-services-core/src/main/kotlin/org/microg/gms/games/GamesService.kt b/play-services-core/src/main/kotlin/org/microg/gms/games/GamesService.kt index d6b3e96e..13e37a41 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/games/GamesService.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/games/GamesService.kt @@ -14,6 +14,7 @@ import android.os.Bundle import android.os.IBinder import android.os.Parcel import android.util.Log +import androidx.core.app.PendingIntentCompat import androidx.core.os.bundleOf import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner @@ -55,7 +56,7 @@ class GamesService : BaseService(TAG, GmsService.GAMES) { Log.d(TAG, "Sending SIGN_IN_REQUIRED to $packageName") callback.onPostInitCompleteWithConnectionInfo(ConnectionResult.SIGN_IN_REQUIRED, null, ConnectionInfo().apply { params = bundleOf( - "pendingIntent" to PendingIntent.getActivity( + "pendingIntent" to PendingIntentCompat.getActivity( this@GamesService, packageName.hashCode(), Intent(this@GamesService, GamesSignInActivity::class.java).apply { @@ -63,7 +64,8 @@ class GamesService : BaseService(TAG, GmsService.GAMES) { putExtra(EXTRA_ACCOUNT, request.account) putExtra(EXTRA_SCOPES, request.scopes) }, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + PendingIntent.FLAG_UPDATE_CURRENT, + false ) ) }) diff --git a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/privileged/Fido2PrivilegedService.kt b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/privileged/Fido2PrivilegedService.kt index 00745089..0ac84f8f 100644 --- a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/privileged/Fido2PrivilegedService.kt +++ b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/privileged/Fido2PrivilegedService.kt @@ -6,14 +6,13 @@ package org.microg.gms.fido.core.privileged import android.app.KeyguardManager -import android.app.PendingIntent -import android.app.PendingIntent.FLAG_IMMUTABLE import android.app.PendingIntent.FLAG_UPDATE_CURRENT import android.content.Context import android.content.Context.KEYGUARD_SERVICE import android.content.Intent import android.os.Build.VERSION.SDK_INT import android.os.Parcel +import androidx.core.app.PendingIntentCompat import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope @@ -70,7 +69,7 @@ class Fido2PrivilegedServiceImpl(private val context: Context, override val life .putExtra(KEY_OPTIONS, options.serializeToBytes()) val pendingIntent = - PendingIntent.getActivity(context, options.hashCode(), intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE) + PendingIntentCompat.getActivity(context, options.hashCode(), intent, FLAG_UPDATE_CURRENT, false) callbacks.onPendingIntent(Status.SUCCESS, pendingIntent) } } @@ -84,7 +83,7 @@ class Fido2PrivilegedServiceImpl(private val context: Context, override val life .putExtra(KEY_OPTIONS, options.serializeToBytes()) val pendingIntent = - PendingIntent.getActivity(context, options.hashCode(), intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE) + PendingIntentCompat.getActivity(context, options.hashCode(), intent, FLAG_UPDATE_CURRENT, false) callbacks.onPendingIntent(Status.SUCCESS, pendingIntent) } } diff --git a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/nfc/NfcTransportHandler.kt b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/nfc/NfcTransportHandler.kt new file mode 100644 index 00000000..3d49cf89 --- /dev/null +++ b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/nfc/NfcTransportHandler.kt @@ -0,0 +1,113 @@ +/* + * SPDX-FileCopyrightText: 2022 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.fido.core.transport.nfc + +import android.app.Activity +import android.app.PendingIntent +import android.content.Intent +import android.content.IntentFilter +import android.nfc.NfcAdapter +import android.nfc.Tag +import android.nfc.tech.IsoDep +import android.util.Log +import androidx.core.app.OnNewIntentProvider +import androidx.core.app.PendingIntentCompat +import androidx.core.util.Consumer +import com.google.android.gms.fido.fido2.api.common.AuthenticatorAssertionResponse +import com.google.android.gms.fido.fido2.api.common.AuthenticatorAttestationResponse +import com.google.android.gms.fido.fido2.api.common.AuthenticatorResponse +import com.google.android.gms.fido.fido2.api.common.RequestOptions +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CompletableDeferred +import org.microg.gms.fido.core.RequestOptionsType +import org.microg.gms.fido.core.transport.Transport +import org.microg.gms.fido.core.transport.TransportHandler +import org.microg.gms.fido.core.transport.TransportHandlerCallback +import org.microg.gms.fido.core.type + +class NfcTransportHandler(private val activity: Activity, callback: TransportHandlerCallback? = null) : + TransportHandler(Transport.NFC, callback) { + override val isSupported: Boolean + get() = NfcAdapter.getDefaultAdapter(activity)?.isEnabled == true && activity is OnNewIntentProvider + + private var deferred = CompletableDeferred() + + private suspend fun waitForNewNfcTag(adapter: NfcAdapter): Tag { + val intent = Intent(activity, activity.javaClass).apply { addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) } + val pendingIntent: PendingIntent = PendingIntentCompat.getActivity(activity, 0, intent, 0, true)!! + adapter.enableForegroundDispatch( + activity, + pendingIntent, + arrayOf(IntentFilter(NfcAdapter.ACTION_TECH_DISCOVERED)), + arrayOf(arrayOf(IsoDep::class.java.name)) + ) + invokeStatusChanged(TransportHandlerCallback.STATUS_WAITING_FOR_DEVICE) + val tag = deferred.await() + deferred = CompletableDeferred() + return tag + } + + suspend fun register( + options: RequestOptions, + callerPackage: String, + tag: Tag + ): AuthenticatorAttestationResponse { + return CtapNfcConnection(activity, tag).open { + register(it, activity, options, callerPackage) + } + } + + suspend fun sign( + options: RequestOptions, + callerPackage: String, + tag: Tag + ): AuthenticatorAssertionResponse { + return CtapNfcConnection(activity, tag).open { + sign(it, activity, options, callerPackage) + } + } + + + suspend fun handle( + options: RequestOptions, + callerPackage: String, + tag: Tag + ): AuthenticatorResponse { + return when (options.type) { + RequestOptionsType.REGISTER -> register(options, callerPackage, tag) + RequestOptionsType.SIGN -> sign(options, callerPackage, tag) + } + } + + + override suspend fun start(options: RequestOptions, callerPackage: String): AuthenticatorResponse { + val adapter = NfcAdapter.getDefaultAdapter(activity) + val newIntentListener = Consumer { + if (it?.action != NfcAdapter.ACTION_TECH_DISCOVERED) return@Consumer + val tag = it.getParcelableExtra(NfcAdapter.EXTRA_TAG) ?: return@Consumer + deferred.complete(tag) + } + try { + (activity as OnNewIntentProvider).addOnNewIntentListener(newIntentListener) + while (true) { + val tag = waitForNewNfcTag(adapter) + try { + return handle(options, callerPackage, tag) + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + Log.w(TAG, e) + } + } + } finally { + (activity as OnNewIntentProvider).removeOnNewIntentListener(newIntentListener) + } + } + + companion object { + const val TAG = "FidoNfcHandler" + } +} diff --git a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/usb/UsbDevicePermissionManager.kt b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/usb/UsbDevicePermissionManager.kt new file mode 100644 index 00000000..ef3c4098 --- /dev/null +++ b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/usb/UsbDevicePermissionManager.kt @@ -0,0 +1,84 @@ +/* + * SPDX-FileCopyrightText: 2022 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.fido.core.transport.usb + +import android.app.PendingIntent +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.hardware.usb.UsbDevice +import android.hardware.usb.UsbManager +import android.os.Build.VERSION.SDK_INT +import androidx.core.app.PendingIntentCompat +import kotlinx.coroutines.CompletableDeferred + +private val Context.usbPermissionCallbackAction + get() = "$packageName.USB_PERMISSION_CALLBACK" + +private object UsbDevicePermissionReceiver : BroadcastReceiver() { + private var registered = false + private val pendingRequests = hashMapOf>>() + + fun register(context: Context) = synchronized(this) { + if (!registered) { + context.registerReceiver(this, IntentFilter(context.usbPermissionCallbackAction)) + registered = true + } + } + + fun addDeferred(device: UsbDevice, deferred: CompletableDeferred) = synchronized(this) { + if (pendingRequests.containsKey(device)) { + pendingRequests[device]!!.add(deferred) + false + } else { + pendingRequests[device] = arrayListOf(deferred) + true + } + } + + fun unregister(context: Context) = synchronized(this) { + if (registered) { + context.unregisterReceiver(this) + registered = false + } + } + + override fun onReceive(context: Context?, intent: Intent?) { + if (intent == null || context == null) return + if (intent.action == context.usbPermissionCallbackAction) { + val device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE) + if (device != null) { + synchronized(this) { + if (pendingRequests.containsKey(device)) { + val hasPermission = context.usbManager?.hasPermission(device) == true + for (deferred in pendingRequests[device].orEmpty()) { + deferred.complete(hasPermission) + } + pendingRequests.remove(device) + if (pendingRequests.isEmpty()) { + unregister(context) + } + } + } + } + } + } +} + +class UsbDevicePermissionManager(private val context: Context) { + + suspend fun awaitPermission(device: UsbDevice): Boolean { + if (context.usbManager?.hasPermission(device) == true) return true + val res = CompletableDeferred() + if (UsbDevicePermissionReceiver.addDeferred(device, res)) { + UsbDevicePermissionReceiver.register(context) + val intent = PendingIntentCompat.getBroadcast(context, 0, Intent(context.usbPermissionCallbackAction).apply { `package` = context.packageName }, 0, true) + context.usbManager?.requestPermission(device, intent) + } + return res.await() + } +} diff --git a/play-services-location/core/provider/build.gradle b/play-services-location/core/provider/build.gradle new file mode 100644 index 00000000..5eac5823 --- /dev/null +++ b/play-services-location/core/provider/build.gradle @@ -0,0 +1,57 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +dependencies { + api project(':play-services-location') + compileOnly project(':play-services-location-core-system-api') + implementation project(':play-services-base-core') + implementation project(':play-services-location-core-base') + + implementation "androidx.core:core-ktx:$coreVersion" + implementation "androidx.lifecycle:lifecycle-service:$lifecycleVersion" + implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion" + + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutineVersion" + + implementation "com.android.volley:volley:$volleyVersion" + + implementation 'org.microg:address-formatter:0.3.1' +} + +android { + namespace "org.microg.gms.location.provider" + + compileSdkVersion androidCompileSdk + buildToolsVersion "$androidBuildVersionTools" + + buildFeatures { + buildConfig = true + } + + defaultConfig { + versionName version + minSdkVersion androidMinSdk + targetSdkVersion androidTargetSdk + buildConfigField "String", "ICHNAEA_KEY", "\"${localProperties.get("ichnaea.key", "")}\"" + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + compileOptions { + sourceCompatibility = 1.8 + targetCompatibility = 1.8 + } + + kotlinOptions { + jvmTarget = 1.8 + } +} diff --git a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/NetworkLocationProviderPreTiramisu.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/NetworkLocationProviderPreTiramisu.kt new file mode 100644 index 00000000..f2a4001c --- /dev/null +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/provider/NetworkLocationProviderPreTiramisu.kt @@ -0,0 +1,124 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.location.provider + +import android.app.PendingIntent +import android.app.PendingIntent.FLAG_UPDATE_CURRENT +import android.content.Context +import android.content.Intent +import android.location.Criteria +import android.location.Location +import android.os.Build.VERSION.SDK_INT +import android.os.WorkSource +import androidx.annotation.RequiresApi +import androidx.core.app.PendingIntentCompat +import com.android.location.provider.ProviderPropertiesUnbundled +import com.android.location.provider.ProviderRequestUnbundled +import org.microg.gms.location.* +import org.microg.gms.location.network.LOCATION_EXTRA_PRECISION +import org.microg.gms.location.network.NetworkLocationService +import org.microg.gms.location.provider.NetworkLocationProviderService.Companion.ACTION_REPORT_LOCATION +import java.io.PrintWriter +import kotlin.math.max + +class NetworkLocationProviderPreTiramisu : AbstractLocationProviderPreTiramisu { + @Deprecated("Use only with SDK < 31") + constructor(context: Context, legacy: Unit) : super(properties) { + this.context = context + } + + @RequiresApi(31) + constructor(context: Context) : super(context, properties) { + this.context = context + } + + private val context: Context + private var enabled = false + private var currentRequest: ProviderRequestUnbundled? = null + private var pendingIntent: PendingIntent? = null + private var lastReportedLocation: Location? = null + + private fun updateRequest() { + if (enabled) { + val forceNow: Boolean + val intervalMillis: Long + if (currentRequest?.reportLocation == true) { + forceNow = true + intervalMillis = max(currentRequest?.interval ?: Long.MAX_VALUE, MIN_INTERVAL_MILLIS) + } else { + forceNow = false + intervalMillis = Long.MAX_VALUE + } + val intent = Intent(context, NetworkLocationService::class.java) + intent.putExtra(EXTRA_PENDING_INTENT, pendingIntent) + intent.putExtra(EXTRA_ENABLE, true) + intent.putExtra(EXTRA_INTERVAL_MILLIS, intervalMillis) + intent.putExtra(EXTRA_FORCE_NOW, forceNow) + if (SDK_INT >= 31) { + intent.putExtra(EXTRA_LOW_POWER, currentRequest?.isLowPower ?: false) + intent.putExtra(EXTRA_WORK_SOURCE, currentRequest?.workSource) + } + if (SDK_INT >= 29) { + intent.putExtra(EXTRA_BYPASS, currentRequest?.isLocationSettingsIgnored ?: false) + } + context.startService(intent) + } + } + + override fun dump(writer: PrintWriter) { + writer.println("Enabled: $enabled") + writer.println("Current request: $currentRequest") + writer.println("Last reported: $lastReportedLocation") + } + + override fun onSetRequest(request: ProviderRequestUnbundled, source: WorkSource) { + synchronized(this) { + currentRequest = request + updateRequest() + } + } + + override fun enable() { + synchronized(this) { + if (enabled) throw IllegalStateException() + val intent = Intent(context, NetworkLocationProviderService::class.java) + intent.action = ACTION_REPORT_LOCATION + pendingIntent = PendingIntentCompat.getService(context, 0, intent, FLAG_UPDATE_CURRENT, true) + currentRequest = null + enabled = true + when { + SDK_INT >= 30 -> isAllowed = true + SDK_INT >= 29 -> isEnabled = true + } + } + } + + override fun disable() { + synchronized(this) { + if (!enabled) throw IllegalStateException() + val intent = Intent(context, NetworkLocationService::class.java) + intent.putExtra(EXTRA_PENDING_INTENT, pendingIntent) + intent.putExtra(EXTRA_ENABLE, false) + context.startService(intent) + pendingIntent?.cancel() + pendingIntent = null + currentRequest = null + enabled = false + } + } + + override fun reportLocationToSystem(location: Location) { + location.provider = "network" + location.extras?.remove(LOCATION_EXTRA_PRECISION) + lastReportedLocation = location + super.reportLocation(location) + } + + companion object { + private const val MIN_INTERVAL_MILLIS = 20000L + private val properties = ProviderPropertiesUnbundled.create(false, false, false, false, true, true, true, Criteria.POWER_LOW, Criteria.ACCURACY_COARSE) + } +} \ No newline at end of file diff --git a/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/AdvertiserService.kt b/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/AdvertiserService.kt new file mode 100644 index 00000000..09a0af4e --- /dev/null +++ b/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/AdvertiserService.kt @@ -0,0 +1,268 @@ +/* + * SPDX-FileCopyrightText: 2020, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.nearby.exposurenotification + +import android.annotation.SuppressLint +import android.annotation.TargetApi +import android.app.AlarmManager +import android.app.PendingIntent.FLAG_ONE_SHOT +import android.app.PendingIntent.FLAG_UPDATE_CURRENT +import android.bluetooth.BluetoothAdapter.* +import android.bluetooth.le.* +import android.bluetooth.le.AdvertiseSettings.* +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.Build.VERSION.SDK_INT +import android.os.Handler +import android.os.Looper +import android.os.SystemClock +import android.util.Log +import androidx.core.app.PendingIntentCompat +import androidx.lifecycle.LifecycleService +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.microg.gms.common.ForegroundServiceContext +import org.microg.gms.common.ForegroundServiceInfo +import java.io.FileDescriptor +import java.io.PrintWriter +import java.nio.ByteBuffer +import java.util.* + +@TargetApi(21) +@ForegroundServiceInfo("Exposure Notification") +class AdvertiserService : LifecycleService() { + private val version = VERSION_1_1 + private var advertising = false + private var wantStartAdvertising = false + private val advertiser: BluetoothLeAdvertiser? + get() = getDefaultAdapter()?.bluetoothLeAdvertiser + private val alarmManager: AlarmManager + get() = getSystemService(Context.ALARM_SERVICE) as AlarmManager + private val callback: AdvertiseCallback = object : AdvertiseCallback() { + override fun onStartSuccess(settingsInEffect: AdvertiseSettings?) { + Log.d(TAG, "Advertising active for ${settingsInEffect?.timeout}ms") + } + + override fun onStartFailure(errorCode: Int) { + Log.w(TAG, "Advertising failed: $errorCode") + stopOrRestartAdvertising() + } + } + + @TargetApi(23) + private var setCallback: Any? = null + private val trigger = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + if (intent?.action == ACTION_STATE_CHANGED) { + when (intent.getIntExtra(EXTRA_STATE, -1)) { + STATE_TURNING_OFF, STATE_OFF -> stopOrRestartAdvertising() + STATE_ON -> startAdvertisingIfNeeded() + } + } + } + } + private val handler = Handler(Looper.getMainLooper()) + private val startLaterRunnable = Runnable { startAdvertisingIfNeeded() } + + override fun onCreate() { + super.onCreate() + registerReceiver(trigger, IntentFilter().apply { addAction(ACTION_STATE_CHANGED) }) + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + ForegroundServiceContext.completeForegroundService(this, intent, TAG) + Log.d(TAG, "AdvertisingService.start: $intent") + super.onStartCommand(intent, flags, startId) + if (intent?.action == ACTION_RESTART_ADVERTISING && advertising) { + stopOrRestartAdvertising() + } else { + startAdvertisingIfNeeded() + } + return START_STICKY + } + + override fun onDestroy() { + super.onDestroy() + unregisterReceiver(trigger) + stopOrRestartAdvertising() + handler.removeCallbacks(startLaterRunnable) + } + + private fun startAdvertisingIfNeeded() { + if (ExposurePreferences(this).enabled) { + lifecycleScope.launchWhenStarted { + withContext(Dispatchers.IO) { + startAdvertising() + } + } + } else { + stopSelf() + } + } + + private var lastStartTime = System.currentTimeMillis() + private var sendingBytes = ByteArray(0) + private var starting = false + + @SuppressLint("MissingPermission") + private suspend fun startAdvertising() { + val advertiser = synchronized(this) { + if (advertising || starting) return + val advertiser = advertiser ?: return + wantStartAdvertising = false + starting = true + advertiser + } + try { + val aemBytes = when (version) { + VERSION_1_0 -> byteArrayOf( + version, // Version and flags + currentDeviceInfo.txPowerCorrection, // TX power + 0x00, // Reserved + 0x00 // Reserved + ) + VERSION_1_1 -> byteArrayOf( + (version + currentDeviceInfo.confidence.toByte() * 4).toByte(), // Version and flags + currentDeviceInfo.txPowerCorrection, // TX power + 0x00, // Reserved + 0x00 // Reserved + ) + else -> return + } + var nextSend = nextKeyMillis.coerceAtLeast(10000) + val payload = ExposureDatabase.with(this@AdvertiserService) { database -> + database.generateCurrentPayload(aemBytes) + } + val data = AdvertiseData.Builder().addServiceUuid(SERVICE_UUID).addServiceData(SERVICE_UUID, payload).build() + Log.i(TAG, "Starting advertiser") + if (SDK_INT >= 26) { + setCallback = SetCallback() + val params = AdvertisingSetParameters.Builder() + .setInterval(AdvertisingSetParameters.INTERVAL_MEDIUM) + .setLegacyMode(true) + .setTxPowerLevel(AdvertisingSetParameters.TX_POWER_LOW) + .setConnectable(false) + .build() + try { + advertiser.startAdvertisingSet(params, data, null, null, null, setCallback as AdvertisingSetCallback) + } catch (e: SecurityException) { + Log.e(TAG, "Couldn't start advertising: Need android.permission.BLUETOOTH_ADVERTISE permission.", ) + } + } else { + nextSend = nextSend.coerceAtMost(180000) + val settings = Builder() + .setTimeout(nextSend.toInt()) + .setAdvertiseMode(ADVERTISE_MODE_BALANCED) + .setTxPowerLevel(ADVERTISE_TX_POWER_LOW) + .setConnectable(false) + .build() + try { + advertiser.startAdvertising(settings, data, callback) + } catch (e: SecurityException) { + Log.e(TAG, "Couldn't start advertising.", ) + } + } + synchronized(this) { advertising = true } + sendingBytes = payload + lastStartTime = System.currentTimeMillis() + scheduleRestartAdvertising(nextSend) + } finally { + synchronized(this) { starting = false } + } + } + + override fun dump(fd: FileDescriptor?, writer: PrintWriter?, args: Array?) { + writer?.println("Advertising: $advertising") + try { + val startTime = lastStartTime + val bytes = sendingBytes + val (uuid, aem) = ByteBuffer.wrap(bytes).let { UUID(it.long, it.long) to it.int } + writer?.println(""" + Last advertising: + Since: ${Date(startTime)} + RPI: $uuid + Version: 0x${version.toString(16)} + TX Power: ${currentDeviceInfo.txPowerCorrection} + AEM: 0x${aem.toLong().let { if (it < 0) 0x100000000L + it else it }.toString(16)} + """.trimIndent()) + } catch (e: Exception) { + writer?.println("Last advertising: ${e.message ?: e.toString()}") + } + } + + private fun scheduleRestartAdvertising(nextSend: Long) { + val intent = Intent(this, AdvertiserService::class.java).apply { action = ACTION_RESTART_ADVERTISING } + val pendingIntent = PendingIntentCompat.getService(this, ACTION_RESTART_ADVERTISING.hashCode(), intent, FLAG_ONE_SHOT or FLAG_UPDATE_CURRENT, false)!! + when { + SDK_INT >= 23 -> + alarmManager.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + nextSend, pendingIntent) + else -> + alarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + nextSend, pendingIntent) + } + } + + @Synchronized + @SuppressLint("MissingPermission") + private fun stopOrRestartAdvertising() { + if (!advertising) return + Log.i(TAG, "Stopping advertiser") + advertising = false + if (SDK_INT >= 26) { + wantStartAdvertising = true + try { + advertiser?.stopAdvertisingSet(setCallback as AdvertisingSetCallback) + } catch (e: SecurityException) { + Log.i(TAG, "Tried calling stopAdvertisingSet without android.permission.BLUETOOTH_ADVERTISE permission.", ) + } + } else { + try { + advertiser?.stopAdvertising(callback) + } catch (e: SecurityException) { + Log.i(TAG, "stopAdvertising() failed with a SecurityException. Maybe some permissions are missing?", ) + } + } + handler.postDelayed(startLaterRunnable, 1000) + } + + @TargetApi(26) + inner class SetCallback : AdvertisingSetCallback() { + override fun onAdvertisingSetStarted(advertisingSet: AdvertisingSet?, txPower: Int, status: Int) { + Log.d(TAG, "Advertising active, status=$status") + } + + override fun onAdvertisingSetStopped(advertisingSet: AdvertisingSet?) { + Log.d(TAG, "Advertising stopped") + if (wantStartAdvertising) { + startAdvertisingIfNeeded() + } else { + stopOrRestartAdvertising() + } + } + } + + + companion object { + private const val ACTION_RESTART_ADVERTISING = "org.microg.gms.nearby.exposurenotification.RESTART_ADVERTISING" + + fun isNeeded(context: Context): Boolean { + return ExposurePreferences(context).enabled + } + + fun isSupported(context: Context): Boolean? { + val adapter = getDefaultAdapter() + return when { + adapter == null -> false + SDK_INT >= 26 && (adapter.isLeExtendedAdvertisingSupported || adapter.isLePeriodicAdvertisingSupported) -> true + adapter.state != STATE_ON -> null + adapter.bluetoothLeAdvertiser != null -> true + else -> false + } + } + } +} diff --git a/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/CleanupService.kt b/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/CleanupService.kt new file mode 100644 index 00000000..ba842d3a --- /dev/null +++ b/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/CleanupService.kt @@ -0,0 +1,64 @@ +/* + * SPDX-FileCopyrightText: 2020, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.nearby.exposurenotification + +import android.app.AlarmManager +import android.app.PendingIntent.FLAG_ONE_SHOT +import android.app.PendingIntent.FLAG_UPDATE_CURRENT +import android.content.Context +import android.content.Intent +import android.util.Log +import androidx.core.app.PendingIntentCompat +import androidx.lifecycle.LifecycleService +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.microg.gms.common.ForegroundServiceContext +import org.microg.gms.common.ForegroundServiceInfo + +@ForegroundServiceInfo("Exposure Notification") +class CleanupService : LifecycleService() { + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + ForegroundServiceContext.completeForegroundService(this, intent, TAG) + Log.d(TAG, "CleanupService.start: $intent") + super.onStartCommand(intent, flags, startId) + if (isNeeded(this, true)) { + lifecycleScope.launchWhenStarted { + withContext(Dispatchers.IO) { + var workPending = true + while (workPending) { + ExposureDatabase.with(this@CleanupService) { + workPending = !it.dailyCleanup() + } + if (workPending) delay(5000L) + } + ExposurePreferences(this@CleanupService).lastCleanup = System.currentTimeMillis() + } + stop() + } + } else { + stop() + } + return START_NOT_STICKY + } + + fun stop() { + val alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager + val pendingIntent = PendingIntentCompat.getService(applicationContext, CleanupService::class.java.name.hashCode(), Intent(applicationContext, CleanupService::class.java), FLAG_ONE_SHOT or FLAG_UPDATE_CURRENT, false)!! + alarmManager.set(AlarmManager.RTC, ExposurePreferences(this).lastCleanup + CLEANUP_INTERVAL, pendingIntent) + stopSelf() + } + + companion object { + fun isNeeded(context: Context, now: Boolean = false): Boolean { + return ExposurePreferences(context).let { + (it.enabled && !now) || it.lastCleanup < System.currentTimeMillis() - CLEANUP_INTERVAL + } + } + } +} diff --git a/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ExposureNotificationServiceImpl.kt b/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ExposureNotificationServiceImpl.kt index 12241493..e31eeb73 100644 --- a/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ExposureNotificationServiceImpl.kt +++ b/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ExposureNotificationServiceImpl.kt @@ -16,6 +16,7 @@ import android.location.LocationManager import android.os.* import android.util.Base64 import android.util.Log +import androidx.core.app.PendingIntentCompat import androidx.core.location.LocationManagerCompat import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleCoroutineScope @@ -113,7 +114,7 @@ class ExposureNotificationServiceImpl(private val context: Context, override val Log.w(TAG, e) } Log.d(TAG, "Pending: $intent") - val pi = PendingIntent.getActivity(context, permission.hashCode(), intent, PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE) + val pi = PendingIntentCompat.getActivity(context, permission.hashCode(), intent, PendingIntent.FLAG_ONE_SHOT, false)!! Log.d(TAG, "Pending: $pi") return pi } diff --git a/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/NotifyService.kt b/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/NotifyService.kt new file mode 100644 index 00000000..8adec591 --- /dev/null +++ b/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/NotifyService.kt @@ -0,0 +1,140 @@ +/* + * SPDX-FileCopyrightText: 2020, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.nearby.exposurenotification + +import android.annotation.TargetApi +import android.app.* +import android.app.PendingIntent.FLAG_UPDATE_CURRENT +import android.bluetooth.BluetoothAdapter +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.pm.PackageManager +import android.graphics.Color +import android.location.LocationManager +import android.os.Build.VERSION.SDK_INT +import android.util.Log +import android.util.TypedValue +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.core.app.PendingIntentCompat +import androidx.core.content.ContextCompat +import androidx.core.location.LocationManagerCompat +import androidx.lifecycle.LifecycleService +import com.google.android.gms.nearby.exposurenotification.ExposureNotificationClient +import org.microg.gms.common.ForegroundServiceContext +import org.microg.gms.common.ForegroundServiceInfo +import org.microg.gms.nearby.core.R + +@ForegroundServiceInfo("Exposure Notification") +class NotifyService : LifecycleService() { + private val notificationId = NotifyService::class.java.name.hashCode() + private val trigger = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent?) { + updateNotification() + } + } + + @TargetApi(26) + private fun createNotificationChannel(): String { + val channel = NotificationChannel("exposure-notifications", "Exposure Notifications", NotificationManager.IMPORTANCE_HIGH) + channel.setSound(null, null) + channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC + channel.setShowBadge(true) + if (SDK_INT >= 29) { + channel.setAllowBubbles(false) + } + channel.vibrationPattern = longArrayOf(0) + getSystemService(NotificationManager::class.java).createNotificationChannel(channel) + return channel.id + } + + @TargetApi(21) + private fun updateNotification() { + val location = !LocationManagerCompat.isLocationEnabled(getSystemService(Context.LOCATION_SERVICE) as LocationManager) + val bluetooth = BluetoothAdapter.getDefaultAdapter()?.state.let { it != BluetoothAdapter.STATE_ON && it != BluetoothAdapter.STATE_TURNING_ON } + val nearbyPermissions = arrayOf("android.permission.BLUETOOTH_ADVERTISE", "android.permission.BLUETOOTH_SCAN") + val permissionNeedsHandling = SDK_INT >= 31 && nearbyPermissions.any { + ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED + } + Log.d( TAG,"notify: location: $location, bluetooth: $bluetooth, permissionNeedsHandling: $permissionNeedsHandling") + + val text: String = when { + permissionNeedsHandling -> getString(R.string.exposure_notify_off_nearby) + location && bluetooth -> getString(R.string.exposure_notify_off_bluetooth_location) + location -> getString(R.string.exposure_notify_off_location) + bluetooth -> getString(R.string.exposure_notify_off_bluetooth) + else -> { + NotificationManagerCompat.from(this).cancel(notificationId) + return + } + } + + if (SDK_INT >= 26) { + NotificationCompat.Builder(this, createNotificationChannel()) + } else { + NotificationCompat.Builder(this) + }.apply { + val typedValue = TypedValue() + try { + var resolved = theme.resolveAttribute(androidx.appcompat.R.attr.colorError, typedValue, true) + if (!resolved && SDK_INT >= 26) resolved = theme.resolveAttribute(android.R.attr.colorError, typedValue, true) + color = if (resolved) { + ContextCompat.getColor(this@NotifyService, typedValue.resourceId) + } else { + Color.RED + } + if (SDK_INT >= 26) setColorized(true) + } catch (e: Exception) { + // Ignore + } + setSmallIcon(R.drawable.ic_virus_outline) + setContentTitle(getString(R.string.exposure_notify_off_title)) + setContentText(text) + setStyle(NotificationCompat.BigTextStyle()) + try { + val intent = Intent(ExposureNotificationClient.ACTION_EXPOSURE_NOTIFICATION_SETTINGS).apply { `package` = packageName } + intent.resolveActivity(packageManager) + setContentIntent(PendingIntentCompat.getActivity(this@NotifyService, notificationId, Intent(ExposureNotificationClient.ACTION_EXPOSURE_NOTIFICATION_SETTINGS).apply { `package` = packageName }, FLAG_UPDATE_CURRENT, false)) + } catch (e: Exception) { + // Ignore + } + }.let { + NotificationManagerCompat.from(this).notify(notificationId, it.build()) + } + } + + override fun onCreate() { + super.onCreate() + registerReceiver(trigger, IntentFilter().apply { + addAction(BluetoothAdapter.ACTION_STATE_CHANGED) + if (SDK_INT >= 19) addAction(LocationManager.MODE_CHANGED_ACTION) + addAction(LocationManager.PROVIDERS_CHANGED_ACTION) + addAction(NOTIFICATION_UPDATE_ACTION) + }) + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + ForegroundServiceContext.completeForegroundService(this, intent, TAG) + Log.d(TAG, "NotifyService.start: $intent") + super.onStartCommand(intent, flags, startId) + updateNotification() + return Service.START_STICKY + } + + override fun onDestroy() { + super.onDestroy() + NotificationManagerCompat.from(this).cancel(notificationId) + unregisterReceiver(trigger) + } + + companion object { + fun isNeeded(context: Context): Boolean { + return ExposurePreferences(context).let { it.enabled } + } + } +} diff --git a/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ScannerService.kt b/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ScannerService.kt new file mode 100644 index 00000000..f363737c --- /dev/null +++ b/play-services-nearby/core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ScannerService.kt @@ -0,0 +1,197 @@ +/* + * SPDX-FileCopyrightText: 2020, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.nearby.exposurenotification + +import android.annotation.SuppressLint +import android.annotation.TargetApi +import android.app.AlarmManager +import android.app.PendingIntent.FLAG_ONE_SHOT +import android.app.PendingIntent.FLAG_UPDATE_CURRENT +import android.bluetooth.BluetoothAdapter.* +import android.bluetooth.le.* +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.* +import android.util.Log +import androidx.core.app.PendingIntentCompat +import androidx.lifecycle.LifecycleService +import androidx.lifecycle.lifecycleScope +import org.microg.gms.common.ForegroundServiceContext +import org.microg.gms.common.ForegroundServiceInfo +import java.io.FileDescriptor +import java.io.PrintWriter +import java.util.* + +@TargetApi(21) +@ForegroundServiceInfo("Exposure Notification") +class ScannerService : LifecycleService() { + private var scanning = false + private var lastStartTime = 0L + private var seenAdvertisements = 0L + private var lastAdvertisement = 0L + private val callback = object : ScanCallback() { + override fun onScanResult(callbackType: Int, result: ScanResult?) { + result?.let { onScanResult(it) } + } + + override fun onBatchScanResults(results: MutableList) { + for (result in results) { + onScanResult(result) + } + } + + override fun onScanFailed(errorCode: Int) { + Log.w(TAG, "onScanFailed: $errorCode") + stopScan() + } + } + private val trigger = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + if (intent?.action == ACTION_STATE_CHANGED) { + when (intent.getIntExtra(EXTRA_STATE, -1)) { + STATE_TURNING_OFF, STATE_OFF -> stopScan() + STATE_ON -> startScanIfNeeded() + } + } + } + } + private val handler = Handler(Looper.getMainLooper()) + private val stopLaterRunnable = Runnable { stopScan() } + + // Wake lock for the duration of scan. Otherwise we might fall asleep while scanning + // resulting in potentially very long scan times + private val wakeLock: PowerManager.WakeLock by lazy { + powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, ScannerService::class.java.canonicalName).apply { setReferenceCounted(false) } + } + + private val scanner: BluetoothLeScanner? + get() = getDefaultAdapter()?.bluetoothLeScanner + private val alarmManager: AlarmManager + get() = getSystemService(Context.ALARM_SERVICE) as AlarmManager + private val powerManager: PowerManager + get() = getSystemService(Context.POWER_SERVICE) as PowerManager + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + ForegroundServiceContext.completeForegroundService(this, intent, TAG) + Log.d(TAG, "ScannerService.start: $intent") + super.onStartCommand(intent, flags, startId) + startScanIfNeeded() + return START_STICKY + } + + fun onScanResult(result: ScanResult) { + val data = result.scanRecord?.serviceData?.get(SERVICE_UUID) ?: return + if (data.size < 16) return // Ignore invalid advertisements + seenAdvertisements++ + lastAdvertisement = System.currentTimeMillis() + lifecycleScope.launchWhenStarted { + ExposureDatabase.with(this@ScannerService) { database -> + database.noteAdvertisement(data.sliceArray(0..15), data.drop(16).toByteArray(), result.rssi) + } + } + } + + fun startScanIfNeeded() { + if (ExposurePreferences(this).enabled) { + startScan() + } else { + stopSelf() + } + } + + override fun onCreate() { + super.onCreate() + registerReceiver(trigger, IntentFilter().apply { addAction(ACTION_STATE_CHANGED) }) + } + + override fun onDestroy() { + super.onDestroy() + unregisterReceiver(trigger) + stopScan() + } + + @SuppressLint("WakelockTimeout", "MissingPermission") + @Synchronized + private fun startScan() { + if (scanning) return + val scanner = scanner ?: return + Log.i(TAG, "Starting scanner for service $SERVICE_UUID for ${SCANNING_TIME_MS}ms") + seenAdvertisements = 0 + wakeLock.acquire() + try { + scanner.startScan( + listOf(ScanFilter.Builder() + .setServiceUuid(SERVICE_UUID) + .setServiceData(SERVICE_UUID, byteArrayOf(0), byteArrayOf(0)) + .build()), + ScanSettings.Builder().build(), + callback + ) + } catch (e: SecurityException) { + Log.e(TAG, "Couldn't start ScannerService, need android.permission.BLUETOOTH_SCAN permission.") + } + scanning = true + lastStartTime = System.currentTimeMillis() + handler.postDelayed(stopLaterRunnable, SCANNING_TIME_MS) + } + + @SuppressLint("MissingPermission") + @Synchronized + private fun stopScan() { + if (!scanning) return + Log.i(TAG, "Stopping scanner for service $SERVICE_UUID, had seen $seenAdvertisements advertisements") + handler.removeCallbacks(stopLaterRunnable) + scanning = false + try { + scanner?.stopScan(callback) + } catch (e: Exception) { + // Ignored + } + if (ExposurePreferences(this).enabled) { + scheduleStartScan(((lastStartTime + SCANNING_INTERVAL_MS) - System.currentTimeMillis()).coerceIn(0, SCANNING_INTERVAL_MS)) + } + wakeLock.release() + } + + private fun scheduleStartScan(nextScan: Long) { + val intent = Intent(this, ScannerService::class.java) + val pendingIntent = PendingIntentCompat.getService(this, ScannerService::class.java.hashCode(), intent, FLAG_ONE_SHOT or FLAG_UPDATE_CURRENT, false)!! + if (Build.VERSION.SDK_INT >= 23) { + // Note: there is no setWindowAndAllowWhileIdle() + alarmManager.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + nextScan, pendingIntent) + } else { + alarmManager.setWindow(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + nextScan - SCANNING_TIME_MS / 2, SCANNING_TIME_MS, pendingIntent) + } + } + + override fun dump(fd: FileDescriptor?, writer: PrintWriter?, args: Array?) { + writer?.println("Scanning now: $scanning") + writer?.println("Last scan start: ${Date(lastStartTime)}") + if (Build.VERSION.SDK_INT >= 29) { + writer?.println("Scan stop pending: ${handler.hasCallbacks(stopLaterRunnable)}") + } + writer?.println("Seen advertisements since last scan start: $seenAdvertisements") + writer?.println("Last advertisement seen: ${Date(lastAdvertisement)}") + } + + companion object { + fun isNeeded(context: Context): Boolean { + return ExposurePreferences(context).enabled + } + + fun isSupported(context: Context): Boolean? { + val adapter = getDefaultAdapter() + return when { + adapter == null -> false + adapter.state != STATE_ON -> null + adapter.bluetoothLeScanner != null -> true + else -> false + } + } + } +} diff --git a/play-services-tasks/src/main/java/com/google/android/gms/tasks/Task.java b/play-services-tasks/src/main/java/com/google/android/gms/tasks/Task.java index 7cf39477..9d27a773 100644 --- a/play-services-tasks/src/main/java/com/google/android/gms/tasks/Task.java +++ b/play-services-tasks/src/main/java/com/google/android/gms/tasks/Task.java @@ -114,7 +114,7 @@ public abstract class Task { public abstract Task addOnFailureListener(Activity activity, OnFailureListener listener); /** - * Adds an Activity-scoped listener that is called if the Task fails. + * Adds a listener that is called if the Task fails. *

* The listener will be called on main application thread. If the Task has already failed, a * call to the listener will be immediately scheduled. If multiple listeners are added, they From f164c49a65f6a5ad195f40b30b407dedccbdf643 Mon Sep 17 00:00:00 2001 From: Marvin W Date: Sun, 10 Mar 2024 20:59:22 +0100 Subject: [PATCH 122/209] Add experimental support for in app billing Co-Authored-by: DaVinci9196 <150454414+DaVinci9196@users.noreply.github.com> --- .../firebase/auth/IdentityToolkitClient.kt | 6 +- .../org/microg/gms/profile/ProfileManager.kt | 42 +- .../microg/gms/settings/SettingsContract.kt | 13 +- .../microg/gms/settings/SettingsProvider.kt | 2 + .../src/huawei/AndroidManifest.xml | 6 + .../src/huaweilh/AndroidManifest.xml | 6 + .../org/microg/gms/ui/SettingsFragment.kt | 17 + .../org/microg/gms/ui/VendingFragment.kt | 69 ++ .../microg/gms/vending/VendingPreferences.kt | 41 + .../src/main/res/values-zh-rCN/strings.xml | 2 + .../src/main/res/values-zh-rTW/strings.xml | 5 + .../src/main/res/values/strings.xml | 10 + .../src/main/res/xml/preferences_vending.xml | 31 +- vending-app/build.gradle | 30 + vending-app/src/huaweilh/AndroidManifest.xml | 21 + vending-app/src/main/AndroidManifest.xml | 55 +- .../vending/billing/IInAppBillingService.aidl | 370 +++++++++ .../gms/checkin/internal/ICheckinService.aidl | 7 + .../billingclient/api/BillingClient.java | 217 ++++++ .../android/billingclient/package-info.java | 9 + .../com/android/vending/VendingPreferences.kt | 27 + .../vending/billing/InAppBillingService.kt | 24 + .../vending/licensing/LicenseRequest.java | 2 +- .../vending/licensing/LicensingService.java | 33 +- .../org/microg/vending/billing/AuthManager.kt | 35 + .../vending/billing/CheckinServiceClient.kt | 49 ++ .../org/microg/vending/billing/Constants.kt | 17 + .../microg/vending/billing/ContextProvider.kt | 19 + .../vending/billing/DeviceIdentifier.kt | 71 ++ .../org/microg/vending/billing/GServices.kt | 20 + .../billing/InAppBillingServiceImpl.kt | 629 ++++++++++++++++ ...lowActivity.java => PurchaseActivity.java} | 2 +- .../microg/vending/billing/PurchaseManager.kt | 129 ++++ .../microg/vending/billing/SettingsManager.kt | 31 + .../java/org/microg/vending/billing/Utils.kt | 340 +++++++++ .../billing/core/AcknowledgePurchaseParams.kt | 7 + .../billing/core/AcknowledgePurchaseResult.kt | 36 + .../vending/billing/core/AcquireParams.kt | 9 + .../vending/billing/core/AcquireResult.kt | 29 + .../microg/vending/billing/core/AuthData.kt | 11 + .../vending/billing/core/BuyFlowParams.kt | 15 + .../microg/vending/billing/core/ClientInfo.kt | 7 + .../billing/core/ConsumePurchaseParams.kt | 7 + .../billing/core/ConsumePurchaseResult.kt | 30 + .../vending/billing/core/DeviceEnvInfo.kt | 68 ++ .../billing/core/GetPurchaseHistoryParams.kt | 8 + .../billing/core/GetPurchaseHistoryResult.kt | 53 ++ .../billing/core/GetSkuDetailsParams.kt | 10 + .../billing/core/GetSkuDetailsResult.kt | 42 ++ .../vending/billing/core/GooglePlayApi.kt | 14 + .../vending/billing/core/HeaderProvider.kt | 40 + .../microg/vending/billing/core/HttpClient.kt | 140 ++++ .../vending/billing/core/IAPCacheManager.kt | 34 + .../microg/vending/billing/core/IAPCore.kt | 322 ++++++++ .../microg/vending/billing/core/IAPResult.kt | 11 + .../vending/billing/core/PurchaseItem.kt | 25 + .../org/microg/vending/billing/core/Utils.kt | 309 ++++++++ .../vending/billing/core/ui/UIComponents.kt | 301 ++++++++ .../vending/billing/core/ui/UIParser.kt | 705 ++++++++++++++++++ .../microg/vending/billing/core/ui/UIType.kt | 28 + .../org/microg/vending/billing/ui/Extends.kt | 161 ++++ .../org/microg/vending/billing/ui/HtmlText.kt | 278 +++++++ .../billing/ui/InAppBillingHostActivity.kt | 149 ++++ .../vending/billing/ui/PlayWebViewActivity.kt | 104 +++ .../microg/vending/billing/ui/SheetUIPage.kt | 580 ++++++++++++++ .../org/microg/vending/billing/ui/Utils.kt | 120 +++ .../vending/billing/ui/WebViewHelper.kt | 117 +++ .../billing/ui/logic/InAppBillingViewModel.kt | 310 ++++++++ .../microg/vending/billing/ui/logic/Models.kt | 38 + .../microg/vending/billing/ui/theme/Color.kt | 13 + .../microg/vending/billing/ui/theme/Theme.kt | 78 ++ .../microg/vending/billing/ui/theme/Type.kt | 34 + .../billing/ui/widgets/LoadingDialog.kt | 36 + .../billing/ui/widgets/PasswdInputDialog.kt | 182 +++++ vending-app/src/main/proto/GooglePlay.proto | 688 +++++++++++++++++ vending-app/src/main/proto/Timestamp.proto | 33 + .../main/res/drawable-v21/anim_check_mark.xml | 178 +++++ .../src/main/res/drawable-v21/google_play.xml | 16 + .../res/layout/activiy_confirm_purchase.xml | 13 + .../src/main/res/values-zh-rCN/strings.xml | 12 +- vending-app/src/main/res/values/colors.xml | 5 + vending-app/src/main/res/values/strings.xml | 12 +- vending-app/src/main/res/values/styles.xml | 33 + 83 files changed, 7775 insertions(+), 63 deletions(-) create mode 100644 play-services-core/src/main/kotlin/org/microg/gms/ui/VendingFragment.kt create mode 100644 play-services-core/src/main/kotlin/org/microg/gms/vending/VendingPreferences.kt create mode 100644 vending-app/src/huaweilh/AndroidManifest.xml create mode 100644 vending-app/src/main/aidl/com/android/vending/billing/IInAppBillingService.aidl create mode 100644 vending-app/src/main/aidl/com/google/android/gms/checkin/internal/ICheckinService.aidl create mode 100644 vending-app/src/main/java/com/android/billingclient/api/BillingClient.java create mode 100644 vending-app/src/main/java/com/android/billingclient/package-info.java create mode 100644 vending-app/src/main/java/com/android/vending/VendingPreferences.kt create mode 100644 vending-app/src/main/java/com/android/vending/billing/InAppBillingService.kt create mode 100644 vending-app/src/main/java/org/microg/vending/billing/AuthManager.kt create mode 100644 vending-app/src/main/java/org/microg/vending/billing/CheckinServiceClient.kt create mode 100644 vending-app/src/main/java/org/microg/vending/billing/Constants.kt create mode 100644 vending-app/src/main/java/org/microg/vending/billing/ContextProvider.kt create mode 100644 vending-app/src/main/java/org/microg/vending/billing/DeviceIdentifier.kt create mode 100644 vending-app/src/main/java/org/microg/vending/billing/GServices.kt create mode 100644 vending-app/src/main/java/org/microg/vending/billing/InAppBillingServiceImpl.kt rename vending-app/src/main/java/org/microg/vending/billing/{LightPurchaseFlowActivity.java => PurchaseActivity.java} (90%) create mode 100644 vending-app/src/main/java/org/microg/vending/billing/PurchaseManager.kt create mode 100644 vending-app/src/main/java/org/microg/vending/billing/SettingsManager.kt create mode 100644 vending-app/src/main/java/org/microg/vending/billing/Utils.kt create mode 100644 vending-app/src/main/java/org/microg/vending/billing/core/AcknowledgePurchaseParams.kt create mode 100644 vending-app/src/main/java/org/microg/vending/billing/core/AcknowledgePurchaseResult.kt create mode 100644 vending-app/src/main/java/org/microg/vending/billing/core/AcquireParams.kt create mode 100644 vending-app/src/main/java/org/microg/vending/billing/core/AcquireResult.kt create mode 100644 vending-app/src/main/java/org/microg/vending/billing/core/AuthData.kt create mode 100644 vending-app/src/main/java/org/microg/vending/billing/core/BuyFlowParams.kt create mode 100644 vending-app/src/main/java/org/microg/vending/billing/core/ClientInfo.kt create mode 100644 vending-app/src/main/java/org/microg/vending/billing/core/ConsumePurchaseParams.kt create mode 100644 vending-app/src/main/java/org/microg/vending/billing/core/ConsumePurchaseResult.kt create mode 100644 vending-app/src/main/java/org/microg/vending/billing/core/DeviceEnvInfo.kt create mode 100644 vending-app/src/main/java/org/microg/vending/billing/core/GetPurchaseHistoryParams.kt create mode 100644 vending-app/src/main/java/org/microg/vending/billing/core/GetPurchaseHistoryResult.kt create mode 100644 vending-app/src/main/java/org/microg/vending/billing/core/GetSkuDetailsParams.kt create mode 100644 vending-app/src/main/java/org/microg/vending/billing/core/GetSkuDetailsResult.kt create mode 100644 vending-app/src/main/java/org/microg/vending/billing/core/GooglePlayApi.kt create mode 100644 vending-app/src/main/java/org/microg/vending/billing/core/HeaderProvider.kt create mode 100644 vending-app/src/main/java/org/microg/vending/billing/core/HttpClient.kt create mode 100644 vending-app/src/main/java/org/microg/vending/billing/core/IAPCacheManager.kt create mode 100644 vending-app/src/main/java/org/microg/vending/billing/core/IAPCore.kt create mode 100644 vending-app/src/main/java/org/microg/vending/billing/core/IAPResult.kt create mode 100644 vending-app/src/main/java/org/microg/vending/billing/core/PurchaseItem.kt create mode 100644 vending-app/src/main/java/org/microg/vending/billing/core/Utils.kt create mode 100644 vending-app/src/main/java/org/microg/vending/billing/core/ui/UIComponents.kt create mode 100644 vending-app/src/main/java/org/microg/vending/billing/core/ui/UIParser.kt create mode 100644 vending-app/src/main/java/org/microg/vending/billing/core/ui/UIType.kt create mode 100644 vending-app/src/main/java/org/microg/vending/billing/ui/Extends.kt create mode 100644 vending-app/src/main/java/org/microg/vending/billing/ui/HtmlText.kt create mode 100644 vending-app/src/main/java/org/microg/vending/billing/ui/InAppBillingHostActivity.kt create mode 100644 vending-app/src/main/java/org/microg/vending/billing/ui/PlayWebViewActivity.kt create mode 100644 vending-app/src/main/java/org/microg/vending/billing/ui/SheetUIPage.kt create mode 100644 vending-app/src/main/java/org/microg/vending/billing/ui/Utils.kt create mode 100644 vending-app/src/main/java/org/microg/vending/billing/ui/WebViewHelper.kt create mode 100644 vending-app/src/main/java/org/microg/vending/billing/ui/logic/InAppBillingViewModel.kt create mode 100644 vending-app/src/main/java/org/microg/vending/billing/ui/logic/Models.kt create mode 100644 vending-app/src/main/java/org/microg/vending/billing/ui/theme/Color.kt create mode 100644 vending-app/src/main/java/org/microg/vending/billing/ui/theme/Theme.kt create mode 100644 vending-app/src/main/java/org/microg/vending/billing/ui/theme/Type.kt create mode 100644 vending-app/src/main/java/org/microg/vending/billing/ui/widgets/LoadingDialog.kt create mode 100644 vending-app/src/main/java/org/microg/vending/billing/ui/widgets/PasswdInputDialog.kt create mode 100644 vending-app/src/main/proto/GooglePlay.proto create mode 100644 vending-app/src/main/proto/Timestamp.proto create mode 100644 vending-app/src/main/res/drawable-v21/anim_check_mark.xml create mode 100644 vending-app/src/main/res/drawable-v21/google_play.xml create mode 100644 vending-app/src/main/res/layout/activiy_confirm_purchase.xml create mode 100644 vending-app/src/main/res/values/colors.xml create mode 100644 vending-app/src/main/res/values/styles.xml diff --git a/firebase-auth/core/src/main/kotlin/org/microg/gms/firebase/auth/IdentityToolkitClient.kt b/firebase-auth/core/src/main/kotlin/org/microg/gms/firebase/auth/IdentityToolkitClient.kt index 2b8608e1..ca391a6d 100644 --- a/firebase-auth/core/src/main/kotlin/org/microg/gms/firebase/auth/IdentityToolkitClient.kt +++ b/firebase-auth/core/src/main/kotlin/org/microg/gms/firebase/auth/IdentityToolkitClient.kt @@ -127,10 +127,10 @@ class IdentityToolkitClient(context: Context, private val apiKey: String, privat suspend fun getTokenByRefreshToken(refreshToken: String): JSONObject = suspendCoroutine { continuation -> queue.add(object : JsonRequest(POST, buildStsUrl("token"), "grant_type=refresh_token&refresh_token=$refreshToken", { continuation.resume(it) }, { continuation.resumeWithException(RuntimeException(it)) }) { - override fun parseNetworkResponse(response: NetworkResponse?): Response { + override fun parseNetworkResponse(response: NetworkResponse): Response { return try { - val jsonString = String(response!!.data, Charset.forName(HttpHeaderParser.parseCharset(response!!.headers, PROTOCOL_CHARSET))) - Response.success(JSONObject(jsonString), HttpHeaderParser.parseCacheHeaders(response)) + val jsonString = String(response.data, Charset.forName(HttpHeaderParser.parseCharset(response.headers, PROTOCOL_CHARSET))) + Response.success(JSONObject(jsonString), null) } catch (e: UnsupportedEncodingException) { Response.error(ParseError(e)) } catch (je: JSONException) { diff --git a/play-services-base-core/src/main/kotlin/org/microg/gms/profile/ProfileManager.kt b/play-services-base-core/src/main/kotlin/org/microg/gms/profile/ProfileManager.kt index 0cbc69b8..abcdb8d1 100644 --- a/play-services-base-core/src/main/kotlin/org/microg/gms/profile/ProfileManager.kt +++ b/play-services-base-core/src/main/kotlin/org/microg/gms/profile/ProfileManager.kt @@ -7,7 +7,10 @@ package org.microg.gms.profile import android.annotation.SuppressLint import android.content.Context +import android.content.pm.PackageManager import android.content.res.XmlResourceParser +import android.net.Uri +import android.os.Bundle import android.util.Log import org.microg.gms.utils.FileXmlResourceParser import org.microg.mgms.settings.SettingsContract @@ -20,11 +23,13 @@ import kotlin.random.Random @Suppress("Warnings") object ProfileManager { private const val TAG = "ProfileManager" + const val META_DATA_KEY_SOURCE_PACKAGE = "org.microg.gms.profile:source-package" const val PROFILE_REAL = "real" const val PROFILE_AUTO = "auto" const val PROFILE_NATIVE = "native" const val PROFILE_USER = "user" const val PROFILE_SYSTEM = "system" + const val PROFILE_REMOTE = "remote" private var activeProfile: String? = null @@ -118,6 +123,17 @@ object ProfileManager { } } + private fun getRemoteProfileData(context: Context, packageName: String): Map { + val data = mutableMapOf() + val cursor = context.contentResolver.query(Uri.parse("content://${packageName}.microg.profile"), null, null, null, null) + cursor?.use { + while (cursor.moveToNext()) { + data[cursor.getString(0)] = cursor.getString(1) + } + } + return data + } + private fun getProfile(context: Context) = getConfiguredProfile(context).let { if (it != PROFILE_AUTO) it else getAutoProfile(context) } private fun getSerialFromSettings(context: Context): String? = Profile.getContentUri(context)?.let { @@ -283,6 +299,17 @@ object ProfileManager { activeProfile = profile } + private fun applyRemoteProfile(context: Context, packageName: String) { + val profileData = getRemoteProfileData(context, packageName) + if (Log.isLoggable(TAG, Log.VERBOSE)) { + for ((key, value) in profileData) { + Log.v(TAG, "") + } + } + applyProfileData(profileData) + activeProfile = PROFILE_REMOTE + } + fun getProfileName(context: Context, profile: String): String? = getProfileName { getProfileXml(context, profile) } private fun getProfileName(parserCreator: () -> XmlResourceParser?): String? = parserCreator()?.use { @@ -328,14 +355,23 @@ object ProfileManager { @JvmStatic fun ensureInitialized(context: Context) { + val metaData = runCatching { context.packageManager.getApplicationInfo(context.packageName, PackageManager.GET_META_DATA).metaData }.getOrNull() ?: Bundle.EMPTY synchronized(this) { try { - val profile = getProfile(context) - if (activeProfile == profile) return - applyProfile(context, profile) + if (metaData.containsKey(META_DATA_KEY_SOURCE_PACKAGE)) { + if (activeProfile != PROFILE_REMOTE) { + val packageName = metaData.getString(META_DATA_KEY_SOURCE_PACKAGE)!! + applyRemoteProfile(context, packageName) + } + } else { + val profile = getProfile(context) + if (activeProfile == profile) return + applyProfile(context, profile) + } } catch (e: Exception) { Log.w(TAG, e) } + Unit } } } diff --git a/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsContract.kt b/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsContract.kt index c2b37fdf..9791f4f0 100644 --- a/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsContract.kt +++ b/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsContract.kt @@ -7,12 +7,19 @@ package org.microg.gms.settings import android.content.ContentValues import android.content.Context +import android.content.pm.PackageManager import android.database.Cursor import android.net.Uri import android.os.Binder +import android.os.Bundle object SettingsContract { - fun getAuthority(context: Context) = "${context.packageName}.microg.settings" + const val META_DATA_KEY_SOURCE_PACKAGE = "org.microg.gms.settings:source-package" + fun getAuthority(context: Context): String { + val metaData = runCatching { context.packageManager.getApplicationInfo(context.packageName, PackageManager.GET_META_DATA).metaData }.getOrNull() ?: Bundle.EMPTY + val sourcePackage = metaData.getString(META_DATA_KEY_SOURCE_PACKAGE, context.packageName) + return "${sourcePackage}.microg.settings" + } fun getAuthorityUri(context: Context): Uri = Uri.parse("content://${getAuthority(context)}") object CheckIn { @@ -179,9 +186,11 @@ object SettingsContract { fun getContentType(context: Context) = "vnd.android.cursor.item/vnd.${getAuthority(context)}.$id" const val LICENSING = "vending_licensing" + const val BILLING = "vending_billing" val PROJECTION = arrayOf( - LICENSING + LICENSING, + BILLING, ) } diff --git a/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsProvider.kt b/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsProvider.kt index 065c7079..dfb027d9 100644 --- a/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsProvider.kt +++ b/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsProvider.kt @@ -343,6 +343,7 @@ class SettingsProvider : ContentProvider() { private fun queryVending(p: Array): Cursor = MatrixCursor(p).addRow(p) { key -> when (key) { Vending.LICENSING -> getSettingsBoolean(key, false) + Vending.BILLING -> getSettingsBoolean(key, false) else -> throw IllegalArgumentException("Unknown key: $key") } } @@ -353,6 +354,7 @@ class SettingsProvider : ContentProvider() { values.valueSet().forEach { (key, value) -> when (key) { Vending.LICENSING -> editor.putBoolean(key, value as Boolean) + Vending.BILLING -> editor.putBoolean(key, value as Boolean) else -> throw IllegalArgumentException("Unknown key: $key") } } diff --git a/play-services-core/src/huawei/AndroidManifest.xml b/play-services-core/src/huawei/AndroidManifest.xml index f6b7f6d1..cfe4c5bc 100644 --- a/play-services-core/src/huawei/AndroidManifest.xml +++ b/play-services-core/src/huawei/AndroidManifest.xml @@ -28,5 +28,11 @@ + + \ No newline at end of file diff --git a/play-services-core/src/huaweilh/AndroidManifest.xml b/play-services-core/src/huaweilh/AndroidManifest.xml index b2359ec5..244b93bf 100644 --- a/play-services-core/src/huaweilh/AndroidManifest.xml +++ b/play-services-core/src/huaweilh/AndroidManifest.xml @@ -28,6 +28,12 @@ + + (PREF_GCM)?.setSummary(R.string.service_status_disabled_short) } +<<<<<<< HEAD findPreference(PREF_CHECKIN)?.setSummary(if (CheckinPrefs.isEnabled(context) == true) R.string.service_status_enabled_short else R.string.service_status_disabled_short) +======= + findPreference(PREF_CHECKIN)!!.setSummary(if (CheckinPreferences.isEnabled(requireContext())) org.microg.gms.base.core.R.string.service_status_enabled_short else org.microg.gms.base.core.R.string.service_status_disabled_short) + findPreference(PREF_SNET)!!.setSummary(if (SafetyNetPreferences.isEnabled(requireContext())) org.microg.gms.base.core.R.string.service_status_enabled_short else org.microg.gms.base.core.R.string.service_status_disabled_short) + + lifecycleScope.launchWhenResumed { + val entries = getAllSettingsProviders(requireContext()).flatMap { it.getEntriesDynamic(requireContext()) } + for (preference in createdPreferences) { + if (!entries.any { it.key == preference.key }) preference.isVisible = false + } + for (entry in entries) { + val preference = createdPreferences.find { it.key == entry.key } + if (preference != null) preference.fillFromEntry(entry) + else entry.createPreference() + } + } +>>>>>>> d4d7713f (Add experimental support for in app billing) } companion object { diff --git a/play-services-core/src/main/kotlin/org/microg/gms/ui/VendingFragment.kt b/play-services-core/src/main/kotlin/org/microg/gms/ui/VendingFragment.kt new file mode 100644 index 00000000..0d0308cd --- /dev/null +++ b/play-services-core/src/main/kotlin/org/microg/gms/ui/VendingFragment.kt @@ -0,0 +1,69 @@ +/* + * SPDX-FileCopyrightText: 2023, e Foundation + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.ui + +import android.annotation.SuppressLint +import android.os.Bundle +import androidx.lifecycle.lifecycleScope +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import androidx.preference.TwoStatePreference +import com.google.android.gms.R +import org.microg.gms.vending.VendingPreferences + +class VendingFragment : PreferenceFragmentCompat() { + private lateinit var licensingEnabled: TwoStatePreference + private lateinit var iapEnable: TwoStatePreference + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + addPreferencesFromResource(R.xml.preferences_vending) + } + + @SuppressLint("RestrictedApi") + override fun onBindPreferences() { + licensingEnabled = preferenceScreen.findPreference(PREF_LICENSING_ENABLED) ?: licensingEnabled + licensingEnabled.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> + val appContext = requireContext().applicationContext + lifecycleScope.launchWhenResumed { + if (newValue is Boolean) { + VendingPreferences.setLicensingEnabled(appContext, newValue) + } + updateContent() + } + true + } + + iapEnable = preferenceScreen.findPreference(PREF_IAP_ENABLED) ?: iapEnable + iapEnable.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> + val appContext = requireContext().applicationContext + lifecycleScope.launchWhenResumed { + if (newValue is Boolean) { + VendingPreferences.setBillingEnabled(appContext, newValue) + } + updateContent() + } + true + } + } + + override fun onResume() { + super.onResume() + updateContent() + } + + private fun updateContent() { + val appContext = requireContext().applicationContext + lifecycleScope.launchWhenResumed { + licensingEnabled.isChecked = VendingPreferences.isLicensingEnabled(appContext) + iapEnable.isChecked = VendingPreferences.isBillingEnabled(appContext) + } + } + + companion object { + const val PREF_LICENSING_ENABLED = "vending_licensing" + const val PREF_IAP_ENABLED = "vending_iap" + } +} \ No newline at end of file diff --git a/play-services-core/src/main/kotlin/org/microg/gms/vending/VendingPreferences.kt b/play-services-core/src/main/kotlin/org/microg/gms/vending/VendingPreferences.kt new file mode 100644 index 00000000..46b5cb83 --- /dev/null +++ b/play-services-core/src/main/kotlin/org/microg/gms/vending/VendingPreferences.kt @@ -0,0 +1,41 @@ +/* + * SPDX-FileCopyrightText: 2023, e Foundation + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.vending + +import android.content.Context +import org.microg.gms.settings.SettingsContract + +object VendingPreferences { + @JvmStatic + fun isLicensingEnabled(context: Context): Boolean { + val projection = arrayOf(SettingsContract.Vending.LICENSING) + return SettingsContract.getSettings(context, SettingsContract.Vending.getContentUri(context), projection) { c -> + c.getInt(0) != 0 + } + } + + @JvmStatic + fun setLicensingEnabled(context: Context, enabled: Boolean) { + SettingsContract.setSettings(context, SettingsContract.Vending.getContentUri(context)) { + put(SettingsContract.Vending.LICENSING, enabled) + } + } + + @JvmStatic + fun isBillingEnabled(context: Context): Boolean { + val projection = arrayOf(SettingsContract.Vending.BILLING) + return SettingsContract.getSettings(context, SettingsContract.Vending.getContentUri(context), projection) { c -> + c.getInt(0) != 0 + } + } + + @JvmStatic + fun setBillingEnabled(context: Context, enabled: Boolean) { + SettingsContract.setSettings(context, SettingsContract.Vending.getContentUri(context)) { + put(SettingsContract.Vending.BILLING, enabled) + } + } +} \ No newline at end of file diff --git a/play-services-core/src/main/res/values-zh-rCN/strings.xml b/play-services-core/src/main/res/values-zh-rCN/strings.xml index 3bd2223a..3945435b 100644 --- a/play-services-core/src/main/res/values-zh-rCN/strings.xml +++ b/play-services-core/src/main/res/values-zh-rCN/strings.xml @@ -281,6 +281,8 @@ microG GmsCore 内置一套自由的 SafetyNet 实现,但是官方服务器要 允许并共享 反馈功能不可用 备份功能不可用 + 使用 Google 应用内购买服务 + 启用后一些应用可以通过Google 应用内购买服务完成应用内服务购买订阅 <<<<<<< HEAD >>>>>>> 64670de2 (Improve dummy for feedback service (#2185)) diff --git a/play-services-core/src/main/res/values-zh-rTW/strings.xml b/play-services-core/src/main/res/values-zh-rTW/strings.xml index 0c4fb76c..88b17dff 100644 --- a/play-services-core/src/main/res/values-zh-rTW/strings.xml +++ b/play-services-core/src/main/res/values-zh-rTW/strings.xml @@ -253,5 +253,10 @@ Vanced+ microG 會把 Google 帳戶名稱以外的識別位 microG GmsCore包含了SafetyNet的自由執行,但官方伺服器需要SafetyNet請求經非自由軟體性質的DroidGuard系統簽名。 測試SafetyNet驗證 +<<<<<<< HEAD >>>>>>> 9f8323d8 (Translated using Weblate (Chinese (Traditional))) +======= + 啟用後一些應用程式可以透過Google 應用程式內購買服務完成應用程式內服務購買訂閱 + +>>>>>>> d4d7713f (Add experimental support for in app billing) diff --git a/play-services-core/src/main/res/values/strings.xml b/play-services-core/src/main/res/values/strings.xml index 4147a703..6d901296 100755 --- a/play-services-core/src/main/res/values/strings.xml +++ b/play-services-core/src/main/res/values/strings.xml @@ -240,11 +240,21 @@ This can take a couple of minutes." Licensing off Licensing on + Google Play Licensing Answer license verification requests Some apps require verification that you have purchased them on Google Play. When requested by an app, microG can download a proof of purchase from Google. If disabled, or if no Google account is added, requests for license verification are ignored. Feedback currently not possible Backup currently not possible +<<<<<<< HEAD >>>>>>> 64670de2 (Improve dummy for feedback service (#2185)) +======= + Google Play Billing + Handle billing requests + Once enabled, some apps can complete purchases or start subscriptions through Google\'s Play Billing service. + This feature is experimental and may lead to loss of money. You have been warned. + Some apps may require you to also enable license verification to verify your purchases. + +>>>>>>> d4d7713f (Add experimental support for in app billing) diff --git a/play-services-core/src/main/res/xml/preferences_vending.xml b/play-services-core/src/main/res/xml/preferences_vending.xml index 2d7d0f03..91922f74 100644 --- a/play-services-core/src/main/res/xml/preferences_vending.xml +++ b/play-services-core/src/main/res/xml/preferences_vending.xml @@ -1,20 +1,35 @@ - - - + + + + - + + + + + android:summary="@string/pref_vending_billing_note_licensing" /> diff --git a/vending-app/build.gradle b/vending-app/build.gradle index f57b700b..7347a81b 100644 --- a/vending-app/build.gradle +++ b/vending-app/build.gradle @@ -66,6 +66,8 @@ android { buildFeatures { aidl = true + buildConfig = true + compose true } lintOptions { @@ -80,6 +82,9 @@ android { kotlinOptions { jvmTarget = 1.8 } + composeOptions { + kotlinCompilerExtensionVersion '1.5.10' + } } dependencies { @@ -89,6 +94,31 @@ dependencies { implementation "com.squareup.wire:wire-runtime:$wireVersion" implementation "com.android.volley:volley:$volleyVersion" + + implementation "androidx.webkit:webkit:$webkitVersion" + + implementation "com.squareup.wire:wire-grpc-client:$wireVersion" + + //compose + implementation platform('androidx.compose:compose-bom:2022.10.00') + implementation 'androidx.compose.ui:ui' + implementation 'androidx.compose.material3:material3' + implementation 'androidx.compose.animation:animation-graphics' + implementation 'androidx.activity:activity-compose:1.7.2' + implementation("io.coil-kt:coil-compose:2.4.0") + implementation("io.coil-kt:coil-svg:2.2.2") + implementation "com.google.android.material:material:$materialVersion" + implementation "com.google.accompanist:accompanist-systemuicontroller:0.28.0" + + //droidguard + implementation project(':play-services-droidguard') + + //androidx + implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion" + implementation "androidx.core:core-ktx:$coreVersion" + implementation "androidx.appcompat:appcompat:$appcompatVersion" + implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.2' + implementation "androidx.preference:preference-ktx:$preferenceVersion" } wire { diff --git a/vending-app/src/huaweilh/AndroidManifest.xml b/vending-app/src/huaweilh/AndroidManifest.xml new file mode 100644 index 00000000..77c053a6 --- /dev/null +++ b/vending-app/src/huaweilh/AndroidManifest.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vending-app/src/main/AndroidManifest.xml b/vending-app/src/main/AndroidManifest.xml index b3ed457f..10adb52d 100644 --- a/vending-app/src/main/AndroidManifest.xml +++ b/vending-app/src/main/AndroidManifest.xml @@ -12,10 +12,13 @@ - - + + + + + + + + + - - - - - - - - - + + + + + + + + + + + + diff --git a/vending-app/src/main/aidl/com/android/vending/billing/IInAppBillingService.aidl b/vending-app/src/main/aidl/com/android/vending/billing/IInAppBillingService.aidl new file mode 100644 index 00000000..0342932f --- /dev/null +++ b/vending-app/src/main/aidl/com/android/vending/billing/IInAppBillingService.aidl @@ -0,0 +1,370 @@ +package com.android.vending.billing; + +import android.os.Bundle; + +/** + * InAppBillingService is the service that provides in-app billing version 3 and beyond. + * This service provides the following features: + * 1. Provides a new API to get details of in-app items published for the app including + * price, type, title and description. + * 2. The purchase flow is synchronous and purchase information is available immediately + * after it completes. + * 3. Purchase information of in-app purchases is maintained within the Google Play system + * till the purchase is consumed. + * 4. An API to consume a purchase of an inapp item. All purchases of one-time + * in-app items are consumable and thereafter can be purchased again. + * 5. An API to get current purchases of the user immediately. This will not contain any + * consumed purchases. + * + * All calls will give a response code with the following possible values + * RESULT_OK = 0 - success + * RESULT_USER_CANCELED = 1 - User pressed back or canceled a dialog + * RESULT_SERVICE_UNAVAILABLE = 2 - The network connection is down + * RESULT_BILLING_UNAVAILABLE = 3 - This billing API version is not supported for the type requested + * RESULT_ITEM_UNAVAILABLE = 4 - Requested SKU is not available for purchase + * RESULT_DEVELOPER_ERROR = 5 - Invalid arguments provided to the API + * RESULT_ERROR = 6 - Fatal error during the API action + * RESULT_ITEM_ALREADY_OWNED = 7 - Failure to purchase since item is already owned + * RESULT_ITEM_NOT_OWNED = 8 - Failure to consume since item is not owned + */ +interface IInAppBillingService { + /** + * Checks support for the requested billing API version, package and in-app type. + * Minimum API version supported by this interface is 3. + * @param apiVersion billing API version that the app is using + * @param packageName the package name of the calling app + * @param type type of the in-app item being purchased ("inapp" for one-time purchases + * and "subs" for subscriptions) + * @return RESULT_OK(0) on success and appropriate response code on failures. + */ + int isBillingSupported(int apiVersion, String packageName, String type) = 0; + + /** + * Provides details of a list of SKUs + * Given a list of SKUs of a valid type in the skusBundle, this returns a bundle + * with a list JSON strings containing the productId, price, title and description. + * This API can be called with a maximum of 20 SKUs. + * @param apiVersion billing API version that the app is using + * @param packageName the package name of the calling app + * @param type of the in-app items ("inapp" for one-time purchases + * and "subs" for subscriptions) + * @param skusBundle bundle containing a StringArrayList of SKUs with key "ITEM_ID_LIST" + * @return Bundle containing the following key-value pairs + * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response codes + * on failures. + * "DETAILS_LIST" with a StringArrayList containing purchase information + * in JSON format similar to: + * '{ "productId" : "exampleSku", + * "type" : "inapp", + * "price" : "$5.00", + * "price_currency": "USD", + * "price_amount_micros": 5000000, + * "title : "Example Title", + * "description" : "This is an example description" }' + */ + Bundle getSkuDetails(int apiVersion, String packageName, String type, in Bundle skusBundle) = 1; + + /** + * Returns a pending intent to launch the purchase flow for an in-app item by providing a SKU, + * the type, a unique purchase token and an optional developer payload. + * @param apiVersion billing API version that the app is using + * @param packageName package name of the calling app + * @param sku the SKU of the in-app item as published in the developer console + * @param type of the in-app item being purchased ("inapp" for one-time purchases + * and "subs" for subscriptions) + * @param developerPayload optional argument to be sent back with the purchase information + * @return Bundle containing the following key-value pairs + * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response codes + * on failures. + * "BUY_INTENT" - PendingIntent to start the purchase flow + * + * The Pending intent should be launched with startIntentSenderForResult. When purchase flow + * has completed, the onActivityResult() will give a resultCode of OK or CANCELED. + * If the purchase is successful, the result data will contain the following key-value pairs + * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response + * codes on failures. + * "INAPP_PURCHASE_DATA" - String in JSON format similar to + * '{"orderId":"12999763169054705758.1371079406387615", + * "packageName":"com.example.app", + * "productId":"exampleSku", + * "purchaseTime":1345678900000, + * "purchaseToken" : "122333444455555", + * "developerPayload":"example developer payload" }' + * "INAPP_DATA_SIGNATURE" - String containing the signature of the purchase data that + * was signed with the private key of the developer + */ + Bundle getBuyIntent(int apiVersion, String packageName, String sku, String type, + String developerPayload) = 2; + + /** + * Returns the current SKUs owned by the user of the type and package name specified along with + * purchase information and a signature of the data to be validated. + * This will return all SKUs that have been purchased in V3 and managed items purchased using + * V1 and V2 that have not been consumed. + * @param apiVersion billing API version that the app is using + * @param packageName package name of the calling app + * @param type of the in-app items being requested ("inapp" for one-time purchases + * and "subs" for subscriptions) + * @param continuationToken to be set as null for the first call, if the number of owned + * skus are too many, a continuationToken is returned in the response bundle. + * This method can be called again with the continuation token to get the next set of + * owned skus. + * @return Bundle containing the following key-value pairs + * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response codes + on failures. + * "INAPP_PURCHASE_ITEM_LIST" - StringArrayList containing the list of SKUs + * "INAPP_PURCHASE_DATA_LIST" - StringArrayList containing the purchase information + * "INAPP_DATA_SIGNATURE_LIST"- StringArrayList containing the signatures + * of the purchase information + * "INAPP_CONTINUATION_TOKEN" - String containing a continuation token for the + * next set of in-app purchases. Only set if the + * user has more owned skus than the current list. + */ + Bundle getPurchases(int apiVersion, String packageName, String type, String continuationToken) = 3; + + /** + * Consume the last purchase of the given SKU. This will result in this item being removed + * from all subsequent responses to getPurchases() and allow re-purchase of this item. + * @param apiVersion billing API version that the app is using + * @param packageName package name of the calling app + * @param purchaseToken token in the purchase information JSON that identifies the purchase + * to be consumed + * @return RESULT_OK(0) if consumption succeeded, appropriate response codes on failures. + */ + int consumePurchase(int apiVersion, String packageName, String purchaseToken) = 4; + + int isPromoEligible(int apiVersion, String packageName, String type) = 5; + + /** + * Returns a pending intent to launch the purchase flow for upgrading or downgrading a + * subscription. The existing owned SKU(s) should be provided along with the new SKU that + * the user is upgrading or downgrading to. + * @param apiVersion billing API version that the app is using, must be 5 or later + * @param packageName package name of the calling app + * @param oldSkus the SKU(s) that the user is upgrading or downgrading from, + * if null or empty this method will behave like {@link #getBuyIntent} + * @param newSku the SKU that the user is upgrading or downgrading to + * @param type of the item being purchased, currently must be "subs" + * @param developerPayload optional argument to be sent back with the purchase information + * @return Bundle containing the following key-value pairs + * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response codes + * on failures. + * "BUY_INTENT" - PendingIntent to start the purchase flow + * + * The Pending intent should be launched with startIntentSenderForResult. When purchase flow + * has completed, the onActivityResult() will give a resultCode of OK or CANCELED. + * If the purchase is successful, the result data will contain the following key-value pairs + * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response + * codes on failures. + * "INAPP_PURCHASE_DATA" - String in JSON format similar to + * '{"orderId":"12999763169054705758.1371079406387615", + * "packageName":"com.example.app", + * "productId":"exampleSku", + * "purchaseTime":1345678900000, + * "purchaseToken" : "122333444455555", + * "developerPayload":"example developer payload" }' + * "INAPP_DATA_SIGNATURE" - String containing the signature of the purchase data that + * was signed with the private key of the developer + */ + Bundle getBuyIntentToReplaceSkus(int apiVersion, String packageName, + in List oldSkus, String newSku, String type, String developerPayload) = 6; + + /** + * Returns a pending intent to launch the purchase flow for an in-app item. This method is + * a variant of the {@link #getBuyIntent} method and takes an additional {@code extraParams} + * parameter. This parameter is a Bundle of optional keys and values that affect the + * operation of the method. + * @param apiVersion billing API version that the app is using, must be 6 or later + * @param packageName package name of the calling app + * @param sku the SKU of the in-app item as published in the developer console + * @param type of the in-app item being purchased ("inapp" for one-time purchases + * and "subs" for subscriptions) + * @param developerPayload optional argument to be sent back with the purchase information + * @extraParams a Bundle with the following optional keys: + * "skusToReplace" - List - an optional list of SKUs that the user is + * upgrading or downgrading from. + * Pass this field if the purchase is upgrading or downgrading + * existing subscriptions. + * The specified SKUs are replaced with the SKUs that the user is + * purchasing. Google Play replaces the specified SKUs at the start of + * the next billing cycle. + * "replaceSkusProration" - Boolean - whether the user should be credited for any unused + * subscription time on the SKUs they are upgrading or downgrading. + * If you set this field to true, Google Play swaps out the old SKUs + * and credits the user with the unused value of their subscription + * time on a pro-rated basis. + * Google Play applies this credit to the new subscription, and does + * not begin billing the user for the new subscription until after + * the credit is used up. + * If you set this field to false, the user does not receive credit for + * any unused subscription time and the recurrence date does not + * change. + * Default value is true. Ignored if you do not pass skusToReplace. + * "accountId" - String - an optional obfuscated string that is uniquely + * associated with the user's account in your app. + * If you pass this value, Google Play can use it to detect irregular + * activity, such as many devices making purchases on the same + * account in a short period of time. + * Do not use the developer ID or the user's Google ID for this field. + * In addition, this field should not contain the user's ID in + * cleartext. + * We recommend that you use a one-way hash to generate a string from + * the user's ID, and store the hashed string in this field. + * "vr" - Boolean - an optional flag indicating whether the returned intent + * should start a VR purchase flow. The apiVersion must also be 7 or + * later to use this flag. + */ + Bundle getBuyIntentExtraParams(int apiVersion, String packageName, String sku, + String type, String developerPayload, in Bundle extraParams) = 7; + + /** + * Returns the most recent purchase made by the user for each SKU, even if that purchase is + * expired, canceled, or consumed. + * @param apiVersion billing API version that the app is using, must be 6 or later + * @param packageName package name of the calling app + * @param type of the in-app items being requested ("inapp" for one-time purchases + * and "subs" for subscriptions) + * @param continuationToken to be set as null for the first call, if the number of owned + * skus is too large, a continuationToken is returned in the response bundle. + * This method can be called again with the continuation token to get the next set of + * owned skus. + * @param extraParams a Bundle with the following optional keys: + * "playBillingLibraryVersion" - String + * "enablePendingPurchases" - Boolean + * @return Bundle containing the following key-value pairs + * "RESPONSE_CODE" with int value: RESULT_OK(0) if success, + * {@link IabHelper#BILLING_RESPONSE_RESULT_*} response codes on failures. + * "DEBUG_MESSAGE" - String + * "INAPP_PURCHASE_ITEM_LIST" - ArrayList containing the list of SKUs + * "INAPP_PURCHASE_DATA_LIST" - ArrayList containing the purchase information + * "INAPP_DATA_SIGNATURE_LIST"- ArrayList containing the signatures + * of the purchase information + * "INAPP_CONTINUATION_TOKEN" - String containing a continuation token for the + * next set of in-app purchases. Only set if the + * user has more owned skus than the current list. + */ + Bundle getPurchaseHistory(int apiVersion, String packageName, String type, + String continuationToken, in Bundle extraParams) = 8; + + /** + * This method is a variant of {@link #isBillingSupported}} that takes an additional + * {@code extraParams} parameter. + * @param apiVersion billing API version that the app is using, must be 7 or later + * @param packageName package name of the calling app + * @param type of the in-app item being purchased ("inapp" for one-time purchases and "subs" + * for subscriptions) + * @param extraParams a Bundle with the following optional keys: + * "vr" - Boolean - an optional flag to indicate whether {link #getBuyIntentExtraParams} + * supports returning a VR purchase flow. + * @return RESULT_OK(0) on success and appropriate response code on failures. + */ + int isBillingSupportedExtraParams(int apiVersion, String packageName, String type, in Bundle extraParams) = 9; + + /** + * This method is a variant of {@link #getPurchases}} that takes an additional + * {@code extraParams} parameter. + * @param apiVersion billing API version that the app is using, must be 9 or later + * @param packageName package name of the calling app + * @param type of the in-app items being requested ("inapp" for one-time purchases + * and "subs" for subscriptions) + * @param continuationToken to be set as null for the first call, if the number of owned + * skus are too many, a continuationToken is returned in the response bundle. + * This method can be called again with the continuation token to get the next set of + * owned skus. + * @param extraParams a Bundle with the following optional keys: + * "playBillingLibraryVersion" - String + * "enablePendingPurchases" - Boolean + * @return Bundle containing the following key-value pairs + * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response codes + on failures. + * "DEBUG_MESSAGE" - String + * "INAPP_PURCHASE_ITEM_LIST" - StringArrayList containing the list of SKUs + * "INAPP_PURCHASE_DATA_LIST" - StringArrayList containing the purchase information + * "INAPP_DATA_SIGNATURE_LIST"- StringArrayList containing the signatures + * of the purchase information + * "INAPP_CONTINUATION_TOKEN" - String containing a continuation token for the + * next set of in-app purchases. Only set if the + * user has more owned skus than the current list. + */ + Bundle getPurchasesExtraParams(int apiVersion, String packageName, String type, String continuationToken, in Bundle extraParams) = 10; + + /** + * This method is a variant of {@link #consumePurchase}} that takes an additional + * {@code extraParams} parameter. + * @param apiVersion billing API version that the app is using, must be 9 or later + * @param packageName package name of the calling app + * @param purchaseToken token in the purchase information JSON that identifies the purchase + * to be consumed + * @param extraParams a Bundle with the following optional keys: + * "playBillingLibraryVersion" - String + * @return Bundle containing the following key-value pairs + * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response codes + * on failures. + * "DEBUG_MESSAGE" - String + */ + Bundle consumePurchaseExtraParams(int apiVersion, String packageName, String purchaseToken, in Bundle extraParams) = 11; + + Bundle getPriceChangeConfirmationIntent(int apiVersion, String packageName, String sku, String type, in Bundle extraParams) = 800; + + /** + * This method is a variant of {@link #getSkuDetails}} that takes an additional + * {@code extraParams} parameter. + * @param apiVersion billing API version that the app is using, must be 9 or later + * @param packageName package name of the calling app + * @param type of the in-app item being purchased ("inapp" for one-time purchases and "subs" + * for subscriptions) + * @param skusBundle bundle containing a StringArrayList of SKUs with key "ITEM_ID_LIST" + * @param extraParams a Bundle with the following optional keys: + * "SKU_DETAILS_RESPONSE_FORMAT" - String + * "SKU_OFFER_ID_TOKEN_LIST" - ArrayList + * "SKU_SERIALIZED_DOCID_LIST" - ArrayList + * "accountName" - String + * "playBillingLibraryVersion" - String + * "enablePendingPurchases" - Boolean + * @return Bundle containing the following key-value pairs + * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response codes + * on failures. + * "DEBUG_MESSAGE" - String + * "DETAILS_LIST" with a StringArrayList containing purchase information + * in JSON format similar to: + * '{ "productId" : "exampleSku", + * "type" : "inapp", + * "price" : "$5.00", + * "price_currency": "USD", + * "price_amount_micros": 5000000, + * "title : "Example Title", + * "description" : "This is an example description" }' + */ + Bundle getSkuDetailsExtraParams(int apiVersion, String packageName, String type, in Bundle skuBundle, in Bundle extraParams) = 900; + + /** + * @param apiVersion billing API version that the app is using, must be 12 or later + * @param packageName package name of the calling app + * @param extraParams a Bundle with the following optional keys: + * "playBillingLibraryVersion" - String + * @return Bundle containing the following key-value pairs + * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response codes + * on failures. + * "DEBUG_MESSAGE" - String + */ + Bundle acknowledgePurchase(int apiVersion, String packageName, String purchaseToken, in Bundle extraParams) = 901; + + Bundle o(int apiVersion, String packageName, String arg3, in Bundle arg4) = 1100; + + /** + * @param apiVersion billing API version that the app is using, must be 12 or later + * @param packageName package name of the calling app + * @param extraParams a Bundle with the following optional keys: + * "KEY_WINDOW_TOKEN" - IBinder + * "KEY_DIMEN_LEFT" - Integer + * "KEY_DIMEN_TOP" - Integer + * "KEY_DIMEN_RIGHT" - Integer + * "KEY_DIMEN_BOTTOM" - Integer + * "KEY_DIMEN_BOTTOM" - Integer + * "KEY_CATEGORY_IDS" - ArrayList + * "playBillingLibraryVersion" - String + */ +// void showInAppMessages(int apiVersion, String packageName, in Bundle extraParams, IInAppBillingServiceCallback callback) = 1200; + +// void r(String packageName, Bundle arg2, eql arg3) = 1300; +} \ No newline at end of file diff --git a/vending-app/src/main/aidl/com/google/android/gms/checkin/internal/ICheckinService.aidl b/vending-app/src/main/aidl/com/google/android/gms/checkin/internal/ICheckinService.aidl new file mode 100644 index 00000000..b94721ca --- /dev/null +++ b/vending-app/src/main/aidl/com/google/android/gms/checkin/internal/ICheckinService.aidl @@ -0,0 +1,7 @@ +package com.google.android.gms.checkin.internal; + +interface ICheckinService { + String getDeviceDataVersionInfo(); + long getLastCheckinSuccessTime(); + String getLastSimOperator(); +} \ No newline at end of file diff --git a/vending-app/src/main/java/com/android/billingclient/api/BillingClient.java b/vending-app/src/main/java/com/android/billingclient/api/BillingClient.java new file mode 100644 index 00000000..28e133db --- /dev/null +++ b/vending-app/src/main/java/com/android/billingclient/api/BillingClient.java @@ -0,0 +1,217 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + * Notice: Portions of this file are reproduced from work created and shared by Google and used + * according to terms described in the Creative Commons 4.0 Attribution License. + * See https://developers.google.com/readme/policies for details. + */ + +package com.android.billingclient.api; + +import android.app.Activity; +import android.os.Bundle; +import androidx.annotation.AnyThread; +import androidx.annotation.UiThread; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Main interface for communication between the library and user application code. + *

+ * It provides convenience methods for in-app billing. You can create one instance of this class for your application and + * use it to process in-app billing operations. It provides synchronous (blocking) and asynchronous (non-blocking) + * methods for many common in-app billing operations. + *

+ * It's strongly recommended that you instantiate only one BillingClient instance at one time to avoid multiple + * {@link PurchasesUpdatedListener#onPurchasesUpdated} callbacks for a single event. + *

+ * All methods annotated with {@link AnyThread} can be called from any thread and all the asynchronous callbacks will be + * returned on the same thread. Methods annotated with {@link UiThread} should be called from the Ui thread and all the + * asynchronous callbacks will be returned on the Ui thread as well. + *

+ * After instantiating, you must perform setup in order to start using the object. To perform setup, call the + * {@link #startConnection} method and provide a listener; that listener will be notified when setup is complete, after which + * (and not before) you may start calling other methods. After setup is complete, you will typically want to request an + * inventory of owned items and subscriptions. See {@link #queryPurchasesAsync} and {@link #queryProductDetailsAsync}. + *

+ * When you are done with this object, don't forget to call {@link #endConnection} to ensure proper cleanup. This object holds a + * binding to the in-app billing service and the manager to handle broadcast events, which will leak unless you dispose it + * correctly. If you created the object inside the {@link Activity#onCreate(Bundle)} method, then the recommended place to dispose is + * the {@link Activity#onDestroy()} method. After cleanup, it cannot be reused again for connection. + *

+ * To get library logs inside Android logcat, set corresponding logging level. E.g.: {@code adb shell setprop + * log.tag.BillingClient VERBOSE} + */ +public abstract class BillingClient { + + /** + * Possible response codes. + */ + @Retention(RetentionPolicy.SOURCE) + public @interface BillingResponseCode { + /** + * The request has reached the maximum timeout before Google Play responds. + *

+ * Since this state is transient, your app should automatically retry (e.g. with exponential back off) to recover from this + * error. Be mindful of how long you retry if the retry is happening during a user interaction. + * + * @deprecated See {@link #SERVICE_UNAVAILABLE} which will be used instead of this code. + */ + @Deprecated + int SERVICE_TIMEOUT = -3; + /** + * The requested feature is not supported by the Play Store on the current device. + *

+ * If your app would like to check if a feature is supported before trying to use the feature your app can call + * {@link #isFeatureSupported} to check if a feature is supported. For a list of feature types that can be supported, see + * {@link FeatureType}. + *

+ * For example: Before calling {@link #showInAppMessages} API, you can call {@link #isFeatureSupported} with the + * {@link FeatureType#IN_APP_MESSAGING} featureType to check if it is supported. + */ + int FEATURE_NOT_SUPPORTED = -2; + + /** + * The app is not connected to the Play Store service via the Google Play Billing Library. + *

+ * Examples where this error may occur: + *

    + *
  • The Play Store could have been updated in the background while your app was still running and the library lost + * connection.
  • + *
  • {@link #startConnection} was never called or has not completed yet.
  • + *
+ * Since this state is transient, your app should automatically retry (e.g. with exponential back off) to recover from this + * error. Be mindful of how long you retry if the retry is happening during a user interaction. The retry should lead to a + * call to {@link #startConnection} right after or in some time after you received this code. + */ + int SERVICE_DISCONNECTED = -1; + + /** + * Success. + */ + int OK = 0; + + /** + * Transaction was canceled by the user. + */ + int USER_CANCELED = 1; + + /** + * The service is currently unavailable. + *

+ * Since this state is transient, your app should automatically retry (e.g. with exponential back off) to recover from this + * error. Be mindful of how long you retry if the retry is happening during a user interaction. + */ + int SERVICE_UNAVAILABLE = 2; + + /** + * A user billing error occurred during processing. + *

+ * Examples where this error may occur: + *

    + *
  • The Play Store app on the user's device is out of date.
  • + *
  • The user is in an unsupported country.
  • + *
  • The user is an enterprise user and their enterprise admin has disabled users from making purchases.
  • + *
  • Google Play is unable to charge the user?s payment method.
  • + *
+ * Letting the user retry may succeed if the condition causing the error has changed (e.g. An enterprise user's admin has allowed purchases for the organization). + */ + int BILLING_UNAVAILABLE = 3; + + /** + * The requested product is not available for purchase. + *

+ * Please ensure the product is available in the user?s country. If you recently changed the country availability and are + * still receiving this error then it may be because of a propagation delay. + */ + int ITEM_UNAVAILABLE = 4; + + /** + * Error resulting from incorrect usage of the API. + *

+ * Examples where this error may occur: + *

    + *
  • Invalid arguments such as providing an empty product list where required.
  • + *
  • Misconfiguration of the app such as not signing the app or not having the necessary permissions in the manifest.
  • + *
+ */ + int DEVELOPER_ERROR = 5; + + /** + * Fatal error during the API action. + *

+ * This is an internal Google Play error that may be transient or due to an unexpected condition during processing. You + * can automatically retry (e.g. with exponential back off) for this case and contact Google Play if issues persist. Be + * mindful of how long you retry if the retry is happening during a user interaction. + */ + int ERROR = 6; + + /** + * The purchase failed because the item is already owned. + *

+ * Make sure your app is up-to-date with recent purchases using guidance in the Fetching purchases section in the + * integration guide. If this error occurs despite making the check for recent purchases, then it may be due to stale + * purchase information that was cached on the device by Play. When you receive this error, the cache should get + * updated. After this, your purchases should be reconciled, and you can process them as outlined in the processing + * purchases section in the integration guide. + */ + int ITEM_ALREADY_OWNED = 7; + + /** + * Requested action on the item failed since it is not owned by the user. + *

+ * Make sure your app is up-to-date with recent purchases using guidance in the Fetching purchases section in the + * integration guide. If this error occurs despite making the check for recent purchases, then it may be due to stale + * purchase information cached on the device by Play. When you receive this error, the cache should get updated. After + * this, your purchases should be reconciled, and you can process the purchases accordingly. For example, if you are + * trying to consume an item and if the updated purchase information says it is already consumed, you can ignore the + * error now. + */ + int ITEM_NOT_OWNED = 8; + + int EXPIRED_OFFER_TOKEN = 11; + + /** + * A network error occurred during the operation. + *

+ * This error indicates that there was a problem with the network connection between the device and Play systems. This + * could potentially also be due to the user not having an active network connection. + */ + int NETWORK_ERROR = 12; + + int RESPONSE_CODE_UNSPECIFIED = -999; + } + + /** + * Supported Product types. + */ + @Retention(RetentionPolicy.SOURCE) + public @interface ProductType { + /** + * A Product type for Android apps in-app products. + */ + String INAPP = "inapp"; + /** + * A Product type for Android apps subscriptions. + */ + String SUBS = "subs"; + } + + /** + * Supported SKU types. + * @deprecated Use {@link ProductType} instead. + */ + @Deprecated + @Retention(RetentionPolicy.SOURCE) + public @interface SkuType { + /** + * A type of SKU for Android apps in-app products. + */ + String INAPP = ProductType.INAPP; + /** + * A type of SKU for Android apps subscriptions. + */ + String SUBS = ProductType.SUBS; + } +} diff --git a/vending-app/src/main/java/com/android/billingclient/package-info.java b/vending-app/src/main/java/com/android/billingclient/package-info.java new file mode 100644 index 00000000..66eb361d --- /dev/null +++ b/vending-app/src/main/java/com/android/billingclient/package-info.java @@ -0,0 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * TODO: Move to own library, as a drop-in to replace com.android.billingclient:billing + */ +package com.android.billingclient; \ No newline at end of file diff --git a/vending-app/src/main/java/com/android/vending/VendingPreferences.kt b/vending-app/src/main/java/com/android/vending/VendingPreferences.kt new file mode 100644 index 00000000..51d4e84d --- /dev/null +++ b/vending-app/src/main/java/com/android/vending/VendingPreferences.kt @@ -0,0 +1,27 @@ +/* + * SPDX-FileCopyrightText: 2023, e Foundation + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.android.vending + +import android.content.Context +import org.microg.gms.settings.SettingsContract + +object VendingPreferences { + @JvmStatic + fun isLicensingEnabled(context: Context): Boolean { + val projection = arrayOf(SettingsContract.Vending.LICENSING) + return SettingsContract.getSettings(context, SettingsContract.Vending.getContentUri(context), projection) { c -> + c.getInt(0) != 0 + } + } + + @JvmStatic + fun isBillingEnabled(context: Context): Boolean { + val projection = arrayOf(SettingsContract.Vending.BILLING) + return SettingsContract.getSettings(context, SettingsContract.Vending.getContentUri(context), projection) { c -> + c.getInt(0) != 0 + } + } +} \ No newline at end of file diff --git a/vending-app/src/main/java/com/android/vending/billing/InAppBillingService.kt b/vending-app/src/main/java/com/android/vending/billing/InAppBillingService.kt new file mode 100644 index 00000000..34694881 --- /dev/null +++ b/vending-app/src/main/java/com/android/vending/billing/InAppBillingService.kt @@ -0,0 +1,24 @@ +package com.android.vending.billing + +import android.app.Service +import android.content.Intent +import android.os.IBinder +import androidx.annotation.RequiresApi +import org.microg.gms.profile.ProfileManager +import org.microg.vending.billing.ContextProvider +import org.microg.vending.billing.InAppBillingServiceImpl + +@RequiresApi(21) +class InAppBillingService: Service() { + override fun onCreate() { + super.onCreate() + ProfileManager.ensureInitialized(this) + ContextProvider.init(application) + } + + override fun onBind(intent: Intent): IBinder { + return InAppBillingServiceImpl(this) + } + + +} \ No newline at end of file diff --git a/vending-app/src/main/java/com/android/vending/licensing/LicenseRequest.java b/vending-app/src/main/java/com/android/vending/licensing/LicenseRequest.java index 843716f0..b4c3dbb3 100644 --- a/vending-app/src/main/java/com/android/vending/licensing/LicenseRequest.java +++ b/vending-app/src/main/java/com/android/vending/licensing/LicenseRequest.java @@ -173,7 +173,7 @@ public abstract class LicenseRequest extends Request { private static Timestamp makeTimestamp(long millis) { return new Timestamp.Builder() - .seconds((int) (millis / 1000)) + .seconds((millis / 1000)) .nanos(Math.floorMod(millis, 1000) * 1000000) .build(); } diff --git a/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java b/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java index 5042c3fc..661abbcd 100644 --- a/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java +++ b/vending-app/src/main/java/com/android/vending/licensing/LicensingService.java @@ -24,6 +24,7 @@ import android.os.IBinder; import android.os.RemoteException; import android.util.Log; +import com.android.vending.VendingPreferences; import com.android.volley.RequestQueue; import com.android.volley.toolbox.Volley; @@ -51,42 +52,14 @@ public class LicensingService extends Service { private static final Uri CHECKIN_SETTINGS_PROVIDER = Uri.parse("content://com.google.android.gms.microg.settings/check-in"); - private static final Uri SETTINGS_PROVIDER = Uri.parse("content://com.google.android.gms.microg.settings/vending"); - - private static final String PREFERENCE_LICENSING_ENABLED = "vending_licensing"; - private final ILicensingService.Stub mLicenseService = new ILicensingService.Stub() { - private boolean shouldCheckLicense() { - - Cursor cursor = null; - try { - cursor = getContentResolver().query( - SETTINGS_PROVIDER, new String[]{PREFERENCE_LICENSING_ENABLED}, null, null, null - ); - - if (cursor == null || cursor.getColumnCount() != 1) { - Log.e(TAG, "settings provider not available"); - return false; - } else { - cursor.moveToNext(); - return cursor.getInt(0) != 0; - } - - } finally { - if (cursor != null) { - cursor.close(); - } - } - } - - @Override public void checkLicense(long nonce, String packageName, ILicenseResultListener listener) throws RemoteException { Log.v(TAG, "checkLicense(" + nonce + ", " + packageName + ")"); int callingUid = getCallingUid(); - if (!shouldCheckLicense()) { + if (!VendingPreferences.isLicensingEnabled(getApplicationContext())) { Log.d(TAG, "not checking license, as it is disabled by user"); return; } @@ -121,7 +94,7 @@ public class LicensingService extends Service { Log.v(TAG, "checkLicenseV2(" + packageName + ", " + extraParams + ")"); int callingUid = getCallingUid(); - if (!shouldCheckLicense()) { + if (!VendingPreferences.isLicensingEnabled(getApplicationContext())) { Log.d(TAG, "not checking license, as it is disabled by user"); return; } diff --git a/vending-app/src/main/java/org/microg/vending/billing/AuthManager.kt b/vending-app/src/main/java/org/microg/vending/billing/AuthManager.kt new file mode 100644 index 00000000..1363445e --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/billing/AuthManager.kt @@ -0,0 +1,35 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.vending.billing + +import android.accounts.Account +import android.accounts.AccountManager +import android.content.Context +import android.content.Intent +import android.util.Log +import org.microg.vending.billing.core.AuthData +import java.util.concurrent.TimeUnit + +object AuthManager { + private const val TOKEN_TYPE = "oauth2:https://www.googleapis.com/auth/googleplay https://www.googleapis.com/auth/accounts.reauth" + fun getAuthData(context: Context, account: Account): AuthData? { + val deviceCheckInConsistencyToken = CheckinServiceClient.getConsistencyToken(context) + val gsfId = GServices.getString(context.contentResolver, "android_id", "0")!!.toBigInteger().toString(16) + if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "gsfId: $gsfId, deviceDataVersionInfo: $deviceCheckInConsistencyToken") + val accountManager: AccountManager = AccountManager.get(context) + val future = accountManager.getAuthToken(account, TOKEN_TYPE, false, null, null) + val bundle = future.getResult(15, TimeUnit.SECONDS) + val launch = bundle.getParcelable(AccountManager.KEY_INTENT) as Intent? + return if (launch != null) { + Log.e(TAG, "[getAuthData]need start activity by intent: $launch") + null + } else { + bundle.getString(AccountManager.KEY_AUTHTOKEN)?.let { + AuthData(account.name, it, gsfId, deviceCheckInConsistencyToken) + } + } + } +} \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/billing/CheckinServiceClient.kt b/vending-app/src/main/java/org/microg/vending/billing/CheckinServiceClient.kt new file mode 100644 index 00000000..ccf8ce05 --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/billing/CheckinServiceClient.kt @@ -0,0 +1,49 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.vending.billing + +import android.content.ComponentName +import android.content.Context +import android.content.Context.BIND_AUTO_CREATE +import android.content.Intent +import android.content.ServiceConnection +import android.os.IBinder +import android.util.Log +import com.google.android.gms.checkin.internal.ICheckinService +import java.util.concurrent.LinkedBlockingQueue +import java.util.concurrent.TimeUnit + +// TODO: Connect to check-in settings provider instead +object CheckinServiceClient { + private val serviceQueue = LinkedBlockingQueue() + + fun getConsistencyToken(context: Context): String { + try { + val conn = object : ServiceConnection { + override fun onServiceConnected(name: ComponentName?, service: IBinder?) { + service?.let { serviceQueue.add(ICheckinService.Stub.asInterface(it)) } + } + + override fun onServiceDisconnected(name: ComponentName?) { + serviceQueue.clear() + } + } + val intent = Intent("com.google.android.gms.checkin.BIND_TO_SERVICE") + intent.setPackage("com.google.android.gms") + val res = context.bindService(intent, conn, BIND_AUTO_CREATE) + if (!res) return "" + try { + val service = serviceQueue.poll(10, TimeUnit.SECONDS) ?: return "" + return service.deviceDataVersionInfo ?: "" + } finally { + context.unbindService(conn) + } + } catch (e: Exception) { + Log.e(TAG, "getConsistencyToken", e) + return "" + } + } +} \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/billing/Constants.kt b/vending-app/src/main/java/org/microg/vending/billing/Constants.kt new file mode 100644 index 00000000..93af4deb --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/billing/Constants.kt @@ -0,0 +1,17 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.vending.billing + +const val TAG = "Billing" + +// TODO: What versions to use? +const val VENDING_VERSION_CODE = 83061810L +const val VENDING_VERSION_NAME = "30.6.18-21 [0] [PR] 450795914" +const val VENDING_PACKAGE_NAME = "com.android.vending" +// TODO: Replace key name +const val KEY_IAP_SHEET_UI_PARAM = "key_iap_sheet_ui_param" +const val DEFAULT_ACCOUNT_TYPE = "com.google" +const val ADD_PAYMENT_METHOD_URL = "https://play.google.com/store/paymentmethods" \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/billing/ContextProvider.kt b/vending-app/src/main/java/org/microg/vending/billing/ContextProvider.kt new file mode 100644 index 00000000..eaabfff9 --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/billing/ContextProvider.kt @@ -0,0 +1,19 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.vending.billing + +import android.app.Application +import org.microg.gms.profile.ProfileManager + +// TODO: Get rid +object ContextProvider { + lateinit var context: Application + private set + + fun init(application: Application) { + context = application + } +} \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/billing/DeviceIdentifier.kt b/vending-app/src/main/java/org/microg/vending/billing/DeviceIdentifier.kt new file mode 100644 index 00000000..b93ef17b --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/billing/DeviceIdentifier.kt @@ -0,0 +1,71 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.vending.billing + +import java.util.Random + +// TODO: Use existing code +object DeviceIdentifier { + var wifiMac = randomMacAddress() + var meid = randomMeid() + var serial = randomSerial("008741A0B2C4D6E8") + var esn: String = "" + + + private fun randomMacAddress(): String { + var mac = "b407f9" + val rand = Random() + for (i in 0..5) { + mac += rand.nextInt(16).toString(16) + } + return mac + } + + private fun randomSerial( + template: String, + prefixLength: Int = (template.length / 2).coerceAtMost(6) + ): String { + val serial = StringBuilder() + template.forEachIndexed { index, c -> + serial.append( + when { + index < prefixLength -> c + c.isDigit() -> '0' + kotlin.random.Random.nextInt(10) + c.isLowerCase() && c <= 'f' -> 'a' + kotlin.random.Random.nextInt(6) + c.isLowerCase() -> 'a' + kotlin.random.Random.nextInt(26) + c.isUpperCase() && c <= 'F' -> 'A' + kotlin.random.Random.nextInt(6) + c.isUpperCase() -> 'A' + kotlin.random.Random.nextInt(26) + else -> c + } + ) + } + return serial.toString() + } + + private fun randomMeid(): String { + // http://en.wikipedia.org/wiki/International_Mobile_Equipment_Identity + // We start with a known base, and generate random MEID + var meid = "35503104" + val rand = Random() + for (i in 0..5) { + meid += rand.nextInt(10).toString() + } + + // Luhn algorithm (check digit) + var sum = 0 + for (i in meid.indices) { + var c = meid[i].toString().toInt() + if ((meid.length - i - 1) % 2 == 0) { + c *= 2 + c = c % 10 + c / 10 + } + sum += c + } + val check = (100 - sum) % 10 + meid += check.toString() + return meid + } +} \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/billing/GServices.kt b/vending-app/src/main/java/org/microg/vending/billing/GServices.kt new file mode 100644 index 00000000..60b15132 --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/billing/GServices.kt @@ -0,0 +1,20 @@ +package org.microg.vending.billing + +import android.content.ContentResolver +import android.net.Uri + +// TODO: Move +object GServices { + private val CONTENT_URI: Uri = Uri.parse("content://com.google.android.gsf.gservices") + + fun getString(resolver: ContentResolver, key: String, defaultValue: String?): String? { + var result = defaultValue + val cursor = resolver.query(CONTENT_URI, null, null, arrayOf(key), null) + cursor?.use { + if (cursor.moveToNext()) { + result = cursor.getString(1) + } + } + return result + } +} \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/billing/InAppBillingServiceImpl.kt b/vending-app/src/main/java/org/microg/vending/billing/InAppBillingServiceImpl.kt new file mode 100644 index 00000000..5803da0d --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/billing/InAppBillingServiceImpl.kt @@ -0,0 +1,629 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.vending.billing + +import android.accounts.Account +import android.app.PendingIntent +import android.app.PendingIntent.FLAG_CANCEL_CURRENT +import android.app.PendingIntent.FLAG_IMMUTABLE +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.util.Log +import androidx.annotation.RequiresApi +import androidx.core.os.bundleOf +import com.android.billingclient.api.BillingClient.BillingResponseCode +import com.android.billingclient.api.BillingClient.ProductType +import com.android.vending.VendingPreferences +import com.android.vending.billing.IInAppBillingService +import org.microg.vending.billing.ui.InAppBillingHostActivity +import org.microg.vending.billing.ui.logic.BuyFlowResult +import com.google.android.gms.droidguard.DroidGuardClient +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.runBlocking +import org.microg.gms.utils.toHexString +import org.microg.vending.billing.core.* + +private class BuyFlowCacheEntry( + var packageName: String, + var account: Account, + var buyFlowParams: BuyFlowParams? = null, + var lastAcquireResult: AcquireResult? = null, + var droidGuardResult: String = "" +) + +private const val EXPIRE_MS = 1 * 60 * 1000 + +private data class IAPCoreCacheEntry( + val iapCore: IAPCore, + val expiredAt: Long +) + + +private const val requestCode = 10001 +@RequiresApi(21) +class InAppBillingServiceImpl(private val context: Context) : IInAppBillingService.Stub() { + + + companion object { + private val buyFlowCacheMap = mutableMapOf() + private val iapCoreCacheMap = mutableMapOf() + private val typeList = listOf( + ProductType.SUBS, + ProductType.INAPP, + "first_party", + "audio_book", + "book", + "book_subs", + "nest_subs", + "play_pass_subs", + "stadia_item", + "stadia_subs", + "movie", + "tv_show", + "tv_episode", + "tv_season" + ) + fun acquireRequest( + context: Context, + cacheKey: String, + actionContexts: List = emptyList(), + authToken: String? = null, + firstRequest: Boolean = false + ): BuyFlowResult { + if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "acquireRequest(cacheKey=$cacheKey, actionContexts=${actionContexts.map { it.toHexString() }}, authToken=$authToken)") + val buyFlowCacheEntry = buyFlowCacheMap[cacheKey] ?: return BuyFlowResult( + null, null, resultBundle(BillingResponseCode.DEVELOPER_ERROR, "Parameter check error.") + ) + val buyFlowParams = buyFlowCacheEntry.buyFlowParams ?: return BuyFlowResult( + null, buyFlowCacheEntry.account, resultBundle(BillingResponseCode.DEVELOPER_ERROR, "Parameter check error.") + ) + val params = AcquireParams( + buyFlowParams = buyFlowParams, + actionContext = actionContexts, + authToken = authToken, + droidGuardResult = buyFlowCacheEntry.droidGuardResult.takeIf { !firstRequest }, + lastAcquireResult = buyFlowCacheEntry.lastAcquireResult.takeIf { !firstRequest } + ) + + val coreResult = try { + val deferred = CoroutineScope(Dispatchers.IO).async { + createIAPCore( + context, + buyFlowCacheEntry.account, + buyFlowCacheEntry.packageName + ).doAcquireRequest( + params + ) + } + runBlocking { deferred.await() } + } catch (e: RuntimeException) { + if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "acquireRequest", e) + return BuyFlowResult(null, buyFlowCacheEntry.account, resultBundle(BillingResponseCode.DEVELOPER_ERROR, e.message)) + } catch (e: Exception) { + if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "acquireRequest", e) + return BuyFlowResult(null, buyFlowCacheEntry.account, resultBundle(BillingResponseCode.DEVELOPER_ERROR, "Internal error.")) + } + if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "acquireRequest acquireParsedResult: ${coreResult.acquireParsedResult}") + buyFlowCacheEntry.lastAcquireResult = coreResult + if (coreResult.acquireParsedResult.action?.droidGuardMap?.isNotEmpty() == true) { + DroidGuardClient.getResults(context, "phonesky_acquire_flow", coreResult.acquireParsedResult.action.droidGuardMap).addOnCompleteListener { task -> + buyFlowCacheEntry.droidGuardResult = task.result + } + } + coreResult.acquireParsedResult.purchaseItems.forEach { + PurchaseManager.addPurchase(buyFlowCacheEntry.account, buyFlowCacheEntry.packageName, it) + } + return BuyFlowResult( + coreResult.acquireParsedResult, + buyFlowCacheEntry.account, + coreResult.acquireParsedResult.result.toBundle() + ) + } + + fun requestAuthProofToken(context: Context, cacheKey: String, password: String): String { + val buyFlowCacheEntry = buyFlowCacheMap[cacheKey] + ?: throw IllegalStateException("Nothing cached: $cacheKey") + val deferred = CoroutineScope(Dispatchers.IO).async { + createIAPCore( + context, + buyFlowCacheEntry.account, + buyFlowCacheEntry.packageName + ).requestAuthProofToken(password) + } + return runBlocking { deferred.await() } + } + + private fun createIAPCore(context: Context, account: Account, pkgName: String): IAPCore { + val key = "$pkgName:$account" + val cacheEntry = iapCoreCacheMap[key] + if (cacheEntry != null) { + if (cacheEntry.expiredAt > System.currentTimeMillis()) + return cacheEntry.iapCore + iapCoreCacheMap.remove(key) + } + val authData = AuthManager.getAuthData(context, account) + ?: throw RuntimeException("Failed to obtain login token.") + val deviceEnvInfo = createDeviceEnvInfo(context) + ?: throw RuntimeException("Failed to retrieve device information.") + val clientInfo = createClient(context, pkgName) + ?: throw RuntimeException("Failed to retrieve client information.") + val iapCore = IAPCore(context.applicationContext, deviceEnvInfo, clientInfo, authData) + iapCoreCacheMap[key] = + IAPCoreCacheEntry(iapCore, System.currentTimeMillis() + EXPIRE_MS) + return iapCore + } + } + + private fun getPreferredAccount(extraParams: Bundle?): Account { + val name = extraParams?.getString("accountName") + name?.let { + extraParams.remove("accountName") + } + return getGoogleAccount(context, name) + ?: throw RuntimeException("No Google account found.") + } + + private fun isBillingSupported( + apiVersion: Int, + type: String?, + packageName: String, + extraParams: Bundle? + ): Bundle { + if (!VendingPreferences.isBillingEnabled(context)) { + Log.w(TAG, "isBillingSupported: Billing is disabled") + return resultBundle(BillingResponseCode.BILLING_UNAVAILABLE, "Billing is disabled") + } + if (apiVersion < 3 || apiVersion > 17) { + return resultBundle(BillingResponseCode.BILLING_UNAVAILABLE, "Client does not support the requesting billing API.") + } + if (extraParams != null && apiVersion < 7) { + return resultBundle(BillingResponseCode.DEVELOPER_ERROR, "ExtraParams was introduced in API version 7.") + } + if (type.isNullOrBlank()) { + return resultBundle(BillingResponseCode.DEVELOPER_ERROR, "SKU type can't be empty.") + } + if (!typeList.contains(type)) { + return resultBundle(BillingResponseCode.DEVELOPER_ERROR, "Invalid SKU type: $type") + } + if (extraParams != null && !extraParams.isEmpty && extraParams.getBoolean("vr") && type == "subs") { + return resultBundle(BillingResponseCode.BILLING_UNAVAILABLE, "subscription is not supported in VR Mode.") + } + return resultBundle(BillingResponseCode.OK, "") + } + + override fun isBillingSupported(apiVersion: Int, packageName: String?, type: String?): Int { + val result = isBillingSupported(apiVersion, type, packageName!!, null) + Log.d(TAG, "isBillingSupported(apiVersion=$apiVersion, packageName=$packageName, type=$type)=$result") + return result.getInt("RESPONSE_CODE") + } + + override fun getSkuDetails( + apiVersion: Int, + packageName: String?, + type: String?, + skusBundle: Bundle? + ): Bundle { + Log.d(TAG, "getSkuDetails(apiVersion=$apiVersion, packageName=$packageName, type=$type, skusBundle=$skusBundle)") + return resultBundle(BillingResponseCode.BILLING_UNAVAILABLE, "Not yet implemented") + } + + override fun getBuyIntent( + apiVersion: Int, + packageName: String?, + sku: String?, + type: String?, + developerPayload: String? + ): Bundle { + if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "getBuyIntent(apiVersion=$apiVersion, packageName=$packageName, sku=$sku, type=$type, developerPayload=$developerPayload)") + return resultBundle(BillingResponseCode.BILLING_UNAVAILABLE, "Not yet implemented") + } + + override fun getPurchases( + apiVersion: Int, + packageName: String?, + type: String?, + continuationToken: String? + ): Bundle { + if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "getPurchases(apiVersion=$apiVersion, packageName=$packageName, type=$type, continuationToken=$continuationToken)") + return resultBundle(BillingResponseCode.BILLING_UNAVAILABLE, "Not yet implemented") + } + + override fun consumePurchase( + apiVersion: Int, + packageName: String?, + purchaseToken: String? + ): Int { + Log.d(TAG, "consumePurchase(apiVersion=$apiVersion, packageName=$packageName, purchaseToken=$purchaseToken)") + return BillingResponseCode.BILLING_UNAVAILABLE + } + + override fun isPromoEligible(apiVersion: Int, packageName: String?, type: String?): Int { + Log.d(TAG, "isPromoEligible(apiVersion=$apiVersion, packageName=$packageName, type=$type)") + return BillingResponseCode.BILLING_UNAVAILABLE + } + + override fun getBuyIntentToReplaceSkus( + apiVersion: Int, + packageName: String?, + oldSkus: MutableList?, + newSku: String?, + type: String?, + developerPayload: String? + ): Bundle { + Log.d(TAG, "getBuyIntentToReplaceSkus(apiVersion=$apiVersion, packageName=$packageName, oldSkus=$oldSkus, newSku=$newSku, type=$type, developerPayload=$developerPayload)") + return resultBundle(BillingResponseCode.BILLING_UNAVAILABLE, "Not yet implemented") + } + + override fun getBuyIntentExtraParams( + apiVersion: Int, + packageName: String, + sku: String, + type: String, + developerPayload: String?, + extraParams: Bundle? + ): Bundle { + if (!VendingPreferences.isBillingEnabled(context)) { + Log.w(TAG, "getBuyIntentExtraParams: Billing is disabled") + return resultBundle(BillingResponseCode.BILLING_UNAVAILABLE, "Billing is disabled") + } + extraParams?.size() + Log.d(TAG, "getBuyIntentExtraParams(apiVersion=$apiVersion, packageName=$packageName, sku=$sku, type=$type, developerPayload=$developerPayload, extraParams=$extraParams)") + + + val skuSerializedDocIdList = + extraParams?.getStringArrayList("SKU_SERIALIZED_DOCID_LIST") + skuSerializedDocIdList?.forEach { + if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "serializedDocId=$it") + } + val skuOfferTypeList = extraParams?.getIntegerArrayList("SKU_OFFER_TYPE_LIST") + skuOfferTypeList?.forEach { + if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "skuOfferType=$it") + } + val skuOfferIdTokenList = extraParams?.getStringArrayList("SKU_OFFER_ID_TOKEN_LIST") + skuOfferIdTokenList?.forEach { + if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "skuOfferIdToken=$it") + } + val accountName = extraParams?.getString("accountName")?.also { + if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "accountName=$it") + } + val oldSkuPurchaseToken = extraParams?.getString("oldSkuPurchaseToken")?.also { + if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "oldSkuPurchaseToken=$it") + } + val oldSkuPurchaseId = extraParams?.getString("oldSkuPurchaseId")?.also { + if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "oldSkuPurchaseId=$it") + } + + extraParams?.let { + it.remove("skusToReplace") + it.remove("oldSkuPurchaseToken") + it.remove("vr") + it.remove("isDynamicSku") + it.remove("rewardToken") + it.remove("childDirected") + it.remove("underAgeOfConsent") + it.remove("additionalSkus") + it.remove("additionalSkuTypes") + it.remove("SKU_OFFER_ID_TOKEN_LIST") + it.remove("SKU_OFFER_ID_LIST") + it.remove("SKU_OFFER_TYPE_LIST") + it.remove("SKU_SERIALIZED_DOCID_LIST") + it.remove("oldSkuPurchaseId") + } + + val account = try { + getPreferredAccount(extraParams) + } catch (e: RuntimeException) { + return resultBundle(BillingResponseCode.BILLING_UNAVAILABLE, e.message) + } + val params = BuyFlowParams( + apiVersion = apiVersion, + sku = sku, + skuType = type, + developerPayload = developerPayload ?: "", + skuParams = bundleToMap(extraParams), + needAuth = SettingsManager(context).getAuthStatus(), + skuSerializedDockIdList = skuSerializedDocIdList, + skuOfferIdTokenList = skuOfferIdTokenList, + oldSkuPurchaseId = oldSkuPurchaseId, + oldSkuPurchaseToken = oldSkuPurchaseToken + ) + val cacheEntryKey = "${packageName}:${account.name}" + buyFlowCacheMap[cacheEntryKey] = + BuyFlowCacheEntry(packageName, account, buyFlowParams = params) + val intent = Intent(context, InAppBillingHostActivity::class.java) + intent.putExtra(KEY_IAP_SHEET_UI_PARAM, cacheEntryKey) + val buyFlowPendingIntent = PendingIntent.getActivity(context, requestCode, intent, FLAG_CANCEL_CURRENT or FLAG_IMMUTABLE) + return resultBundle(BillingResponseCode.OK, "", bundleOf("BUY_INTENT" to buyFlowPendingIntent)) + } + + override fun getPurchaseHistory( + apiVersion: Int, + packageName: String?, + type: String?, + continuationToken: String?, + extraParams: Bundle? + ): Bundle { + if (!VendingPreferences.isBillingEnabled(context)) { + Log.w(TAG, "getPurchaseHistory: Billing is disabled") + return resultBundle(BillingResponseCode.BILLING_UNAVAILABLE, "Billing is disabled") + } + extraParams?.size() + Log.d(TAG, "getPurchaseHistory(apiVersion=$apiVersion, packageName=$packageName, type=$type, continuationToken=$continuationToken, extraParams=$extraParams)") + val account = try { + getPreferredAccount(extraParams) + } catch (e: RuntimeException) { + return resultBundle(BillingResponseCode.BILLING_UNAVAILABLE, e.message) + } + val params = GetPurchaseHistoryParams( + apiVersion = apiVersion, + type = type!!, + continuationToken = continuationToken, + extraParams = bundleToMap(extraParams) + ) + val coreResult = try { + val deferred = CoroutineScope(Dispatchers.IO).async { + createIAPCore(context, account, packageName!!).getPurchaseHistory(params) + } + runBlocking { + deferred.await() + } + } catch (e: RuntimeException) { + return resultBundle(BillingResponseCode.DEVELOPER_ERROR, e.message) + } catch (e: Exception) { + Log.e(TAG, "getPurchaseHistory", e) + return resultBundle(BillingResponseCode.DEVELOPER_ERROR, "Internal error.") + } + if (coreResult.getCode() == BillingResponseCode.OK) { + val itemList = ArrayList() + val dataList = ArrayList() + val signatureList = ArrayList() + coreResult.purchaseHistoryList?.forEach { + itemList.add(it.sku) + dataList.add(it.jsonData) + signatureList.add(it.signature) + } + val result = Bundle() + result.putStringArrayList("INAPP_PURCHASE_ITEM_LIST", itemList) + result.putStringArrayList("INAPP_PURCHASE_DATA_LIST", dataList) + result.putStringArrayList("INAPP_DATA_SIGNATURE_LIST", signatureList) + if (!coreResult.continuationToken.isNullOrEmpty()) { + result.putString("INAPP_CONTINUATION_TOKEN", coreResult.continuationToken) + } + return resultBundle(BillingResponseCode.OK, "", result) + } + return coreResult.resultMap.toBundle() + } + + override fun isBillingSupportedExtraParams( + apiVersion: Int, + packageName: String?, + type: String?, + extraParams: Bundle? + ): Int { + extraParams?.size() + val result = isBillingSupported(apiVersion, type, packageName!!, extraParams) + Log.d(TAG, "isBillingSupportedExtraParams(apiVersion=$apiVersion, packageName=$packageName, type=$type, extraParams=$extraParams)=$result") + return result.getInt("RESPONSE_CODE") + } + + override fun getPurchasesExtraParams( + apiVersion: Int, + packageName: String?, + type: String?, + continuationToken: String?, + extraParams: Bundle? + ): Bundle { + if (!VendingPreferences.isBillingEnabled(context)) { + Log.w(TAG, "getPurchasesExtraParams: Billing is disabled") + return resultBundle(BillingResponseCode.BILLING_UNAVAILABLE, "Billing is disabled") + } + extraParams?.size() + Log.d(TAG, "getPurchasesExtraParams(apiVersion=$apiVersion, packageName=$packageName, type=$type, continuationToken=$continuationToken, extraParams=$extraParams)") + if (apiVersion < 7 && extraParams != null) { + return resultBundle(BillingResponseCode.DEVELOPER_ERROR, "Parameter check error.") + } + val account = try { + getPreferredAccount(extraParams) + } catch (e: RuntimeException) { + return resultBundle(BillingResponseCode.BILLING_UNAVAILABLE, e.message) + } + val enablePendingPurchases = extraParams?.getBoolean("enablePendingPurchases", false) ?: false + val itemList = ArrayList() + val dataList = ArrayList() + val signatureList = ArrayList() + PurchaseManager.queryPurchases(account, packageName!!, type!!).filter { + if (it.type == "subs" && it.expireAt < System.currentTimeMillis()) return@filter false + true + }.forEach { + if (enablePendingPurchases || it.purchaseState != 4) { + itemList.add(it.sku) + dataList.add(it.jsonData) + signatureList.add(it.signature) + } + } + val result = Bundle() + result.putStringArrayList("INAPP_PURCHASE_ITEM_LIST", itemList) + result.putStringArrayList("INAPP_PURCHASE_DATA_LIST", dataList) + result.putStringArrayList("INAPP_DATA_SIGNATURE_LIST", signatureList) + return resultBundle(BillingResponseCode.OK, "", result) + } + + override fun consumePurchaseExtraParams( + apiVersion: Int, + packageName: String?, + purchaseToken: String, + extraParams: Bundle? + ): Bundle { + if (!VendingPreferences.isBillingEnabled(context)) { + Log.w(TAG, "consumePurchaseExtraParams: Billing is disabled") + return resultBundle(BillingResponseCode.BILLING_UNAVAILABLE, "Billing is disabled") + } + extraParams?.size() + Log.d(TAG, "consumePurchaseExtraParams(apiVersion=$apiVersion, packageName=$packageName, purchaseToken=$purchaseToken, extraParams=$extraParams)") + val account = try { + getPreferredAccount(extraParams) + } catch (e: RuntimeException) { + return resultBundle(BillingResponseCode.BILLING_UNAVAILABLE, e.message) + } + val params = ConsumePurchaseParams( + apiVersion = apiVersion, + purchaseToken = purchaseToken, + extraParams = bundleToMap(extraParams) + ) + val coreResult = try { + val deferred = CoroutineScope(Dispatchers.IO).async { + val coreResult = createIAPCore(context, account, packageName!!).consumePurchase(params) + if (coreResult.getCode() == BillingResponseCode.OK) { + PurchaseManager.removePurchase(purchaseToken) + } + coreResult + } + runBlocking { deferred.await() } + } catch (e: RuntimeException) { + return resultBundle(BillingResponseCode.DEVELOPER_ERROR, e.message) + } catch (e: Exception) { + Log.e(TAG, "consumePurchaseExtraParams", e) + return resultBundle(BillingResponseCode.DEVELOPER_ERROR, "Internal error.") + } + + return coreResult.resultMap.toBundle() + } + + override fun getPriceChangeConfirmationIntent( + apiVersion: Int, + packageName: String?, + sku: String?, + type: String?, + extraParams: Bundle? + ): Bundle { + extraParams?.size() + Log.d(TAG, "getPriceChangeConfirmationIntent(apiVersion=$apiVersion, packageName=$packageName, sku=$sku, type=$type, extraParams=$extraParams)") + return resultBundle(BillingResponseCode.BILLING_UNAVAILABLE, "Not yet implemented") + } + + override fun getSkuDetailsExtraParams( + apiVersion: Int, + packageName: String?, + type: String?, + skuBundle: Bundle?, + extraParams: Bundle? + ): Bundle { + if (!VendingPreferences.isBillingEnabled(context)) { + Log.w(TAG, "getSkuDetailsExtraParams: Billing is disabled") + return resultBundle(BillingResponseCode.BILLING_UNAVAILABLE, "Billing is disabled") + } + extraParams?.size() + skuBundle?.size() + Log.d(TAG, "getSkuDetailsExtraParams(apiVersion=$apiVersion, packageName=$packageName, type=$type, skusBundle=$skuBundle, extraParams=$extraParams)") + val account = try { + getPreferredAccount(extraParams) + } catch (e: RuntimeException) { + return resultBundle(BillingResponseCode.BILLING_UNAVAILABLE, e.message) + } + val idList = skuBundle?.getStringArrayList("ITEM_ID_LIST") + val dynamicPriceTokensList = skuBundle?.getStringArrayList("DYNAMIC_PRICE_TOKENS_LIST") + if (idList.isNullOrEmpty()) { + Log.e(TAG, "Input Error: skusBundle must contain an array associated with key ITEM_ID_LIST.") + return resultBundle(BillingResponseCode.DEVELOPER_ERROR, "SKU bundle must contain sku list") + } + idList.sort() + if (dynamicPriceTokensList != null && dynamicPriceTokensList.isEmpty()) { + Log.e(TAG, "Input Error: skusBundle array associated with key ITEM_ID_LIST or key DYNAMIC_PRICE_TOKENS_LIST cannot be empty.") + return resultBundle(BillingResponseCode.DEVELOPER_ERROR, "SKU bundle must contain sku list") + } + if (apiVersion < 9 && extraParams?.isEmpty == false) { + return resultBundle(BillingResponseCode.DEVELOPER_ERROR, "Must specify an API version >= 9 to use this API.") + } + val params = GetSkuDetailsParams( + apiVersion = apiVersion, + skuType = type!!, + skuIdList = idList, + skuPkgName = extraParams?.getString("SKU_PACKAGE_NAME")?.also { + extraParams.remove("SKU_PACKAGE_NAME") + } ?: "", + sdkVersion = extraParams?.getString("playBillingLibraryVersion") ?: "", + multiOfferSkuDetail = extraParams?.let { bundleToMap(it) } ?: emptyMap() + ) + + val coreResult = try { + val deferred = CoroutineScope(Dispatchers.IO).async { + createIAPCore(context, account, packageName!!).getSkuDetails(params) + } + runBlocking { deferred.await() } + } catch (e: RuntimeException) { + Log.e(TAG, "getSkuDetailsExtraParams", e) + return resultBundle(BillingResponseCode.DEVELOPER_ERROR, e.message) + } catch (e: Exception) { + Log.e(TAG, "getSkuDetailsExtraParams", e) + return resultBundle(BillingResponseCode.DEVELOPER_ERROR, "Internal error.") + } + + coreResult.let { detailsResult -> + val details = ArrayList(detailsResult.skuDetailsList.map { it.jsonDetails }) + if (detailsResult.getCode() == BillingResponseCode.OK) { + return resultBundle(BillingResponseCode.OK, "", bundleOf("DETAILS_LIST" to details)) + } else { + return resultBundle(detailsResult.getCode(), detailsResult.getMessage()) + } + } + } + + override fun acknowledgePurchase( + apiVersion: Int, + packageName: String?, + purchaseToken: String?, + extraParams: Bundle? + ): Bundle { + if (!VendingPreferences.isBillingEnabled(context)) { + Log.w(TAG, "acknowledgePurchase: Billing is disabled") + return resultBundle(BillingResponseCode.BILLING_UNAVAILABLE, "Billing is disabled") + } + extraParams?.size() + Log.d(TAG, "acknowledgePurchase(apiVersion=$apiVersion, packageName=$packageName, purchaseToken=$purchaseToken, extraParams=$extraParams)") + val account = try { + getPreferredAccount(extraParams) + } catch (e: RuntimeException) { + return resultBundle(BillingResponseCode.BILLING_UNAVAILABLE, e.message) + } + val params = AcknowledgePurchaseParams( + apiVersion = apiVersion, + purchaseToken = purchaseToken!!, + extraParams = bundleToMap(extraParams) + ) + val coreResult = try { + val deferred = CoroutineScope(Dispatchers.IO).async { + val coreResult = createIAPCore(context, account, packageName!!).acknowledgePurchase(params) + if (coreResult.getCode() == BillingResponseCode.OK && coreResult.purchaseItem != null) { + PurchaseManager.updatePurchase(coreResult.purchaseItem) + } + coreResult + } + runBlocking { deferred.await() } + } catch (e: RuntimeException) { + return resultBundle(BillingResponseCode.DEVELOPER_ERROR, e.message) + } catch (e: Exception) { + Log.e(TAG, "acknowledgePurchase", e) + return resultBundle(BillingResponseCode.DEVELOPER_ERROR, "Internal error.") + } + + return coreResult.resultMap.toBundle() + } + + override fun o( + apiVersion: Int, + packageName: String?, + arg3: String?, + extraParams: Bundle? + ): Bundle { + extraParams?.size() + Log.d(TAG, "o(apiVersion=$apiVersion, packageName=$packageName, arg3=$arg3, extraParams=$extraParams)") + return resultBundle(BillingResponseCode.BILLING_UNAVAILABLE, "Not yet implemented") + } + +} \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/billing/LightPurchaseFlowActivity.java b/vending-app/src/main/java/org/microg/vending/billing/PurchaseActivity.java similarity index 90% rename from vending-app/src/main/java/org/microg/vending/billing/LightPurchaseFlowActivity.java rename to vending-app/src/main/java/org/microg/vending/billing/PurchaseActivity.java index a55a5ba7..cba32f1d 100644 --- a/vending-app/src/main/java/org/microg/vending/billing/LightPurchaseFlowActivity.java +++ b/vending-app/src/main/java/org/microg/vending/billing/PurchaseActivity.java @@ -13,7 +13,7 @@ import androidx.annotation.Nullable; import com.android.vending.R; -public class LightPurchaseFlowActivity extends Activity { +public class PurchaseActivity extends Activity { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { diff --git a/vending-app/src/main/java/org/microg/vending/billing/PurchaseManager.kt b/vending-app/src/main/java/org/microg/vending/billing/PurchaseManager.kt new file mode 100644 index 00000000..e728ddfe --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/billing/PurchaseManager.kt @@ -0,0 +1,129 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.vending.billing + +import android.accounts.Account +import android.content.ContentValues +import android.content.Context +import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteOpenHelper +import org.microg.vending.billing.core.PurchaseItem + +object PurchaseManager { + private val database by lazy { PurchaseDB(ContextProvider.context) } + + fun queryPurchases(account: Account, pkgName: String, type: String): List = + database.queryPurchases(account, pkgName, type) + + fun updatePurchase(purchaseItem: PurchaseItem) = database.updatePurchase(purchaseItem) + + fun removePurchase(purchaseToken: String) = database.removePurchase(purchaseToken) + + fun addPurchase(account: Account, pkgName: String, purchaseItem: PurchaseItem) = + database.addPurchase(account, pkgName, purchaseItem) + + private class PurchaseDB(mContext: Context?) : SQLiteOpenHelper( + mContext, DATABASE_NAME, null, DATABASE_VERSION + ) { + @Synchronized + fun queryPurchases(account: Account, pkgName: String, type: String): List { + val result = mutableListOf() + val cursor = readableDatabase.query( + PURCHASE_TABLE, + null, + "account=? and package_name=? and type=?", + arrayOf(account.name, pkgName, type), + null, + null, + null + ) + cursor?.use { + while (it.moveToNext()) { + val item = PurchaseItem( + it.getString(2), + it.getString(3), + it.getString(1), + it.getString(4), + it.getInt(5), + it.getString(6), + it.getString(7), + it.getLong(8), + it.getLong(9) + ) + result.add(item) + } + } + + return result + } + + @Synchronized + fun updatePurchase(purchaseItem: PurchaseItem): Int { + val upItem = ContentValues() + upItem.put("purchase_state", purchaseItem.purchaseState) + upItem.put("json_data", purchaseItem.jsonData) + upItem.put("signature", purchaseItem.signature) + return writableDatabase.update( + PURCHASE_TABLE, + upItem, + "purchase_token=?", + arrayOf(purchaseItem.purchaseToken) + ) + } + + @Synchronized + fun removePurchase(purchaseToken: String) { + writableDatabase.delete(PURCHASE_TABLE, "purchase_token=?", arrayOf(purchaseToken)) + } + + @Synchronized + fun addPurchase(account: Account, pkgName: String, purchaseItem: PurchaseItem) { + val cv = ContentValues() + cv.put("account", account.name) + cv.put("package_name", pkgName) + cv.put("type", purchaseItem.type) + cv.put("sku", purchaseItem.sku) + cv.put("purchase_token", purchaseItem.purchaseToken) + cv.put("purchase_state", purchaseItem.purchaseState) + cv.put("json_data", purchaseItem.jsonData) + cv.put("signature", purchaseItem.signature) + cv.put("start_at", purchaseItem.startAt) + cv.put("expire_at", purchaseItem.expireAt) + writableDatabase.insertWithOnConflict( + PURCHASE_TABLE, + null, + cv, + SQLiteDatabase.CONFLICT_REPLACE + ) + } + + override fun onCreate(db: SQLiteDatabase) { + db.execSQL(CREATE_TABLE_PURCHASES) + } + + override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {} + + companion object { + private const val TAG = "PurchaseDB" + private const val DATABASE_NAME = "purchase.db" + private const val PURCHASE_TABLE = "purchases" + private const val DATABASE_VERSION = 1 + private const val CREATE_TABLE_PURCHASES = + "CREATE TABLE IF NOT EXISTS $PURCHASE_TABLE ( " + + "account TEXT, " + + "package_name TEXT, " + + "type TEXT, " + + "sku TEXT, " + + "purchase_token TEXT, " + + "purchase_state INTEGER, " + + "json_data TEXT, " + + "signature TEXT, " + + "start_at INTEGER, " + + "expire_at INTEGER, " + + "PRIMARY KEY (purchase_token));" + } + } +} \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/billing/SettingsManager.kt b/vending-app/src/main/java/org/microg/vending/billing/SettingsManager.kt new file mode 100644 index 00000000..8dd10c27 --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/billing/SettingsManager.kt @@ -0,0 +1,31 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.vending.billing + +import android.content.Context +import android.content.SharedPreferences +import android.util.Log +import androidx.preference.PreferenceManager + +// TODO: Better name? +private const val AUTH_STATUS_KEY = "key_auth_status" + +class SettingsManager(private val context: Context) { + private val preferences: SharedPreferences by lazy { + PreferenceManager.getDefaultSharedPreferences(context.applicationContext) + } + + fun setAuthStatus(needAuth: Boolean) { + if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "setAuthStatus: $needAuth") + val editor = preferences.edit() + editor.putBoolean(AUTH_STATUS_KEY, needAuth) + editor.apply() + } + + fun getAuthStatus(): Boolean { + return preferences.getBoolean(AUTH_STATUS_KEY, true) + } +} \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/billing/Utils.kt b/vending-app/src/main/java/org/microg/vending/billing/Utils.kt new file mode 100644 index 00000000..831dd014 --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/billing/Utils.kt @@ -0,0 +1,340 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.vending.billing + +import android.accounts.Account +import android.accounts.AccountManager +import android.annotation.SuppressLint +import android.content.Context +import android.content.IntentFilter +import android.content.pm.PackageManager +import android.icu.util.TimeZone +import android.net.ConnectivityManager +import android.net.Uri +import android.os.Build.VERSION.SDK_INT +import android.os.Bundle +import android.os.SystemClock +import android.provider.Settings +import android.util.Base64 +import android.util.Log +import android.view.WindowManager +import androidx.core.app.ActivityCompat +import androidx.core.os.bundleOf +import com.android.billingclient.api.BillingClient.BillingResponseCode +import org.microg.gms.profile.Build +import org.microg.gms.utils.digest +import org.microg.gms.utils.getExtendedPackageInfo +import org.microg.gms.utils.toBase64 +import org.microg.vending.billing.core.* +import java.util.* +import kotlin.collections.Collection +import kotlin.collections.List +import kotlin.collections.Map +import kotlin.collections.Set +import kotlin.collections.any +import kotlin.collections.filter +import kotlin.collections.firstOrNull +import kotlin.collections.joinToString +import kotlin.collections.map +import kotlin.collections.mutableListOf +import kotlin.collections.mutableMapOf +import kotlin.collections.set +import kotlin.collections.toByteArray +import kotlin.collections.toList +import kotlin.collections.toSet +import kotlin.collections.toTypedArray + +fun Map.toBundle(): Bundle = bundleOf(*this.toList().toTypedArray()) + +/** + * Returns true if the receiving collection contains any of the specified elements. + * + * @param elements the elements to look for in the receiving collection. + * @return true if any element in [elements] is found in the receiving collection. + */ +fun Collection.containsAny(vararg elements: T): Boolean { + return containsAny(elements.toSet()) +} + +/** + * Returns true if the receiving collection contains any of the elements in the specified collection. + * + * @param elements the elements to look for in the receiving collection. + * @return true if any element in [elements] is found in the receiving collection. + */ +fun Collection.containsAny(elements: Collection): Boolean { + val set = if (elements is Set) elements else elements.toSet() + return any(set::contains) +} + +fun String.decodeHex(): ByteArray { + check(length % 2 == 0) { "Must have an even length" } + return chunked(2) + .map { it.toInt(16).toByte() } + .toByteArray() +} + +fun resultBundle(@BillingResponseCode code: Int, msg: String?, data: Bundle = Bundle.EMPTY): Bundle { + val res = bundleOf( + "RESPONSE_CODE" to code, + "DEBUG_MESSAGE" to msg + ) + res.putAll(data) + Log.d(TAG, "Result: $res") + return res +} + +@SuppressLint("MissingPermission") +fun getDeviceIdentifier(context: Context): String { + // TODO: Improve dummy data + val deviceId = DeviceIdentifier.meid /*try { + (context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager?)?.let { + it.subscriberId ?: it.deviceId + } + } catch (e: Exception) { + null + }*/ + if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "getDeviceIdentifier deviceId: $deviceId") + return deviceId.toByteArray(Charsets.UTF_8).digest("SHA-1").toBase64(Base64.URL_SAFE + Base64.NO_WRAP + Base64.NO_PADDING) +} + +fun getGoogleAccount(context: Context, name: String? = null): Account? { + var accounts = + AccountManager.get(context).getAccountsByType(DEFAULT_ACCOUNT_TYPE).toList() + name?.let { accounts = accounts.filter { it.name == name } } + if (accounts.isEmpty()) + return null + return accounts[0] +} + +fun createClient(context: Context, pkgName: String): ClientInfo? { + return try { + val packageInfo = context.packageManager.getExtendedPackageInfo(pkgName) + ClientInfo( + pkgName, + packageInfo.certificates.firstOrNull()?.digest("MD5")?.toBase64(Base64.URL_SAFE + Base64.NO_WRAP + Base64.NO_PADDING) ?: "", + packageInfo.shortVersionCode + ) + } catch (e: Exception) { + if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "createClient", e) + null + } +} + +fun bundleToMap(bundle: Bundle?): Map { + val result = mutableMapOf() + if (bundle == null) + return result + for (key in bundle.keySet()) { + bundle.get(key)?.let { + result[key] = it + } + } + return result +} + +fun getDisplayInfo(context: Context): DisplayMetrics? { + return try { + val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager + if (windowManager != null) { + val displayMetrics = android.util.DisplayMetrics() + windowManager.defaultDisplay.getRealMetrics(displayMetrics) + return DisplayMetrics( + displayMetrics.widthPixels, + displayMetrics.heightPixels, + displayMetrics.xdpi, + displayMetrics.ydpi, + displayMetrics.densityDpi + ) + } + return DisplayMetrics( + context.resources.displayMetrics.widthPixels, + context.resources.displayMetrics.heightPixels, + context.resources.displayMetrics.xdpi, + context.resources.displayMetrics.ydpi, + context.resources.displayMetrics.densityDpi + ) + } catch (e: Exception) { + null + } +} + +// TODO: Improve privacy +fun getBatteryLevel(context: Context): Int { + var batteryLevel = -1; + val intentFilter = IntentFilter("android.intent.action.BATTERY_CHANGED") + context.registerReceiver(null, intentFilter)?.let { + val level = it.getIntExtra("level", -1) + val scale = it.getIntExtra("scale", -1) + if (scale > 0) { + batteryLevel = level * 100 / scale + } + } + if (batteryLevel == -1 && SDK_INT >= 33) { + context.registerReceiver(null, intentFilter, Context.RECEIVER_EXPORTED)?.let { + val level = it.getIntExtra("level", -1) + val scale = it.getIntExtra("scale", -1) + if (scale > 0) { + batteryLevel = level * 100 / scale + } + } + } + return batteryLevel +} + +fun getTelephonyData(context: Context): TelephonyData? { + // TODO: Dummy data + return null /*try { + context.getSystemService(Context.TELEPHONY_SERVICE)?.let { + val telephonyManager = it as TelephonyManager + return TelephonyData( + telephonyManager.simOperatorName!!, + DeviceIdentifier.meid, + telephonyManager.networkOperator!!, + telephonyManager.simOperator!!, + telephonyManager.phoneType + ) + } + } catch (e: Exception) { + if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "getTelephonyData", e) + null + }*/ +} + +fun hasPermissions(context: Context, permissions: List): Boolean { + for (permission in permissions) { + if (ActivityCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED) + return false + } + return true +} + +@SuppressLint("MissingPermission") +fun getLocationData(context: Context): LocationData? { + // TODO: Dummy data + return null /*try { + (context.getSystemService(Context.LOCATION_SERVICE) as LocationManager?)?.let { locationManager -> + if (hasPermissions( + context, + listOf( + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION + ) + ) + ) { + locationManager.getLastKnownLocation("network")?.let { location -> + return LocationData( + location.altitude, + location.latitude, + location.longitude, + location.accuracy, + location.time.toDouble() + ) + } + } else { + null + } + } + } catch (e: Exception) { + if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "getLocationData", e) + null + }*/ +} + +fun getNetworkData(context: Context): NetworkData { + val connectivityManager = + context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager? + var linkDownstreamBandwidth: Long = 0 + var linkUpstreamBandwidth: Long = 0 + // TODO: Dummy data + /* + if (hasPermissions(context, listOf(Manifest.permission.ACCESS_NETWORK_STATE)) && SDK_INT >= 23) { + connectivityManager?.getNetworkCapabilities(connectivityManager.activeNetwork)?.let { + linkDownstreamBandwidth = (it.linkDownstreamBandwidthKbps * 1000 / 8).toLong() + linkUpstreamBandwidth = (it.linkUpstreamBandwidthKbps * 1000 / 8).toLong() + } + } + */ + val isActiveNetworkMetered = connectivityManager?.isActiveNetworkMetered ?: false + val netAddressList = mutableListOf() + // TODO: Dummy data + /*try { + NetworkInterface.getNetworkInterfaces()?.let { enumeration -> + while (true) { + if (!enumeration.hasMoreElements()) { + break + } + val enumeration1 = enumeration.nextElement().inetAddresses + while (enumeration1.hasMoreElements()) { + val inetAddress = enumeration1.nextElement() as InetAddress + if (inetAddress.isLoopbackAddress) { + continue + } + netAddressList.add(inetAddress.hostAddress) + } + } + } + } catch (socketException: NullPointerException) { + if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "getNetworkData:${socketException.message}") + }*/ + return NetworkData( + linkDownstreamBandwidth, + linkUpstreamBandwidth, + isActiveNetworkMetered, + netAddressList + ) +} + +@SuppressLint("HardwareIds") +fun getAndroidId(context: Context): String { + return Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID) ?: "" +} + +fun getUserAgent(): String { + return "Android-Finsky/${Uri.encode(VENDING_VERSION_NAME)} (api=3,versionCode=$VENDING_VERSION_CODE,sdk=${Build.VERSION.SDK_INT},device=${Build.DEVICE},hardware=${Build.HARDWARE},product=${Build.PRODUCT},platformVersionRelease=${Build.VERSION.RELEASE},model=${Uri.encode(Build.MODEL)},buildId=${Build.ID},isWideScreen=0,supportedAbis=${Build.SUPPORTED_ABIS.joinToString(";")})" +} + +fun createDeviceEnvInfo(context: Context): DeviceEnvInfo? { + try { + val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0) + return DeviceEnvInfo( + gpVersionCode = VENDING_VERSION_CODE, + gpVersionName = VENDING_VERSION_NAME, + gpPkgName = VENDING_PACKAGE_NAME, + androidId = getAndroidId(context), + biometricSupport = true, + biometricSupportCDD = true, + deviceId = getDeviceIdentifier(context), + serialNo = Build.SERIAL ?: "", + locale = Locale.getDefault(), + userAgent = getUserAgent(), + gpLastUpdateTime = packageInfo.lastUpdateTime, + gpFirstInstallTime = packageInfo.firstInstallTime, + gpSourceDir = packageInfo.applicationInfo.sourceDir!!, + device = Build.DEVICE!!, + displayMetrics = getDisplayInfo(context), + telephonyData = getTelephonyData(context), + product = Build.PRODUCT!!, + model = Build.MODEL!!, + manufacturer = Build.MANUFACTURER!!, + fingerprint = Build.FINGERPRINT!!, + release = Build.VERSION.RELEASE!!, + brand = Build.BRAND!!, + batteryLevel = getBatteryLevel(context), + timeZoneOffset = if (SDK_INT >= 24) TimeZone.getDefault().rawOffset.toLong() else 0, + locationData = getLocationData(context), + isAdbEnabled = false, //Settings.Global.getInt(context.contentResolver, "adb_enabled", 0) == 1, + installNonMarketApps = true, //Settings.Secure.getInt(context.contentResolver, "install_non_market_apps", 0) == 1, + networkData = getNetworkData(context), + uptimeMillis = SystemClock.uptimeMillis(), + timeZoneDisplayName = if (SDK_INT >= 24) TimeZone.getDefault().displayName!! else "", + googleAccounts = AccountManager.get(context).getAccountsByType(DEFAULT_ACCOUNT_TYPE).map { it.name } + ) + } catch (e: Exception) { + if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "createDeviceInfo", e) + return null + } +} \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/billing/core/AcknowledgePurchaseParams.kt b/vending-app/src/main/java/org/microg/vending/billing/core/AcknowledgePurchaseParams.kt new file mode 100644 index 00000000..85c09057 --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/billing/core/AcknowledgePurchaseParams.kt @@ -0,0 +1,7 @@ +package org.microg.vending.billing.core + +data class AcknowledgePurchaseParams( + val apiVersion: Int, + val purchaseToken: String, + val extraParams: Map = emptyMap() +) \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/billing/core/AcknowledgePurchaseResult.kt b/vending-app/src/main/java/org/microg/vending/billing/core/AcknowledgePurchaseResult.kt new file mode 100644 index 00000000..5843d53b --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/billing/core/AcknowledgePurchaseResult.kt @@ -0,0 +1,36 @@ +package org.microg.vending.billing.core + +import org.microg.vending.billing.proto.AcknowledgePurchaseResponse + +class AcknowledgePurchaseResult( + val purchaseItem: PurchaseItem? = null, + resultMap: Map = mapOf( + "RESPONSE_CODE" to 0, + "DEBUG_MESSAGE" to "" + ) +) : IAPResult(resultMap) { + companion object { + fun parseFrom( + response: AcknowledgePurchaseResponse? + ): AcknowledgePurchaseResult { + if (response == null) { + throw NullPointerException("response is null") + } + if (response.failedResponse != null) { + return AcknowledgePurchaseResult( + null, + mapOf( + "RESPONSE_CODE" to response.failedResponse.statusCode, + "DEBUG_MESSAGE" to response.failedResponse.msg + ) + ) + } + if (response.purchaseItem == null) { + throw NullPointerException("AcknowledgePurchaseResponse PurchaseItem is null") + } + if (response.purchaseItem.purchaseItemData.size != 1) + throw IllegalStateException("AcknowledgePurchaseResult purchase item count != 1") + return AcknowledgePurchaseResult(parsePurchaseItem(response.purchaseItem).getOrNull(0)) + } + } +} \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/billing/core/AcquireParams.kt b/vending-app/src/main/java/org/microg/vending/billing/core/AcquireParams.kt new file mode 100644 index 00000000..a2339e9d --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/billing/core/AcquireParams.kt @@ -0,0 +1,9 @@ +package org.microg.vending.billing.core + +data class AcquireParams( + val buyFlowParams: BuyFlowParams, + val actionContext: List = emptyList(), + val droidGuardResult: String? = null, + val authToken: String? = null, + var lastAcquireResult: AcquireResult? = null +) \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/billing/core/AcquireResult.kt b/vending-app/src/main/java/org/microg/vending/billing/core/AcquireResult.kt new file mode 100644 index 00000000..5e72c20c --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/billing/core/AcquireResult.kt @@ -0,0 +1,29 @@ +package org.microg.vending.billing.core + +import org.microg.vending.billing.core.ui.AcquireParsedResult +import org.microg.vending.billing.core.ui.parseAcquireResponse +import org.microg.vending.billing.proto.AcquireRequest +import org.microg.vending.billing.proto.AcquireResponse + +data class AcquireResult( + val acquireParsedResult: AcquireParsedResult, + val acquireRequest: AcquireRequest, + val acquireResponse: AcquireResponse, +) { + companion object { + fun parseFrom( + acquireParams: AcquireParams, + acquireRequest: AcquireRequest, + acquireResponse: AcquireResponse? + ): AcquireResult { + if (acquireResponse == null) { + throw NullPointerException("AcquireResponse is null") + } + return AcquireResult( + parseAcquireResponse(acquireParams, acquireResponse), + acquireRequest, + acquireResponse + ) + } + } +} \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/billing/core/AuthData.kt b/vending-app/src/main/java/org/microg/vending/billing/core/AuthData.kt new file mode 100644 index 00000000..1eb75fef --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/billing/core/AuthData.kt @@ -0,0 +1,11 @@ +package org.microg.vending.billing.core + +data class AuthData( + val email: String, + val authToken: String, + val gsfId: String = "", + val deviceCheckInConsistencyToken: String = "", + val deviceConfigToken: String = "", + val experimentsConfigToken: String = "", + val dfeCookie: String = "" +) diff --git a/vending-app/src/main/java/org/microg/vending/billing/core/BuyFlowParams.kt b/vending-app/src/main/java/org/microg/vending/billing/core/BuyFlowParams.kt new file mode 100644 index 00000000..5c85720e --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/billing/core/BuyFlowParams.kt @@ -0,0 +1,15 @@ +package org.microg.vending.billing.core + +data class BuyFlowParams( + val apiVersion: Int, + val sku: String, + val skuType: String, + val developerPayload: String = "", + val sdkVersion: String = "", + val needAuth: Boolean = false, + val skuParams: Map = emptyMap(), + val skuSerializedDockIdList: List? = null, + val skuOfferIdTokenList: List? = null, + val oldSkuPurchaseToken: String? = null, + val oldSkuPurchaseId: String? = null +) \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/billing/core/ClientInfo.kt b/vending-app/src/main/java/org/microg/vending/billing/core/ClientInfo.kt new file mode 100644 index 00000000..0a807395 --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/billing/core/ClientInfo.kt @@ -0,0 +1,7 @@ +package org.microg.vending.billing.core + +data class ClientInfo( + val pkgName: String, + val signatureMD5: String, + val versionCode: Int +) \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/billing/core/ConsumePurchaseParams.kt b/vending-app/src/main/java/org/microg/vending/billing/core/ConsumePurchaseParams.kt new file mode 100644 index 00000000..14fa547c --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/billing/core/ConsumePurchaseParams.kt @@ -0,0 +1,7 @@ +package org.microg.vending.billing.core + +data class ConsumePurchaseParams( + val apiVersion: Int, + val purchaseToken: String, + val extraParams: Map = emptyMap() +) \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/billing/core/ConsumePurchaseResult.kt b/vending-app/src/main/java/org/microg/vending/billing/core/ConsumePurchaseResult.kt new file mode 100644 index 00000000..19dedcc2 --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/billing/core/ConsumePurchaseResult.kt @@ -0,0 +1,30 @@ +package org.microg.vending.billing.core + +import org.microg.vending.billing.proto.ConsumePurchaseResponse + + +class ConsumePurchaseResult( + resultMap: Map = mapOf( + "RESPONSE_CODE" to 0, + "DEBUG_MESSAGE" to "" + ) +) : IAPResult(resultMap) { + companion object { + fun parseFrom( + consumePurchaseResponse: ConsumePurchaseResponse? + ): ConsumePurchaseResult { + if (consumePurchaseResponse == null) { + throw NullPointerException("consumePurchaseResponse is null") + } + if (consumePurchaseResponse.failedResponse != null) { + return ConsumePurchaseResult( + mapOf( + "RESPONSE_CODE" to consumePurchaseResponse.failedResponse.statusCode, + "DEBUG_MESSAGE" to consumePurchaseResponse.failedResponse.msg + ) + ) + } + return ConsumePurchaseResult() + } + } +} \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/billing/core/DeviceEnvInfo.kt b/vending-app/src/main/java/org/microg/vending/billing/core/DeviceEnvInfo.kt new file mode 100644 index 00000000..67d54292 --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/billing/core/DeviceEnvInfo.kt @@ -0,0 +1,68 @@ +package org.microg.vending.billing.core + +import java.util.Locale + +data class DeviceEnvInfo( + val gpVersionCode: Long, + val gpVersionName: String, + val gpPkgName: String, + val gpLastUpdateTime: Long, + val gpFirstInstallTime: Long, + val gpSourceDir: String, + val androidId: String, + val biometricSupport: Boolean, + val biometricSupportCDD: Boolean, + val deviceId: String, + val serialNo: String, + val locale: Locale, + val userAgent: String, + val device: String, + val displayMetrics: DisplayMetrics?, + val telephonyData: TelephonyData?, + val locationData: LocationData?, + val networkData: NetworkData?, + val product: String, + val model: String, + val manufacturer: String, + val fingerprint: String, + val release: String, + val brand: String, + val batteryLevel: Int, + val timeZoneOffset: Long, + val isAdbEnabled: Boolean, + val installNonMarketApps: Boolean, + val uptimeMillis: Long, + val timeZoneDisplayName: String, + val googleAccounts: List +) + +data class DisplayMetrics( + val widthPixels: Int, + val heightPixels: Int, + val xdpi: Float, + val ydpi: Float, + val densityDpi: Int +) + +data class TelephonyData( + val simOperatorName: String, + val phoneDeviceId: String, + val networkOperator: String, + val simOperator: String, + val phoneType: Int = -1 +) + +data class LocationData( + val altitude: Double, + val latitude: Double, + val longitude: Double, + val accuracy: Float, + val time: Double +) + +data class NetworkData( + val linkDownstreamBandwidth: Long, + val linkUpstreamBandwidth: Long, + val isActiveNetworkMetered: Boolean, + val netAddressList: List, +) \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/billing/core/GetPurchaseHistoryParams.kt b/vending-app/src/main/java/org/microg/vending/billing/core/GetPurchaseHistoryParams.kt new file mode 100644 index 00000000..4a3d65f0 --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/billing/core/GetPurchaseHistoryParams.kt @@ -0,0 +1,8 @@ +package org.microg.vending.billing.core + +data class GetPurchaseHistoryParams( + val apiVersion: Int, + val type: String, + val continuationToken: String? = null, + val extraParams: Map = emptyMap() +) \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/billing/core/GetPurchaseHistoryResult.kt b/vending-app/src/main/java/org/microg/vending/billing/core/GetPurchaseHistoryResult.kt new file mode 100644 index 00000000..801c10a9 --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/billing/core/GetPurchaseHistoryResult.kt @@ -0,0 +1,53 @@ +package org.microg.vending.billing.core + +import org.microg.vending.billing.proto.PurchaseHistoryResponse + +class GetPurchaseHistoryResult( + val purchaseHistoryList: List?, + val continuationToken: String?, + resultMap: Map = mapOf( + "RESPONSE_CODE" to 0, + "DEBUG_MESSAGE" to "" + ) +) : IAPResult(resultMap) { + companion object { + fun parseFrom( + response: PurchaseHistoryResponse? + ): GetPurchaseHistoryResult { + if (response == null) { + throw NullPointerException("PurchaseHistoryResponse is null") + } + if (response.failedResponse != null) { + return GetPurchaseHistoryResult( + null, + null, + mapOf( + "RESPONSE_CODE" to response.failedResponse.statusCode, + "DEBUG_MESSAGE" to response.failedResponse.msg + ) + ) + } + if (response.productId.size != response.purchaseJson.size || response.purchaseJson.size != response.signature.size) { + throw IllegalStateException("GetPurchaseHistoryResult item count error") + } + val purchaseHistoryList = mutableListOf() + var continuationToken: String? = null + for (cnt in 0 until response.productId.size) { + purchaseHistoryList.add( + PurchaseHistoryItem( + response.productId[cnt], + response.purchaseJson[cnt], + response.signature[cnt] + ) + ) + } + if (!response.continuationToken.isNullOrEmpty()) { + continuationToken = response.continuationToken + } + + return GetPurchaseHistoryResult(purchaseHistoryList, continuationToken) + } + } + + class PurchaseHistoryItem(val sku: String, val jsonData: String, val signature: String) +} \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/billing/core/GetSkuDetailsParams.kt b/vending-app/src/main/java/org/microg/vending/billing/core/GetSkuDetailsParams.kt new file mode 100644 index 00000000..5ea1f175 --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/billing/core/GetSkuDetailsParams.kt @@ -0,0 +1,10 @@ +package org.microg.vending.billing.core + +data class GetSkuDetailsParams( + val apiVersion: Int, + val skuType: String, + val skuIdList: List, + val skuPkgName: String = "", + val sdkVersion: String = "", + val multiOfferSkuDetail: Map = emptyMap() +) \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/billing/core/GetSkuDetailsResult.kt b/vending-app/src/main/java/org/microg/vending/billing/core/GetSkuDetailsResult.kt new file mode 100644 index 00000000..c6d19633 --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/billing/core/GetSkuDetailsResult.kt @@ -0,0 +1,42 @@ +package org.microg.vending.billing.core + +import org.microg.vending.billing.proto.DocId +import org.microg.vending.billing.proto.SkuDetailsResponse + + +class GetSkuDetailsResult private constructor( + val skuDetailsList: List, + resultMap: Map = mapOf("RESPONSE_CODE" to 0, "DEBUG_MESSAGE" to "") +) : IAPResult(resultMap) { + companion object { + fun parseFrom(skuDetailsResponse: SkuDetailsResponse?): GetSkuDetailsResult { + if (skuDetailsResponse == null) { + throw NullPointerException("SkuDetailsResponse is null") + } + if (skuDetailsResponse.failedResponse != null) { + return GetSkuDetailsResult( + emptyList(), + mapOf( + "RESPONSE_CODE" to skuDetailsResponse.failedResponse.statusCode, + "DEBUG_MESSAGE" to skuDetailsResponse.failedResponse.msg + ) + ) + } + val skuDetailsList = + skuDetailsResponse.details.filter { it.skuDetails.isNotBlank() && it.skuInfo != null} + .map { skuDetails -> + SkuDetailsItem( + skuDetails.skuDetails, + skuDetails.skuInfo!!.skuItem.associate { it.token to it.docId } + ) + } + return GetSkuDetailsResult(skuDetailsList) + } + } + + + data class SkuDetailsItem( + val jsonDetails: String, + val docIdMap: Map + ) +} \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/billing/core/GooglePlayApi.kt b/vending-app/src/main/java/org/microg/vending/billing/core/GooglePlayApi.kt new file mode 100644 index 00000000..046b9ca9 --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/billing/core/GooglePlayApi.kt @@ -0,0 +1,14 @@ +package org.microg.vending.billing.core + +class GooglePlayApi { + companion object { + const val URL_BASE = "https://play-fe.googleapis.com" + const val URL_FDFE = "$URL_BASE/fdfe" + const val URL_SKU_DETAILS = "$URL_FDFE/skuDetails" + const val URL_EES_ACQUIRE = "$URL_FDFE/ees/acquire" + const val URL_ACKNOWLEDGE_PURCHASE = "$URL_FDFE/acknowledgePurchase" + const val URL_CONSUME_PURCHASE = "$URL_FDFE/consumePurchase" + const val URL_GET_PURCHASE_HISTORY = "$URL_FDFE/inAppPurchaseHistory" + const val URL_AUTH_PROOF_TOKENS = "https://www.googleapis.com/reauth/v1beta/users/me/reauthProofTokens" + } +} \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/billing/core/HeaderProvider.kt b/vending-app/src/main/java/org/microg/vending/billing/core/HeaderProvider.kt new file mode 100644 index 00000000..b8ce3328 --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/billing/core/HeaderProvider.kt @@ -0,0 +1,40 @@ +package org.microg.vending.billing.core + +object HeaderProvider { + fun getBaseHeaders(authData: AuthData, deviceInfo: DeviceEnvInfo): MutableMap { + val headers: MutableMap = HashMap() + headers["Authorization"] = "Bearer " + authData.authToken + headers["User-Agent"] = deviceInfo.userAgent + return headers + } + + fun getDefaultHeaders(authData: AuthData, deviceInfo: DeviceEnvInfo): MutableMap { + val headers: MutableMap = HashMap() + headers["Authorization"] = "Bearer " + authData.authToken + headers["User-Agent"] = deviceInfo.userAgent + headers["X-DFE-Device-Id"] = authData.gsfId + headers["Accept-Language"] = "${deviceInfo.locale.language}-${deviceInfo.locale.country}" + headers["X-Limit-Ad-Tracking-Enabled"] = "true" + headers["X-DFE-Network-Type"] = "4" + headers["X-DFE-Client-Id"] = "am-google" + + // TODO: Magic constants? + headers["X-DFE-Phenotype"] = + "H4sIAAAAAAAAAOOKcXb0DQ4oNzCoKNV1c0zMsywL9PVwqvBPcsr2TykJ8HUv9gx1La6I9Dcw9k7xTYtIMnasSopIq0g0SI8IdwxwDbfIygxw8U-PdPR1THML1DXNS_L0yffOinRxtLWVYgAAjtXkomAAAAA" + headers["X-DFE-Encoded-Targets"] = + "CAEaSuMFBdCPgQYJxAIED+cBfS+6AVYBIQojDSI3hAEODGxYvQGMAhRMWQEVWxniBQSSAjycAuESkgrgBeAfgCv4KI8VgxHqGNxrRbkI" + + headers["X-DFE-Request-Params"] = "timeoutMs=4000" + headers["X-Ad-Id"] = "00000000-0000-0000-0000-000000000000" + headers["Connection"] = "Keep-Alive" + if (deviceInfo.androidId.isNotBlank()) + headers["x-public-android-id"] = deviceInfo.androidId + if (authData.dfeCookie.isNotBlank()) + headers["x-dfe-cookie"] = authData.dfeCookie + if (authData.deviceCheckInConsistencyToken.isNotBlank()) { + headers["X-DFE-Device-Checkin-Consistency-Token"] = + authData.deviceCheckInConsistencyToken + } + return headers + } +} \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/billing/core/HttpClient.kt b/vending-app/src/main/java/org/microg/vending/billing/core/HttpClient.kt new file mode 100644 index 00000000..18d2ba55 --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/billing/core/HttpClient.kt @@ -0,0 +1,140 @@ +package org.microg.vending.billing.core + +import android.content.Context +import android.net.Uri +import com.android.volley.* +import com.android.volley.toolbox.HttpHeaderParser +import com.android.volley.toolbox.JsonObjectRequest +import com.android.volley.toolbox.Volley +import com.squareup.wire.Message +import com.squareup.wire.ProtoAdapter +import org.json.JSONObject +import org.microg.gms.utils.singleInstanceOf +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine + +private const val POST_TIMEOUT = 15000 + +class HttpClient(context: Context) { + private val requestQueue = singleInstanceOf { Volley.newRequestQueue(context.applicationContext) } + + suspend fun get( + url: String, + headers: Map = emptyMap(), + params: Map = emptyMap(), + adapter: ProtoAdapter, + cache: Boolean = true + ): O = suspendCoroutine { continuation -> + val uriBuilder = Uri.parse(url).buildUpon() + params.forEach { + uriBuilder.appendQueryParameter(it.key, it.value) + } + requestQueue.add(object : Request(Method.GET, uriBuilder.build().toString(), null) { + override fun parseNetworkResponse(response: NetworkResponse): Response { + if (response.statusCode != 200) throw VolleyError(response) + return Response.success(adapter.decode(response.data), HttpHeaderParser.parseCacheHeaders(response)) + } + + override fun deliverResponse(response: O) { + continuation.resume(response) + } + + override fun deliverError(error: VolleyError) { + continuation.resumeWithException(error) + } + + override fun getHeaders(): Map = headers + }.setShouldCache(cache)) + } + + + + suspend fun , O> post( + url: String, + headers: Map = emptyMap(), + params: Map = emptyMap(), + payload: I, + adapter: ProtoAdapter, + cache: Boolean = false + ): O = suspendCoroutine { continuation -> + val uriBuilder = Uri.parse(url).buildUpon() + params.forEach { + uriBuilder.appendQueryParameter(it.key, it.value) + } + requestQueue.add(object : Request(Method.POST, uriBuilder.build().toString(), null) { + override fun parseNetworkResponse(response: NetworkResponse): Response { + if (response.statusCode != 200) throw VolleyError(response) + return Response.success(adapter.decode(response.data), HttpHeaderParser.parseCacheHeaders(response)) + } + + override fun deliverResponse(response: O) { + continuation.resume(response) + } + + override fun deliverError(error: VolleyError) { + continuation.resumeWithException(error) + } + + override fun getHeaders(): Map = headers + override fun getBody(): ByteArray = payload.encode() + override fun getBodyContentType(): String = "application/x-protobuf" + }.setShouldCache(cache).setRetryPolicy(DefaultRetryPolicy(POST_TIMEOUT, 0, 0.0F))) + } + + suspend fun post( + url: String, + headers: Map = emptyMap(), + params: Map = emptyMap(), + payload: JSONObject, + cache: Boolean = false + ): JSONObject = suspendCoroutine { continuation -> + val uriBuilder = Uri.parse(url).buildUpon() + params.forEach { + uriBuilder.appendQueryParameter(it.key, it.value) + } + requestQueue.add(object : JsonObjectRequest(Method.POST, uriBuilder.build().toString(), payload, null, null) { + + override fun deliverResponse(response: JSONObject) { + continuation.resume(response) + } + + override fun deliverError(error: VolleyError) { + continuation.resumeWithException(error) + } + + override fun getHeaders(): Map = headers + }.setShouldCache(cache).setRetryPolicy(DefaultRetryPolicy(POST_TIMEOUT, 0, 0.0F))) + } + + suspend fun post( + url: String, + headers: Map = emptyMap(), + params: Map = emptyMap(), + form: Map = emptyMap(), + adapter: ProtoAdapter, + cache: Boolean = false + ): O = suspendCoroutine { continuation -> + val uriBuilder = Uri.parse(url).buildUpon() + params.forEach { + uriBuilder.appendQueryParameter(it.key, it.value) + } + requestQueue.add(object : Request(Method.POST, uriBuilder.build().toString(), null) { + override fun parseNetworkResponse(response: NetworkResponse): Response { + if (response.statusCode != 200) throw VolleyError(response) + return Response.success(adapter.decode(response.data), HttpHeaderParser.parseCacheHeaders(response)) + } + + override fun deliverResponse(response: O) { + continuation.resume(response) + } + + override fun deliverError(error: VolleyError) { + continuation.resumeWithException(error) + } + + override fun getHeaders(): Map = headers + override fun getParams(): Map = form + }.setShouldCache(cache).setRetryPolicy(DefaultRetryPolicy(POST_TIMEOUT, 0, 0.0F))) + } +} \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/billing/core/IAPCacheManager.kt b/vending-app/src/main/java/org/microg/vending/billing/core/IAPCacheManager.kt new file mode 100644 index 00000000..53b8cb3a --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/billing/core/IAPCacheManager.kt @@ -0,0 +1,34 @@ +package org.microg.vending.billing.core + +import android.util.LruCache +import org.microg.gms.utils.digest +import org.microg.gms.utils.toHexString + +operator fun LruCache.set(key: K, value: V) { put(key, value) } + +class CacheEntry( + val data: ByteArray, + val expiredAt: Long +) + +class IAPCacheManager(maxSize: Int = 1024, private val expireMs: Int = 7200000) { + private val lruCache = LruCache(maxSize) + + @Synchronized + fun get(requestBody: ByteArray): ByteArray? { + val entry = lruCache[calculateHash(requestBody)] + if (entry == null || entry.expiredAt < System.currentTimeMillis()) + return null + return entry.data + } + + @Synchronized + fun put(requestBody: ByteArray, responseData: ByteArray) { + val key = calculateHash(requestBody) + lruCache[key] = CacheEntry(responseData, System.currentTimeMillis() + expireMs) + } + + private fun calculateHash(body: ByteArray): String { + return body.digest("SHA-256").toHexString() + } +} diff --git a/vending-app/src/main/java/org/microg/vending/billing/core/IAPCore.kt b/vending-app/src/main/java/org/microg/vending/billing/core/IAPCore.kt new file mode 100644 index 00000000..c4cc7bc7 --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/billing/core/IAPCore.kt @@ -0,0 +1,322 @@ +package org.microg.vending.billing.core + +import android.content.Context +import android.util.Base64 +import com.android.vending.Timestamp +import org.json.JSONObject +import org.microg.gms.utils.toBase64 +import org.microg.vending.billing.proto.* +import java.io.IOException +import java.util.concurrent.TimeUnit + +private val skuDetailsCache = IAPCacheManager(2048) + +class IAPCore( + private val context: Context, + private val deviceInfo: DeviceEnvInfo, + private val clientInfo: ClientInfo, + private val authData: AuthData +) { + suspend fun requestAuthProofToken(password: String): String { + return HttpClient(context).post( + GooglePlayApi.URL_AUTH_PROOF_TOKENS, + headers = HeaderProvider.getBaseHeaders(authData, deviceInfo), + payload = JSONObject().apply { + put("credentialType", "password") + put("credential", password) + } + ).getString("encodedRapt") + } + + suspend fun getSkuDetails(params: GetSkuDetailsParams): GetSkuDetailsResult { + val builder = SkuDetailsRequest.Builder() + builder.apply { + apiVersion = params.apiVersion + type = params.skuType + package_ = clientInfo.pkgName + isWifi = true + skuPackage = params.skuPkgName + skuId = params.skuIdList + val skuDetailsExtraBuilder = SkuDetailsExtra.Builder() + skuDetailsExtraBuilder.apply { + version = params.sdkVersion + } + skuDetailsExtra = skuDetailsExtraBuilder.build() + + val multiOfferSkuDetailTemp: MutableList = mutableListOf() + params.multiOfferSkuDetail.forEach { + multiOfferSkuDetailTemp.add( + when (val value = it.value) { + is Boolean -> { + val multiOfferSkuDetailBuilder = MultiOfferSkuDetail.Builder() + multiOfferSkuDetailBuilder.apply { + key = it.key + bv = value + } + multiOfferSkuDetailBuilder.build() + } + + is Long -> { + val multiOfferSkuDetailBuilder = MultiOfferSkuDetail.Builder() + multiOfferSkuDetailBuilder.apply { + key = it.key + iv = value + } + multiOfferSkuDetailBuilder.build() + } + + is Int -> { + val multiOfferSkuDetailBuilder = MultiOfferSkuDetail.Builder() + multiOfferSkuDetailBuilder.apply { + key = it.key + iv = value.toLong() + } + multiOfferSkuDetailBuilder.build() + } + + else -> { + val multiOfferSkuDetailBuilder = MultiOfferSkuDetail.Builder() + multiOfferSkuDetailBuilder.apply { + key = it.key + sv = value.toString() + } + multiOfferSkuDetailBuilder.build() + } + } + ) + } + this.multiOfferSkuDetail = multiOfferSkuDetailTemp + } + val skuDetailsRequest = builder.build() + return try { + val requestBody = skuDetailsRequest.encode() + val cacheEntry = skuDetailsCache.get(requestBody) + if (cacheEntry != null) { + return GetSkuDetailsResult.parseFrom(ResponseWrapper.ADAPTER.decode(cacheEntry).payload?.skuDetailsResponse) + } + val response = HttpClient(context).post( + GooglePlayApi.URL_SKU_DETAILS, + headers = HeaderProvider.getDefaultHeaders(authData, deviceInfo), + payload = skuDetailsRequest, + adapter = ResponseWrapper.ADAPTER + ) + skuDetailsCache.put(requestBody, response.encode()) + GetSkuDetailsResult.parseFrom(response.payload?.skuDetailsResponse) + } catch (e: Exception) { + throw RuntimeException("Network request failed. message=${e.message}") + } + } + + private fun createAcquireRequest(params: AcquireParams): AcquireRequest { + val theme = 2 + + val skuPackageName = params.buyFlowParams.skuParams["skuPackageName"] ?: clientInfo.pkgName + val docId = if (params.buyFlowParams.skuSerializedDockIdList?.isNotEmpty() == true) { + val sDocIdBytes = Base64.decode(params.buyFlowParams.skuSerializedDockIdList[0], Base64.URL_SAFE + Base64.NO_WRAP) + DocId.ADAPTER.decode(sDocIdBytes) + } else { + val docIdBuilder = DocId.Builder() + docIdBuilder.apply { + backendDocId = + "${params.buyFlowParams.skuType}:$skuPackageName:${params.buyFlowParams.sku}" + type = getSkuType(params.buyFlowParams.skuType) + backend = 3 + } + docIdBuilder.build() + } + val documentInfo = DocumentInfo.Builder().apply { + this.docId = docId + this.unknown2 = 1 + if (params.buyFlowParams.skuOfferIdTokenList?.isNotEmpty() == true) { + if (params.buyFlowParams.skuOfferIdTokenList[0].isNotBlank()) + this.token14 = params.buyFlowParams.skuOfferIdTokenList[0] + } + }.build() + + val authFrequency = if (params.buyFlowParams.needAuth) 0 else 3 + return AcquireRequest.Builder().apply { + this.documentInfo = documentInfo + this.clientInfo = org.microg.vending.billing.proto.ClientInfo.Builder().apply { + this.apiVersion = params.buyFlowParams.apiVersion + this.package_ = this@IAPCore.clientInfo.pkgName + this.versionCode = this@IAPCore.clientInfo.versionCode + this.signatureMD5 = this@IAPCore.clientInfo.signatureMD5 + this.skuParamList = mapToSkuParamList(params.buyFlowParams.skuParams) + this.unknown8 = 1 + this.installerPackage = deviceInfo.gpPkgName + this.unknown10 = false + this.unknown11 = false + this.unknown15 = UnkMessage1.Builder().apply { + this.unknown1 = UnkMessage2.Builder().apply { + this.unknown1 = 1 + }.build() + }.build() + this.versionCode1 = this@IAPCore.clientInfo.versionCode + if (params.buyFlowParams.oldSkuPurchaseToken?.isNotBlank() == true) + this.oldSkuPurchaseToken = params.buyFlowParams.oldSkuPurchaseToken + if (params.buyFlowParams.oldSkuPurchaseId?.isNotBlank() == true) + this.oldSkuPurchaseId = params.buyFlowParams.oldSkuPurchaseId + }.build() + this.clientTokenB64 = + createClientToken(this@IAPCore.deviceInfo, this@IAPCore.authData) + this.deviceAuthInfo = DeviceAuthInfo.Builder().apply { + this.canAuthenticate = true + this.unknown5 = 1 + this.unknown9 = true + this.authFrequency = authFrequency + this.itemColor = ItemColor.Builder().apply { + this.androidAppsColor = -16735885 + this.booksColor = -11488012 + this.musicColor = -45771 + this.moviesColor = -52375 + this.newsStandColor = -7686920 + }.build() + }.build() + this.unknown12 = UnkMessage5.Builder().apply { + this.unknown1 = 9 + }.build() + this.deviceIDBase64 = deviceInfo.deviceId + this.newAcquireCacheKey = getAcquireCacheKey( + this@IAPCore.deviceInfo, + this@IAPCore.authData.email, + listOf( + CKDocument.Builder().apply { + this.docId = docId + this.token3 = documentInfo.token3 + this.token14 = documentInfo.token14 + this.unknown3 = 1 + }.build() + ), + this@IAPCore.clientInfo.pkgName, + mapOf( + "enablePendingPurchases" to (params.buyFlowParams.skuParams["enablePendingPurchases"] + ?: false).toString() + ), + authFrequency + ) + this.nonce = createNonce() + this.theme = theme + this.ts = Timestamp.Builder().apply { + val ts = System.currentTimeMillis() + this.seconds = TimeUnit.MILLISECONDS.toSeconds(ts) + this.nanos = ((ts + TimeUnit.HOURS.toMillis(1L)) % 1000L * 1000000L).toInt() + }.build() + }.build() + } + + suspend fun doAcquireRequest(params: AcquireParams): AcquireResult { + val acquireRequest = + if (params.lastAcquireResult == null) { + createAcquireRequest(params) + } else { + params.lastAcquireResult!!.acquireRequest.copy().newBuilder().apply { + this.serverContextToken = + params.lastAcquireResult!!.acquireResponse.serverContextToken + this.actionContext = (params.actionContext.toByteStringList()) + if (params.lastAcquireResult!!.acquireRequest.deviceAuthInfo != null) { + this.deviceAuthInfo = params.lastAcquireResult!!.acquireRequest.deviceAuthInfo!!.copy().newBuilder().apply { + if (params.droidGuardResult?.isNotBlank() == true) { + this.droidGuardPayload = params.droidGuardResult + } + }.build() + } + val authTokensTemp = mutableMapOf() + params.authToken?.let { + authTokensTemp["rpt"] = it + + } + this.authTokens = authTokensTemp + this.ts = Timestamp.Builder().apply { + val ts = System.currentTimeMillis() + this.seconds = TimeUnit.MILLISECONDS.toSeconds(ts) + this.nanos = ((ts + TimeUnit.HOURS.toMillis(1L)) % 1000L * 1000000L).toInt() + }.build() + }.build() + } + return try { + val response = HttpClient(context).post( + GooglePlayApi.URL_EES_ACQUIRE, + headers = HeaderProvider.getDefaultHeaders(authData, deviceInfo), + params = mapOf("theme" to acquireRequest.theme.toString()), + payload = acquireRequest, + ResponseWrapper.ADAPTER + ) + AcquireResult.parseFrom(params, acquireRequest, response.payload?.acquireResponse) + } catch (e: Exception) { + throw RuntimeException("Network request failed. message=${e.message}") + } + } + + suspend fun consumePurchase(params: ConsumePurchaseParams): ConsumePurchaseResult { + val iabx = IABX.Builder().apply { + this.skuParam = mapToSkuParamList(params.extraParams) + }.build().encode().toBase64(Base64.URL_SAFE + Base64.NO_WRAP) + val request = mapOf( + "pt" to params.purchaseToken, + "ot" to "1", + "shpn" to clientInfo.pkgName, + "iabx" to iabx + ) + + return try { + val response = HttpClient(context).post( + GooglePlayApi.URL_CONSUME_PURCHASE, + headers = HeaderProvider.getDefaultHeaders(authData, deviceInfo), + form = request, + adapter = ResponseWrapper.ADAPTER + ) + ConsumePurchaseResult.parseFrom(response.payload?.consumePurchaseResponse) + } catch (e: Exception) { + throw RuntimeException("Network request failed. message=${e.message}") + } + } + + suspend fun acknowledgePurchase(params: AcknowledgePurchaseParams): AcknowledgePurchaseResult { + val acknowledgePurchaseRequest = AcknowledgePurchaseRequest.Builder().apply { + this.purchaseToken = params.purchaseToken + params.extraParams["developerPayload"]?.let { + this.developerPayload = it as String + } + }.build() + + return try { + val response = HttpClient(context).post( + GooglePlayApi.URL_ACKNOWLEDGE_PURCHASE, + headers = HeaderProvider.getDefaultHeaders(authData, deviceInfo), + payload = acknowledgePurchaseRequest, + adapter = ResponseWrapper.ADAPTER + ) + AcknowledgePurchaseResult.parseFrom(response.payload?.acknowledgePurchaseResponse) + } catch (e: Exception) { + throw RuntimeException("Network request failed. message=${e.message}") + } + } + + suspend fun getPurchaseHistory(params: GetPurchaseHistoryParams): GetPurchaseHistoryResult { + val reqParams = mutableMapOf( + "bav" to params.apiVersion.toString(), + "shpn" to clientInfo.pkgName, + "iabt" to params.type + ) + if (!params.continuationToken.isNullOrEmpty()) { + reqParams["ctntkn"] = params.continuationToken + } + if (params.extraParams.isNotEmpty()) { + reqParams["iabx"] = IABX.Builder().apply { + this.skuParam = mapToSkuParamList(params.extraParams) + }.build().encode().toBase64(Base64.URL_SAFE + Base64.NO_WRAP) + } + + return try { + val response = HttpClient(context).get( + GooglePlayApi.URL_GET_PURCHASE_HISTORY, + HeaderProvider.getDefaultHeaders(authData, deviceInfo), + reqParams, + ResponseWrapper.ADAPTER + ) + GetPurchaseHistoryResult.parseFrom(response.payload?.purchaseHistoryResponse) + } catch (e: IOException) { + throw RuntimeException("Network request failed. message=${e.message}") + } + } +} \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/billing/core/IAPResult.kt b/vending-app/src/main/java/org/microg/vending/billing/core/IAPResult.kt new file mode 100644 index 00000000..992d07b6 --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/billing/core/IAPResult.kt @@ -0,0 +1,11 @@ +package org.microg.vending.billing.core + +open class IAPResult(val resultMap: Map) { + fun getCode(): Int { + return resultMap["RESPONSE_CODE"] as Int + } + + fun getMessage(): String { + return resultMap["RESPONSE_MESSAGE"] as String + } +} \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/billing/core/PurchaseItem.kt b/vending-app/src/main/java/org/microg/vending/billing/core/PurchaseItem.kt new file mode 100644 index 00000000..d7c653fd --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/billing/core/PurchaseItem.kt @@ -0,0 +1,25 @@ +package org.microg.vending.billing.core + +// TODO: Use class from billing client instead +data class PurchaseItem( + val type: String, + val sku: String, + val pkgName: String, + val purchaseToken: String, + val purchaseState: Int, + val jsonData: String, + val signature: String, + val startAt: Long = 0L, + val expireAt: Long = 0L, +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is PurchaseItem) return false + if (purchaseToken != other.purchaseToken) return false + return true + } + + override fun hashCode(): Int { + return purchaseToken.hashCode() + } +} diff --git a/vending-app/src/main/java/org/microg/vending/billing/core/Utils.kt b/vending-app/src/main/java/org/microg/vending/billing/core/Utils.kt new file mode 100644 index 00000000..d20419d8 --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/billing/core/Utils.kt @@ -0,0 +1,309 @@ +package org.microg.vending.billing.core + +import android.util.Base64 +import android.util.Log +import com.android.billingclient.api.BillingClient.ProductType +import okio.ByteString +import okio.ByteString.Companion.toByteString +import org.json.JSONObject +import org.microg.vending.billing.TAG +import org.microg.vending.billing.proto.* +import org.microg.vending.billing.proto.PurchaseItem +import java.security.InvalidParameterException +import java.security.SecureRandom +import java.util.* + +fun List.toByteStringList(): List { + return this.map { it.toByteString() } +} + +fun mapToSkuParamList(map: Map?): List { + val result = mutableListOf() + if (map == null) + return result + map.forEach { entry -> + result.add( + when (val value = entry.value) { + is Boolean -> SkuParam.Builder().apply { + name = entry.key + bv = value + }.build() + + is Long -> SkuParam.Builder().apply { + name = entry.key + i64v = value + }.build() + + is Int -> SkuParam.Builder().apply { + name = entry.key + i64v = value.toLong() + }.build() + + is ArrayList<*> -> SkuParam.Builder().apply { + name = entry.key + svList = value.map { it as String } + }.build() + + is String -> SkuParam.Builder().apply { + name = entry.key + sv = value + }.build() + + else -> SkuParam.Builder().apply { + name = entry.key + sv = value.toString() + }.build() + } + ) + } + + return result +} + +fun localeToString(locale: Locale): String { + val result = StringBuilder() + result.append(locale.language) + locale.country?.let { + if (it.isNotEmpty()) + result.append("-$it") + } + locale.variant?.let { + if (it.isNotEmpty()) + result.append("-$it") + } + return result.toString() +} + +fun createClientToken(deviceInfo: DeviceEnvInfo, authData: AuthData): String { + val clientToken = ClientToken.Builder().apply { + this.info1 = ClientToken.Info1.Builder().apply { + this.locale = localeToString(deviceInfo.locale) + this.unknown8 = 2 + this.gpVersionCode = deviceInfo.gpVersionCode + this.deviceInfo = ClientToken.DeviceInfo.Builder().apply { + this.unknown3 = "33" + this.device = deviceInfo.device + deviceInfo.displayMetrics?.let { + this.widthPixels = it.widthPixels + this.heightPixels = it.heightPixels + this.xdpi = it.xdpi + this.ydpi = it.ydpi + this.densityDpi = it.densityDpi + } + this.gpPackage = deviceInfo.gpPkgName + this.gpVersionCode = deviceInfo.gpVersionCode.toString() + this.gpVersionName = deviceInfo.gpVersionName + this.envInfo = ClientToken.EnvInfo.Builder().apply { + this.deviceData = ClientToken.DeviceData.Builder().apply { + this.unknown1 = 0 + deviceInfo.telephonyData?.let { + this.simOperatorName = it.simOperatorName + this.phoneDeviceId = it.phoneDeviceId + this.phoneDeviceId1 = it.phoneDeviceId + } + this.gsfId = authData.gsfId.toLong(16) + this.device = deviceInfo.device + this.product = deviceInfo.product + this.model = deviceInfo.model + this.manufacturer = deviceInfo.manufacturer + this.fingerprint = deviceInfo.fingerprint + this.release = deviceInfo.release + this.brand = deviceInfo.brand + this.serial = deviceInfo.serialNo + this.isEmulator = false + }.build() + this.otherInfo = ClientToken.OtherInfo.Builder().apply { + this.gpInfo = mutableListOf(( + ClientToken.GPInfo.Builder().apply { + this.package_ = deviceInfo.gpPkgName + this.versionCode = deviceInfo.gpVersionCode.toString() + this.lastUpdateTime = deviceInfo.gpLastUpdateTime + this.firstInstallTime = deviceInfo.gpFirstInstallTime + this.sourceDir = deviceInfo.gpSourceDir + }).build()) + this.batteryLevel = deviceInfo.batteryLevel + this.timeZoneOffset = deviceInfo.timeZoneOffset + this.location = ClientToken.Location.Builder().apply { + deviceInfo.locationData?.let { + this.altitude = it.altitude + this.latitude = it.latitude + this.longitude = it.longitude + this.accuracy = it.accuracy + this.time = it.time + } + this.isMock = false + }.build() + this.isAdbEnabled = deviceInfo.isAdbEnabled + this.installNonMarketApps = deviceInfo.installNonMarketApps + this.iso3Language = deviceInfo.locale.isO3Language + this.netAddress = deviceInfo.networkData?.netAddressList ?: emptyList() + this.locale = deviceInfo.locale.toString() + deviceInfo.telephonyData?.let { + this.networkOperator = it.networkOperator + this.simOperator = it.simOperator + this.phoneType = it.phoneType + } + this.language = deviceInfo.locale.language + this.country = deviceInfo.locale.country + this.uptimeMillis = deviceInfo.uptimeMillis + this.timeZoneDisplayName = deviceInfo.timeZoneDisplayName + this.googleAccountCount = deviceInfo.googleAccounts.size + }.build() + }.build() + this.marketClientId = "am-google" + this.unknown15 = 1 + this.unknown16 = 2 + this.unknown22 = 2 + deviceInfo.networkData?.let { + this.linkDownstreamBandwidth = it.linkDownstreamBandwidth + this.linkUpstreamBandwidth = it.linkUpstreamBandwidth + this.isActiveNetworkMetered = it.isActiveNetworkMetered + } + this.unknown34 = 2 + this.uptimeMillis = deviceInfo.uptimeMillis + this.timeZoneDisplayName = deviceInfo.timeZoneDisplayName + this.unknown40 = 1 + }.build() + this.unknown11 = "-5228872483831680725" + this.googleAccounts = deviceInfo.googleAccounts + }.build() + this.info2 = ClientToken.Info2.Builder().apply { + this.unknown1 = + "https://play.app.goo.gl/?link=http%3A%2F%2Funused.google.com&apn=com.android.vending&al=google-orchestration%3A%2F%2Freturn" + this.unknown3 = 1 + this.unknown4 = mutableListOf(2) + this.unknown5 = 1 + }.build() + }.build() + return Base64.encodeToString( + clientToken.encode(), + Base64.URL_SAFE + Base64.NO_WRAP + ) +} + +fun getAcquireCacheKey( + deviceInfo: DeviceEnvInfo, + accountName: String, + docList: List, + callingPackage: String, + extras: Map, + authFrequency: Int +): String { + val stringBuilder = StringBuilder() + stringBuilder.append(accountName) + for (item in docList) { + stringBuilder.append("#") + stringBuilder.append(Base64.encodeToString(item.encode(), Base64.NO_WRAP)) + } + stringBuilder.append("#simId=${deviceInfo.deviceId}") + stringBuilder.append("#clientTheme=2") + stringBuilder.append("#fingerprintValid=false") + stringBuilder.append("#desiredAuthMethod=0") + stringBuilder.append("#authFrequency=$authFrequency") + stringBuilder.append("#userHasFop=false") + stringBuilder.append("#callingAppPackageName=$callingPackage") + for (item in extras) { + stringBuilder.append("#${item.key}=${item.value}") + } + return stringBuilder.toString() +} + +fun createNonce(): String { + val secureRandom = SecureRandom.getInstance("SHA1PRNG") + ?: throw RuntimeException("Uninitialized SecureRandom.") + val result = ByteArray(0x100) + secureRandom.nextBytes(result) + return "nonce=" + Base64.encodeToString(result, Base64.URL_SAFE + Base64.NO_WRAP + Base64.NO_PADDING) +} + +fun responseBundleToMap(responseBundle: ResponseBundle?): Map { + val result = mutableMapOf() + if (responseBundle != null) { + for (bundleItem in responseBundle.bundleItem) { + if (bundleItem.bv != null) { + result[bundleItem.key] = bundleItem.bv + } else if (bundleItem.i32v != null) { + result[bundleItem.key] = bundleItem.i32v + } else if (bundleItem.i64v != null) { + result[bundleItem.key] = bundleItem.i64v + } else if (bundleItem.sv != null) { + result[bundleItem.key] = bundleItem.sv + } else if (bundleItem.sList != null) { + result[bundleItem.key] = + ArrayList(bundleItem.sList.value_) + } else { + + } + } + } + return result +} + +fun getSkuType(skuType: String): Int { + return when (skuType) { + ProductType.SUBS -> 15 + ProductType.INAPP -> 11 + "first_party" -> 15 + else -> throw InvalidParameterException("unknown skuType: $skuType") + } +} + +fun splitDocId(docId: DocId): List { + return docId.backendDocId.split(":") +} + +fun parsePurchaseItem(purchaseItem: PurchaseItem): List { + val result = mutableListOf() + for (it in purchaseItem.purchaseItemData) { + if (it == null) + continue + val spr = if (it.docId != null) { + splitDocId(it.docId) + } else { + emptyList() + } + if (spr.size < 3) + continue + val (type, _, sku) = spr + var startAt = 0L + var expireAt = 0L + val (jsonData, signature) = when (type) { + ProductType.INAPP -> { + if (it.inAppPurchase == null) + continue + it.inAppPurchase.jsonData to it.inAppPurchase.signature + } + + ProductType.SUBS -> { + if (it.subsPurchase == null) + continue + startAt = it.subsPurchase.startAt + expireAt = it.subsPurchase.expireAt + it.subsPurchase.jsonData to it.subsPurchase.signature + } + + else -> { + Log.e(TAG, "unknown sku type $type") + continue + } + } + val jdo = JSONObject(jsonData) + val pkgName = jdo.optString("packageName").takeIf { it.isNotBlank() } ?: continue + val purchaseToken = jdo.optString("purchaseToken").takeIf { it.isNotBlank() } ?: continue + val purchaseState = jdo.optInt("purchaseState", -1).takeIf { it != -1 } ?: continue + result.add( + PurchaseItem( + type, + sku, + pkgName, + purchaseToken, + purchaseState, + jsonData, + signature, + startAt, + expireAt + ) + ) + } + return result +} \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/billing/core/ui/UIComponents.kt b/vending-app/src/main/java/org/microg/vending/billing/core/ui/UIComponents.kt new file mode 100644 index 00000000..3ac8ba36 --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/billing/core/ui/UIComponents.kt @@ -0,0 +1,301 @@ +package org.microg.vending.billing.core.ui + +data class BAction( + var type: ActionType, + var delay: Int? = null, + var result: Map? = null, + var uiInfo: BUIInfo? = null, + var srcScreenId: String? = null, + var screenId: String? = null, + var droidGuardMap: Map = emptyMap(), + var actionContext: MutableList = mutableListOf() +) + +enum class ActionType { + DELAY, + SHOW, + UNKNOWN +} + +data class BScreen( + val uiInfo: BUIInfo? = null, + val action: BAction? = null, + val uiComponents: BUIComponents? = null +) + +data class BUIInfo( + val uiType: UIType +) + +data class BAcquireResult( + val action: BAction? = null, + val screenMap: Map = emptyMap() +) + +data class BUIComponents( + val headerComponents: List, + val contentComponents: List, + val footerComponents: List +) + +data class BComponent( + val tag: String? = null, + val uiInfo: BUIInfo? = null, + val viewInfo: BViewInfo? = null, + val viewType: ViewType, + val clickableTextView: BClickableTextView? = null, + val viewGroup: BViewGroup? = null, + val dividerView: BDividerView? = null, + val moduloImageView: BModuloImageView? = null, + val iconTextCombinationView: BIconTextCombinationView? = null, + val buttonGroupView: BButtonGroupView? = null, + val instrumentItemView: BInstrumentItemView? = null +) + +data class BClickableTextView(val playTextView: BPlayTextView? = null) + +data class BViewGroup( + val imageView1: BImageView? = null, + val imageView2: BImageView? = null, + val imageView3: BImageView? = null, + val imageView4: BImageView? = null, + val playTextView: BPlayTextView? = null +) + +data class BImageGroup( + val imageViews: List, + val viewInfo: BViewInfo? = null +) + +class BDividerView + +data class BModuloImageView( + val imageView: BImageView? = null +) + +data class BIconTextCombinationView( + val headerImageView: BImageView? = null, + val playTextView: BPlayTextView? = null, + val badgeTextView: BPlayTextView? = null, + val middleTextViewList: List? = null, + val footerImageGroup: BImageGroup? = null, + val viewInfo: BViewInfo? = null +) + +data class BSingleLineTextView( + val playTextView1: BPlayTextView? = null, + val playTextView2: BPlayTextView? = null +) + +data class BPlayTextView( + val text: String, + val isHtml: Boolean = true, + val textInfo: BTextInfo? = null, + val viewInfo: BViewInfo? = null, + val textSpan: List, +) + +data class BBulletSpan( + val gapWidth: Int +) + +data class BImageInfo( + val colorFilterValue: Int? = null, + val colorFilterType: Int? = null, + val filterMode: Int? = null, + val scaleType: Int? = null +) + +data class BTextSpan( + val textSpanType: TextSpanType, + val bulletSpan: BBulletSpan? = null, +) + +data class BImageView( + val viewInfo: BViewInfo? = null, + val imageInfo: BImageInfo? = null, + val lightUrl: String? = null, + val darkUrl: String? = null, + val animation: BAnimation? = null, + val iconView: BIconView? = null +) + +data class BAnimation( + val type: Int?, + val repeatCount: Int? +) + +data class BIconView( + val type: Int?, + val text: String? +) + +data class BTextInfo( + val colorType: ColorType? = null, + val maxLines: Int? = null, + val gravityList: List? = null, + val textAlignmentType: TextAlignmentType? = null, + val styleType: Int? = null +) + +data class BButtonGroupView( + val buttonViewList: List +) + +data class BInstrumentItemView( + val icon: BImageView? = null, + val text: BPlayTextView? = null, + val tips: BPlayTextView? = null, + val extraInfo: BPlayTextView? = null, + val state: BImageView? = null, + val action: BAction? = null, +) + +data class BButtonView( + val text: String, + val viewInfo: BViewInfo? = null, + val action: BAction? = null +) + +data class BViewInfo( + val tag: String? = null, + val width: Float? = null, + val height: Float? = null, + val startMargin: Float? = null, + val topMargin: Float? = null, + val endMargin: Float? = null, + val bottomMargin: Float? = null, + val startPadding: Float? = null, + val topPadding: Float? = null, + val endPadding: Float? = null, + val bottomPadding: Float? = null, + val contentDescription: String? = null, + val gravityList: List? = null, + val backgroundColorType: ColorType? = null, + val borderColorType: ColorType? = null, + val action: BAction? = null, + val visibilityType: Int? = null +) + +enum class TextAlignmentType(val value: Int) { + TEXT_ALIGNMENT_INHERIT(0), + TEXT_ALIGNMENT_GRAVITY(1), + TEXT_ALIGNMENT_CENTER(4), + TEXT_ALIGNMENT_TEXT_START(2), + TEXT_ALIGNMENT_TEXT_END(3), + TEXT_ALIGNMENT_VIEW_START(5), + TEXT_ALIGNMENT_VIEW_END(6) +} + +enum class ColorType(val value: Int) { + PHONESKY_SEMANTIC_COLOR_NAME_UNKNOWN(0), + BACKGROUND_PRIMARY(1), + BACKGROUND_SECONDARY(2), + APPS_PRIMARY(3), + APPS_2(4), + APPS_3(5), + BOOKS_PRIMARY(6), + BOOKS_2(7), + BOOKS_3(8), + MOVIES_PRIMARY(9), + MOVIES_2(10), + MOVIES_3(11), + MUSIC_PRIMARY(12), + MUSIC_2(13), + MUSIC_3(14), + ENTERPRISE_PRIMARY(15), + ENTERPRISE_2(16), + ENTERPRISE_3(17), + TEXT_PRIMARY(18), + TEXT_SECONDARY(19), + TEXT_TERTIARY(20), + ICON_DEFAULT(21), + ICON_HIGH_CONTRAST(22), + PRIMARY_BUTTON_LABEL(23), + PRIMARY_BUTTON_LABEL_DISABLED(24), + PRIMARY_BUTTON_FILL_DISABLED(25), + SECONDARY_BUTTON_FILL_DISABLED(26), + CHIP_TEXT_SECONDARY(27), + HAIR_LINE(28), + ERROR_COLOR_PRIMARY(29), + ERROR_COLOR_SECONDARY(30), + BLANK_UNIFORM_ICON(31), + PROGRESS_BAR_BACKGROUND(32), + BRONZE_PRIMARY(33), + BRONZE_2(34), + BRONZE_3(35), + SILVER_PRIMARY(36), + SILVER_2(37), + SILVER_3(38), + GOLD_PRIMARY(39), + GOLD_2(40), + GOLD_3(41), + PLATINUM_PRIMARY(42), + PLATINUM_2(43), + PLATINUM_3(44), + DIAMOND_PRIMARY(45), + DIAMOND_2(46), + DIAMOND_3(0x2F), + NEWS_PRIMARY(0x30), + BACKGROUND_PRIMARY_INVERSE(49), + TEXT_PRIMARY_DISABLED(50), + TEXT_SECONDARY_DISABLED(51), + RATING_STAR_OUTLINE(52), + TOOLTIP_BACKGROUND(53), + YOUTUBE_COMMERCE_NEUTRAL_BLUE(54), + YOUTUBE_COMMERCE_NEUTRAL_BLUE_2(62), + YOUTUBE_COMMERCE_NEUTRAL_BLUE_3(0x3F), + YOUTUBE_COMMERCE_NEUTRAL_BLUE_RIPPLE(55), + YOUTUBE_COMMERCE_DEEP_PURPLE(56), + YOUTUBE_COMMERCE_DEEP_PURPLE_2(0x40), + YOUTUBE_COMMERCE_DEEP_PURPLE_3(65), + YOUTUBE_COMMERCE_DEEP_PURPLE_RIPPLE(57), + YOUTUBE_COMMERCE_MAGENTA(58), + YOUTUBE_COMMERCE_MAGENTA_2(66), + YOUTUBE_COMMERCE_MAGENTA_3(67), + YOUTUBE_COMMERCE_MAGENTA_RIPPLE(59), + YOUTUBE_COMMERCE_TEAL(60), + YOUTUBE_COMMERCE_TEAL_2(68), + YOUTUBE_COMMERCE_TEAL_3(69), + YOUTUBE_COMMERCE_TEAL_RIPPLE(61) +} + +enum class BGravity(val value: Int) { + NO_GRAVITY(0), + BOTTOM(80), + CENTER(17), + CENTER_HORIZONTAL(1), + CENTER_VERTICAL(16), + CLIP_HORIZONTAL(8), + CLIP_VERTICAL(0x80), + END(0x800005), + FILL(0x77), + FILL_HORIZONTAL(7), + FILL_VERTICAL(0x70), + LEFT(0x3), + RIGHT(0x5), + START(0x800003), + TOP(0x30) +} + +enum class ViewType { + CLICKABLETEXTVIEW, + VIEWGROUP, + DIVIDERVIEW, + MODULOIMAGEVIEW, + ICONTEXTCOMBINATIONVIEW, + BUTTONGROUPVIEW, + INSTRUMENTITEMVIEW, + UNKNOWNVIEW +} + + +enum class TextSpanType { + BULLETSPAN, + UNKNOWNSPAN +} + +enum class AnimationType(val value: Int) { + CHECK_MARK(21), + UNKNOWN(0) +} \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/billing/core/ui/UIParser.kt b/vending-app/src/main/java/org/microg/vending/billing/core/ui/UIParser.kt new file mode 100644 index 00000000..ea48a9d9 --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/billing/core/ui/UIParser.kt @@ -0,0 +1,705 @@ +package org.microg.vending.billing.core.ui + +import okio.ByteString +import org.json.JSONObject +import org.microg.vending.billing.core.AcquireParams +import org.microg.vending.billing.core.PurchaseItem +import org.microg.vending.billing.core.parsePurchaseItem +import org.microg.vending.billing.core.responseBundleToMap +import org.microg.vending.billing.proto.* + +data class AcquireParsedResult( + val action: BAction? = null, + val result: Map = mapOf("RESPONSE_CODE" to 4, "DEBUG_MESSAGE" to ""), + val purchaseItems: List, + val screenMap: Map = emptyMap() +) + +private fun typeToDpSize(type: Int): Float { + return when (type) { + 1 -> 2.0f + 2 -> 4.0f + 3 -> 8.0f + 4 -> 12.0f + 5 -> 16.0f + 6 -> 24.0f + 7 -> 32.0f + 8 -> 24.0f + 9 -> 18.0f + 10 -> 0.0f + 11 -> 6.0f + else -> { + 0.0f + } + } +} + +private fun parseUIInfo(uiInfo: UIInfo): BUIInfo { + if (uiInfo.classType == 1) return BUIInfo(UIType.UNKNOWN) + return BUIInfo(UIType.fromValue(uiInfo.uiType)) +} + +private fun parseScreen(screen: Screen): BScreen { + var uiInfo: BUIInfo? = null + var uiComponents: BUIComponents? = null + var action: BAction? = null + screen.uiInfo?.let { + uiInfo = parseUIInfo(it) + } + screen.uiComponents?.let { + uiComponents = parseScreenComponents(it) + } + screen.action?.let { + action = BAction(ActionType.UNKNOWN) + parseAction(it, action!!) + } + return BScreen(uiInfo, action, uiComponents) +} + +private fun parseScreenMap(screenMap: Map): Map { + val result: MutableMap = mutableMapOf() + for ((screenId, screen) in screenMap) { + val bScreen = parseScreen(screen) + result[screenId] = bScreen + } + return result +} + +fun parseAcquireResponse(acquireResponse: AcquireResponse): BAcquireResult { + val action = BAction(ActionType.UNKNOWN) + parseAction(acquireResponse.action, action) + val screenMap = parseScreenMap(acquireResponse.screen) + return BAcquireResult(action, screenMap) +} + +private fun parseAction(action: Action?, result: BAction): Boolean { + if (action == null) return false + if (action.actionContext != ByteString.EMPTY) { + result.actionContext.add(action.actionContext.toByteArray()) + } + if (action.timerAction != null) { + result.delay = action.timerAction.delay + result.type = ActionType.DELAY + result.result = responseBundleToMap(action.timerAction.responseBundle) + return true + } + if (action.actionExt?.extAction != null) { + val extAction = action.actionExt.extAction + if (extAction.droidGuardMap != null) { + result.droidGuardMap = extAction.droidGuardMap.map + } + if (extAction.action != null) { + return parseAction(extAction.action, result) + } + } + if (action.showAction != null) { + result.type = ActionType.SHOW + result.screenId = action.showAction.screenId + if (action.showAction.action1 != null) { + parseAction(action.showAction.action1, result) + } + if (action.showAction.action != null) { + parseAction(action.showAction.action, result) + } + return true + } + if (action.viewClickAction != null) { + if (action.viewClickAction.uiInfo != null && result.uiInfo == null) { + result.uiInfo = parseUIInfo(action.viewClickAction.uiInfo) + } + return parseAction(action.viewClickAction.action, result) + } + if (action.optionalAction != null) { + return parseAction(action.optionalAction.action1, result) + } + if (action.navigateToPage != null) { + result.srcScreenId = action.navigateToPage.from + return parseAction(action.navigateToPage.action, result) + } + return false +} + +private fun parseAnimation(animation: Animation): BAnimation { + var type: Int? = null + var repeatCount: Int? = null + if (animation.type != 0) { + type = animation.type + } + if (animation.repeatCount != 0) { + repeatCount = animation.repeatCount + } + return BAnimation(type, repeatCount) +} + +private fun parseIconView(iconView: IconView): BIconView { + var type: Int? = null + var text: String? = null + if (iconView.type != 0) { + type = iconView.type + } + if (iconView.text.isNotBlank()) { + text = iconView.text + } + return BIconView(type, text) +} + +private fun parseInstrumentItemView(instrumentItemView: InstrumentItemView): BInstrumentItemView { + var icon: BImageView? = null + var text: BPlayTextView? = null + var tips: BPlayTextView? = null + var extraInfo: BPlayTextView? = null + var state: BImageView? = null + var action: BAction? = null + if (instrumentItemView.icon != null) { + icon = parseImageView(instrumentItemView.icon) + } + if (instrumentItemView.text != null) { + text = parsePlayTextView(instrumentItemView.text) + } + if (instrumentItemView.tips != null) { + tips = parsePlayTextView(instrumentItemView.tips) + } + if (instrumentItemView.state != null) { + state = parseImageView(instrumentItemView.state) + } + if (instrumentItemView.action != null) { + action = BAction(ActionType.UNKNOWN) + parseAction(instrumentItemView.action, action) + } + if (instrumentItemView.extraInfo != null) { + extraInfo = parsePlayTextView(instrumentItemView.extraInfo) + } + return BInstrumentItemView(icon, text, tips, extraInfo, state, action) +} + +private fun parseImageGroup(imageGroup: ImageGroup): BImageGroup { + val imageViews = mutableListOf() + var viewInfo: BViewInfo? = null + + if (imageGroup.viewInfo != null) { + viewInfo = parseViewInfo(imageGroup.viewInfo) + } + imageGroup.imageView.forEach { + imageViews.add(parseImageView(it)) + } + return BImageGroup(imageViews, viewInfo) +} + +private fun parseImageView(imageView: ImageView): BImageView { + var darkUrl: String? = null + var lightUrl: String? = null + var viewInfo: BViewInfo? = null + var imageInfo: BImageInfo? = null + var iconView: BIconView? = null + var animation: BAnimation? = null + if (imageView.thumbnailImageView != null) { + if (imageView.thumbnailImageView.darkUrl.isNotBlank()) + darkUrl = imageView.thumbnailImageView.darkUrl + if (imageView.thumbnailImageView.lightUrl.isNotBlank()) + lightUrl = imageView.thumbnailImageView.lightUrl + } + if (imageView.viewInfo != null) { + viewInfo = parseViewInfo(imageView.viewInfo) + } + if (imageView.imageInfo != null) { + imageInfo = parseImageInfo(imageView.imageInfo) + } + if (imageView.iconView != null) { + iconView = parseIconView(imageView.iconView) + } + if (imageView.animation != null) { + animation = parseAnimation(imageView.animation) + } + return BImageView(viewInfo, imageInfo, lightUrl, darkUrl, animation, iconView) +} + +private fun parseTextInfo(textInfo: TextInfo): BTextInfo { + var colorType: ColorType? = null + var maxLines: Int? = null + var gravityList: List? = null + var textAlignmentType: TextAlignmentType? = null + var styleType: Int? = null + if (textInfo.maxLines != 0) { + maxLines = textInfo.maxLines + } + if (textInfo.gravity.isNotEmpty()) { + gravityList = textInfo.gravity.map { BGravity.values()[it] } + } + if (textInfo.textColorType != null) { + colorType = ColorType.values()[textInfo.textColorType] + } + if (textInfo.textAlignmentType != 0) { + textAlignmentType = TextAlignmentType.values()[textInfo.textAlignmentType] + } + if (textInfo.styleType != 0) { + styleType = textInfo.styleType + } + return BTextInfo(colorType, maxLines, gravityList, textAlignmentType, styleType) +} + +private fun parseBulletSpan(bulletSpan: BulletSpan): BBulletSpan { + return BBulletSpan(bulletSpan.gapWidth?.unitValue?.toInt() ?: 0) +} + +private fun parseTextSpan(textSpan: TextSpan): BTextSpan { + return if (textSpan.bulletSpan != null) { + BTextSpan( + TextSpanType.BULLETSPAN, + parseBulletSpan(textSpan.bulletSpan) + ) + } else { + BTextSpan(TextSpanType.UNKNOWNSPAN) + } +} + +private fun parsePlayTextView(playTextView: PlayTextView): BPlayTextView { + var textInfo: BTextInfo? = null + var viewInfo: BViewInfo? = null + var textSpanList: MutableList = mutableListOf() + if (playTextView.textInfo != null) { + textInfo = parseTextInfo(playTextView.textInfo) + } + if (playTextView.viewInfo != null) { + viewInfo = parseViewInfo(playTextView.viewInfo) + } + if (playTextView.textSpan.isNotEmpty()) { + playTextView.textSpan.forEach { + textSpanList.add(parseTextSpan(it)) + } + } + return BPlayTextView( + playTextView.text ?: "", + playTextView.isHtml, + textInfo, + viewInfo, + textSpanList + ) +} + +private fun parseSingleLineTextView(singleLineTextView: SingleLineTextView): BSingleLineTextView { + var playTextView1: BPlayTextView? = null + var playTextView2: BPlayTextView? = null + + if (singleLineTextView.playTextView1 != null) + playTextView1 = parsePlayTextView(singleLineTextView.playTextView1) + if (singleLineTextView.playTextView2 != null) + playTextView2 = parsePlayTextView(singleLineTextView.playTextView2) + + return BSingleLineTextView(playTextView1, playTextView2) +} + +private fun parseIconTextCombinationView(iconTextCombinationView: IconTextCombinationView): BIconTextCombinationView { + var headerImageView: BImageView? = null + var playTextView: BPlayTextView? = null + var badgeTextView: BPlayTextView? = null + var middleTextViewList: List? = null + var viewInfo: BViewInfo? = null + var footerImageGroup: BImageGroup? = null + + if (iconTextCombinationView.headerImageView != null) { + headerImageView = parseImageView(iconTextCombinationView.headerImageView) + } + if (iconTextCombinationView.playTextView != null) { + playTextView = parsePlayTextView(iconTextCombinationView.playTextView) + } + if (iconTextCombinationView.badgeTextView != null) { + badgeTextView = parsePlayTextView(iconTextCombinationView.badgeTextView) + } + if (iconTextCombinationView.singleLineTextView.isNotEmpty()) { + middleTextViewList = + iconTextCombinationView.singleLineTextView.map { parseSingleLineTextView(it) } + } + if (iconTextCombinationView.footerImageGroup != null) { + footerImageGroup = parseImageGroup(iconTextCombinationView.footerImageGroup) + } + if (iconTextCombinationView.viewInfo != null) { + viewInfo = parseViewInfo(iconTextCombinationView.viewInfo) + } + + return BIconTextCombinationView( + headerImageView, + playTextView, + badgeTextView, + middleTextViewList, + footerImageGroup, + viewInfo + ) +} + +private fun parseClickableTextView(clickableTextView: ClickableTextView): BClickableTextView { + var playTextView: BPlayTextView? = null + if (clickableTextView.playTextView != null) + playTextView = parsePlayTextView(clickableTextView.playTextView) + return BClickableTextView(playTextView) +} + +private fun parseViewGroup(viewGroup: ViewGroup): BViewGroup { + var imageView1: BImageView? = null + var imageView2: BImageView? = null + var imageView3: BImageView? = null + var imageView4: BImageView? = null + var playTextView: BPlayTextView? = null + if (viewGroup.imageView1 != null) { + imageView1 = parseImageView(viewGroup.imageView1) + } + if (viewGroup.imageView2 != null) { + imageView2 = parseImageView(viewGroup.imageView2) + } + if (viewGroup.imageView3 != null) { + imageView3 = parseImageView(viewGroup.imageView3) + } + if (viewGroup.imageView4 != null) { + imageView4 = parseImageView(viewGroup.imageView4) + } + if (viewGroup.playTextView != null) { + playTextView = parsePlayTextView(viewGroup.playTextView) + } + return BViewGroup(imageView1, imageView2, imageView3, imageView4, playTextView) +} + +private fun parseModuloImageView(moduloImageView: ModuloImageView): BModuloImageView { + var imageView: BImageView? = null + if (moduloImageView.imageView != null) { + imageView = parseImageView(moduloImageView.imageView) + } + return BModuloImageView(imageView) +} + +private fun parseDividerView(dividerView: DividerView): BDividerView { + return BDividerView() +} + +private fun parseImageInfo(imageInfo: ImageInfo): BImageInfo { + var colorFilterValue: Int? = null + var colorFilterType: Int? = null + var filterMode: Int? = null + var scaleType: Int? = null + if (imageInfo.value_ != null) { + colorFilterValue = imageInfo.value_ + } else if (imageInfo.valueType != null) { + colorFilterType = imageInfo.valueType + } + if (imageInfo.modeType != 0) { + filterMode = imageInfo.modeType + } + if (imageInfo.scaleType != 0) { + scaleType = imageInfo.scaleType + } + return BImageInfo(colorFilterValue, colorFilterType, filterMode, scaleType) +} + +private fun parseViewInfo(viewInfo: ViewInfo): BViewInfo { + var tag: String? = null + var width: Float? = null + var height: Float? = null + var startMargin: Float? = null + var topMargin: Float? = null + var endMargin: Float? = null + var bottomMargin: Float? = null + var startPadding: Float? = null + var topPadding: Float? = null + var endPadding: Float? = null + var bottomPadding: Float? = null + var contentDescription: String? = null + var gravityList: List? = null + var backgroundColorType: ColorType? = null + var borderColorType: ColorType? = null + var action: BAction? = null + var visibilityType: Int? = null + + if (viewInfo.tag.isNotBlank()) { + tag = viewInfo.tag + } + if (viewInfo.widthValue != 0f) { + width = viewInfo.widthValue + } + if (viewInfo.heightValue != 0f) { + height = viewInfo.heightValue + } + if (viewInfo.startMargin != 0f) { + startMargin = viewInfo.startMargin + } + if (viewInfo.topMargin != 0f) { + topMargin = viewInfo.topMargin + } + if (viewInfo.endMargin != 0f) { + endMargin = viewInfo.endMargin + } + if (viewInfo.bottomMargin != 0f) { + bottomMargin = viewInfo.bottomMargin + } + if (viewInfo.startPadding != 0f) { + startPadding = viewInfo.startPadding + } + if (viewInfo.topPadding != 0f) { + topPadding = viewInfo.topPadding + } + if (viewInfo.endPadding != 0f) { + endPadding = viewInfo.endPadding + } + if (viewInfo.bottomPadding != 0f) { + bottomPadding = viewInfo.bottomPadding + } + if (viewInfo.startMarginType != 0) { + startMargin = typeToDpSize(viewInfo.startMarginType) + } + if (viewInfo.topMarginType != 0) { + topMargin = typeToDpSize(viewInfo.topMarginType) + } + if (viewInfo.endMarginType != 0) { + endMargin = typeToDpSize(viewInfo.endMarginType) + } + if (viewInfo.bottomMarginType != 0) { + bottomMargin = typeToDpSize(viewInfo.bottomMarginType) + } + if (viewInfo.startPaddingType != 0) { + startPadding = typeToDpSize(viewInfo.startPaddingType) + } + if (viewInfo.topPaddingType != 0) { + topPadding = typeToDpSize(viewInfo.topPaddingType) + } + if (viewInfo.endPaddingType != 0) { + endPadding = typeToDpSize(viewInfo.endPaddingType) + } + if (viewInfo.bottomPaddingType != 0) { + bottomPadding = typeToDpSize(viewInfo.bottomPaddingType) + } + if (viewInfo.contentDescription.isNotBlank()) { + contentDescription = viewInfo.contentDescription + } + if (viewInfo.gravity.isNotEmpty()) { + gravityList = viewInfo.gravity.map { BGravity.values()[it] } + } + if (viewInfo.backgroundColorType != 0) { + backgroundColorType = ColorType.values()[viewInfo.backgroundColorType] + } + if (viewInfo.borderColorType != 0) { + borderColorType = ColorType.values()[viewInfo.borderColorType] + } + if (viewInfo.action != null) { + action = BAction(ActionType.UNKNOWN) + parseAction(viewInfo.action, action) + } + if (viewInfo.visibilityType != 0) { + visibilityType = viewInfo.visibilityType + } + return BViewInfo( + tag, + width, + height, + startMargin, + topMargin, + endMargin, + bottomMargin, + startPadding, + topPadding, + endPadding, + bottomPadding, + contentDescription, + gravityList, + backgroundColorType, + borderColorType, + action, + visibilityType + ) +} + +private fun parseContentComponent(contentComponent: ContentComponent): BComponent { + val tag = contentComponent.tag + var viewInfo: BViewInfo? = null + var uiInfo: BUIInfo? = null + if (contentComponent.viewInfo != null) { + viewInfo = parseViewInfo(contentComponent.viewInfo) + } + if (contentComponent.uiInfo != null) { + uiInfo = parseUIInfo(contentComponent.uiInfo) + } + return if (contentComponent.iconTextCombinationView != null) { + BComponent( + tag, + uiInfo, + viewInfo, + ViewType.ICONTEXTCOMBINATIONVIEW, + iconTextCombinationView = parseIconTextCombinationView(contentComponent.iconTextCombinationView) + ) + } else if (contentComponent.clickableTextView != null) { + BComponent( + tag, + uiInfo, + viewInfo, + ViewType.CLICKABLETEXTVIEW, + clickableTextView = parseClickableTextView(contentComponent.clickableTextView) + ) + } else if (contentComponent.viewGroup != null) { + BComponent( + tag, + uiInfo, + viewInfo, + ViewType.VIEWGROUP, + viewGroup = parseViewGroup(contentComponent.viewGroup) + ) + } else if (contentComponent.dividerView != null) { + BComponent( + tag, + uiInfo, + viewInfo, + ViewType.DIVIDERVIEW, + dividerView = parseDividerView(contentComponent.dividerView) + ) + } else if (contentComponent.moduloImageView != null) { + BComponent( + tag, + uiInfo, + viewInfo, + ViewType.MODULOIMAGEVIEW, + moduloImageView = parseModuloImageView(contentComponent.moduloImageView) + ) + } else if (contentComponent.buttonGroupView != null) { + BComponent( + tag, + uiInfo, + viewInfo, + ViewType.BUTTONGROUPVIEW, + buttonGroupView = parseButtonGroupView(contentComponent.buttonGroupView) + ) + } else if (contentComponent.instrumentItemView != null) { + BComponent( + tag, + uiInfo, + viewInfo, + ViewType.INSTRUMENTITEMVIEW, + instrumentItemView = parseInstrumentItemView(contentComponent.instrumentItemView) + ) + } else { + BComponent(viewType = ViewType.UNKNOWNVIEW) + } +} + +private fun parseButtonView(buttonView: ButtonView): BButtonView { + var text = "" + var viewInfo: BViewInfo? = null + val action = BAction(ActionType.UNKNOWN) + text = buttonView.text ?: "" + if (buttonView.viewInfo != null) { + viewInfo = parseViewInfo(buttonView.viewInfo) + } + if (buttonView.action != null) { + parseAction(buttonView.action, action) + } + return BButtonView(text, viewInfo, action) +} + +private fun parseButtonGroupView(buttonGroupView: ButtonGroupView): BButtonGroupView { + var buttonViewList = mutableListOf() + + if (buttonGroupView.newButtonView != null) { + if (buttonGroupView.newButtonView.buttonView != null) { + buttonViewList.add(parseButtonView(buttonGroupView.newButtonView.buttonView)) + } + if (buttonGroupView.newButtonView.buttonView2 != null) { + buttonViewList.add(parseButtonView(buttonGroupView.newButtonView.buttonView2)) + } + } + return BButtonGroupView(buttonViewList) +} + +private fun parseFooterComponent(footerComponent: FooterComponent): BComponent { + val tag = footerComponent.tag + var viewInfo: BViewInfo? = null + var uiInfo: BUIInfo? = null + if (footerComponent.viewInfo != null) { + viewInfo = parseViewInfo(footerComponent.viewInfo) + } + if (footerComponent.uiInfo != null) { + uiInfo = parseUIInfo(footerComponent.uiInfo) + } + return if (footerComponent.buttonGroupView != null) { + BComponent( + tag, + uiInfo, + viewInfo, + ViewType.BUTTONGROUPVIEW, + buttonGroupView = parseButtonGroupView(footerComponent.buttonGroupView) + ) + } else if (footerComponent.dividerView != null) { + BComponent( + tag, + uiInfo, + viewInfo, + ViewType.DIVIDERVIEW, + dividerView = parseDividerView(footerComponent.dividerView) + ) + } else { + BComponent(viewType = ViewType.UNKNOWNVIEW) + } +} + +private fun parseScreenComponents(uiComponents: UiComponents): BUIComponents { + val headerComponents = mutableListOf() + val contentComponents = mutableListOf() + val footerComponents = mutableListOf() + for (contentComponent in uiComponents.contentComponent1) { + headerComponents.add(parseContentComponent(contentComponent)) + } + for (contentComponent in uiComponents.contentComponent2) { + contentComponents.add(parseContentComponent(contentComponent)) + } + for (footerComponent in uiComponents.footerComponent) { + footerComponents.add(parseFooterComponent(footerComponent)) + } + + return BUIComponents(headerComponents, contentComponents, footerComponents) +} + +fun parsePurchaseResponse( + acquireParams: AcquireParams, + purchaseResponse: PurchaseResponse? +): Pair, PurchaseItem?> { + if (purchaseResponse == null) { + return mapOf("RESPONSE_CODE" to 0, "DEBUG_MESSAGE" to "") to null + } + val resultMap = responseBundleToMap(purchaseResponse.responseBundle) + val code = resultMap["RESPONSE_CODE"] as Int? ?: return mapOf( + "RESPONSE_CODE" to 0, + "DEBUG_MESSAGE" to "" + ) to null + val pd = resultMap["INAPP_PURCHASE_DATA"] as String? ?: return resultMap to null + val ps = resultMap["INAPP_DATA_SIGNATURE"] as String? ?: return resultMap to null + if (code != 0) return resultMap to null + val pdj = JSONObject(pd) + val packageName = + pdj.optString("packageName").takeIf { it.isNotBlank() } ?: return resultMap to null + val purchaseToken = + pdj.optString("purchaseToken").takeIf { it.isNotBlank() } ?: return resultMap to null + val purchaseState = + pdj.optInt("purchaseState", -1).takeIf { it != -1 } ?: return resultMap to null + return resultMap to PurchaseItem( + acquireParams.buyFlowParams.skuType, + acquireParams.buyFlowParams.sku, + packageName, + purchaseToken, + purchaseState, + pd, ps + ) +} + +fun parseAcquireResponse( + acquireParams: AcquireParams, + acquireResponse: AcquireResponse +): AcquireParsedResult { + val action = BAction(ActionType.UNKNOWN) + parseAction(acquireResponse.action, action) + val screenMap = parseScreenMap(acquireResponse.screen) + val (result, purchaseItem) = parsePurchaseResponse( + acquireParams, + acquireResponse.acquireResult?.purchaseResponse + ) + val purchaseItems = mutableSetOf() + if (acquireResponse.acquireResult?.ownedPurchase != null) { + acquireResponse.acquireResult.ownedPurchase.purchaseItem.forEach { + purchaseItems.addAll(parsePurchaseItem(it)) + } + } + if (purchaseItem != null) purchaseItems.add(purchaseItem) + return AcquireParsedResult(action, result, purchaseItems.toList(), screenMap) +} \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/billing/core/ui/UIType.kt b/vending-app/src/main/java/org/microg/vending/billing/core/ui/UIType.kt new file mode 100644 index 00000000..75a28ff5 --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/billing/core/ui/UIType.kt @@ -0,0 +1,28 @@ +package org.microg.vending.billing.core.ui + +val UITypeIndexMap: Map = UIType.values().associateBy { it.value } + +enum class UIType(val value: Int) { + UNKNOWN(-1), + LOADING_SPINNER(213), + PURCHASE_CART_BUY_BUTTON(711), + PURCHASE_CART_CONTINUE_BUTTON(712), + PURCHASE_CART_PAYMENT_OPTIONS_LINK(714), + PURCHASE_AUTH_SCREEN(750), + PURCHASE_ERROR_SCREEN(770), + BILLING_PROFILE_OPTION_REDEEM_CODE(815), + BILLING_PROFILE_OPTION_ADD_PLAY_CREDIT(816), + BILLING_PROFILE_OPTION_CREATE_INSTRUMENT(817), + BILLING_PROFILE_EXISTING_INSTRUMENT(818), + PURCHASE_SUCCESS_SCREEN_WITH_AUTH_CHOICES(1250), + PURCHASE_CHANGE_SUBSCRIPTION_CONTINUE_BUTTON(1281), + PURCHASE_PAYMENT_DECLINED_CONTINUE_BUTTON(1301), + BILLING_PROFILE_MORE_OPTION_BUTTON_SHOW_HIDEABLE_INSTRUMENT(12034), + BILLING_PROFILE_SCREEN_ABANDON(12035); + + companion object { + fun fromValue(value: Int): UIType { + return UITypeIndexMap[value] ?: UNKNOWN + } + } +} \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/billing/ui/Extends.kt b/vending-app/src/main/java/org/microg/vending/billing/ui/Extends.kt new file mode 100644 index 00000000..dad0fcbb --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/billing/ui/Extends.kt @@ -0,0 +1,161 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.vending.billing.ui + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.layout.layout +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import org.microg.vending.billing.containsAny +import org.microg.vending.billing.core.ui.BGravity +import org.microg.vending.billing.core.ui.BViewInfo +import org.microg.vending.billing.core.ui.UIType + +fun RowScope.applyAlignment(modifier: Modifier, viewInfo: BViewInfo?): Modifier { + val gravitys = viewInfo?.gravityList ?: return modifier + var newModifier = modifier + if (gravitys.containsAny(listOf(BGravity.TOP))) { + newModifier = newModifier.align(Alignment.Top) + } + if (gravitys.containsAny(listOf(BGravity.CENTER, BGravity.CENTER_VERTICAL))) { + newModifier = newModifier.align(Alignment.CenterVertically) + } + if (gravitys.containsAny(listOf(BGravity.BOTTOM))) { + newModifier = newModifier.align(Alignment.Bottom) + } + + return newModifier +} + +fun ColumnScope.applyAlignment(modifier: Modifier, viewInfo: BViewInfo?): Modifier { + val gravitys = viewInfo?.gravityList ?: return modifier + var newModifier = modifier + if (gravitys.containsAny(listOf(BGravity.START, BGravity.LEFT))) { + newModifier = newModifier.align(Alignment.Start) + } + if (gravitys.containsAny(listOf(BGravity.CENTER, BGravity.CENTER_HORIZONTAL))) { + newModifier = newModifier.align(Alignment.CenterHorizontally) + } + if (gravitys.containsAny(listOf(BGravity.RIGHT, BGravity.END))) { + newModifier = newModifier.align(Alignment.End) + } + + return newModifier +} + +fun Modifier.visibility(visibleType: Int): Modifier { + val visible = when (visibleType) { + 1 -> false + else -> true + } + return this.layout { measurable, constraints -> + val placeable = measurable.measure(constraints) + if (visible) { + layout(placeable.width, placeable.height) { + // place this item in the original position + placeable.placeRelative(0, 0) + } + } else { + layout(0, 0) {} + } + } +} + +fun Modifier.applySize(size: DpSize?): Modifier = composed { + this.let { mit -> + var newModifier = mit + size?.let { + newModifier = newModifier.size(size) + } + newModifier + } +} + +@Composable +fun Modifier.applyViewInfo(viewInfo: BViewInfo?): Modifier { + return this.let { mit -> + var newModifier = mit + viewInfo?.visibilityType?.let { + newModifier = newModifier.visibility(it) + } + viewInfo?.startMargin?.let { + newModifier = newModifier.padding(start = Dp(it.takeIf { it > 0 } ?: 0f)) + } + viewInfo?.topMargin?.let { + newModifier = newModifier.padding(top = Dp(it.takeIf { it > 0 } ?: 0f)) + } + viewInfo?.endMargin?.let { + newModifier = newModifier.padding(end = Dp(it.takeIf { it > 0 } ?: 0f)) + } + viewInfo?.bottomMargin?.let { + newModifier = newModifier.padding(bottom = Dp(it.takeIf { it > 0 } ?: 0f)) + } + viewInfo?.action?.let { action -> + when (action.uiInfo?.uiType) { + UIType.BILLING_PROFILE_SCREEN_ABANDON, + UIType.PURCHASE_CART_PAYMENT_OPTIONS_LINK, + UIType.BILLING_PROFILE_OPTION_REDEEM_CODE, + UIType.BILLING_PROFILE_OPTION_ADD_PLAY_CREDIT, + UIType.BILLING_PROFILE_OPTION_CREATE_INSTRUMENT -> { + val viewState = LocalBillingUiViewState.current + newModifier = newModifier.clickable { + viewState.onClickAction(action) + } + } + + else -> {} + } + } + viewInfo?.width?.let { + newModifier = newModifier.width(Dp(it)) + } + viewInfo?.height?.let { + newModifier = newModifier.height(Dp(it)) + } + getColorByType(viewInfo?.borderColorType)?.let { + newModifier = newModifier.border(BorderStroke(1.dp, it), shape = RoundedCornerShape(5)) + } + getColorByType(viewInfo?.backgroundColorType)?.let { + newModifier = newModifier.background(it) + } + viewInfo?.startPadding?.let { + newModifier = newModifier.padding(start = Dp(it.takeIf { it > 0 } ?: 0f)) + } + viewInfo?.topPadding?.let { + newModifier = newModifier.padding(top = Dp(it.takeIf { it > 0 } ?: 0f)) + } + viewInfo?.endPadding?.let { + newModifier = newModifier.padding(end = Dp(it.takeIf { it > 0 } ?: 0f)) + } + viewInfo?.bottomPadding?.let { + newModifier = newModifier.padding(bottom = Dp(it.takeIf { it > 0 } ?: 0f)) + } + + newModifier + } +} + +fun Modifier.applyUITypePadding(uiType: UIType?): Modifier { + if (uiType == UIType.PURCHASE_ERROR_SCREEN) { + return this.padding(top = 15.dp, start = 30.dp, end = 30.dp) + } + return this +} \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/billing/ui/HtmlText.kt b/vending-app/src/main/java/org/microg/vending/billing/ui/HtmlText.kt new file mode 100644 index 00000000..9aa1fc60 --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/billing/ui/HtmlText.kt @@ -0,0 +1,278 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.vending.billing.ui + +import android.graphics.Typeface +import android.os.Build.VERSION.SDK_INT +import android.text.Html +import android.text.Spanned +import android.text.style.ForegroundColorSpan +import android.text.style.StrikethroughSpan +import android.text.style.StyleSpan +import android.text.style.URLSpan +import android.text.style.UnderlineSpan +import androidx.annotation.StringRes +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.text.InlineTextContent +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.semantics.CustomAccessibilityAction +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.customActions +import androidx.compose.ui.semantics.onClick +import androidx.compose.ui.semantics.role +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextLayoutResult +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.TextUnit +import androidx.core.text.HtmlCompat +import androidx.core.text.getSpans + +/** + * Simple Text composable to show the text with html styling from a String. + * Supported are: + * + * <b>Bold</b> + * + * <i>Italic</i> + * + * <u>Underlined</u> + * + * <strike>Strikethrough</strike> + * + * <a href="https://google.de">Link</a> + * + * @see androidx.compose.material.Text + * + */ +@Composable +fun HtmlText( + modifier: Modifier = Modifier, + text: String, + urlSpanStyle: SpanStyle = SpanStyle( + color = MaterialTheme.colorScheme.secondary, + textDecoration = TextDecoration.Underline + ), + colorMapping: Map = emptyMap(), + color: Color = Color.Unspecified, + fontSize: TextUnit = TextUnit.Unspecified, + fontStyle: FontStyle? = null, + fontWeight: FontWeight? = null, + fontFamily: FontFamily? = null, + letterSpacing: TextUnit = TextUnit.Unspecified, + textDecoration: TextDecoration? = null, + textAlign: TextAlign? = null, + lineHeight: TextUnit = TextUnit.Unspecified, + overflow: TextOverflow = TextOverflow.Clip, + softWrap: Boolean = true, + maxLines: Int = Int.MAX_VALUE, + inlineContent: Map = mapOf(), + onTextLayout: (TextLayoutResult) -> Unit = {}, + style: TextStyle = LocalTextStyle.current, + onUriClick: ((String) -> Unit)? = null, +) { + val annotatedString = HtmlCompat.fromHtml(text, HtmlCompat.FROM_HTML_MODE_LEGACY).toAnnotatedString(urlSpanStyle, colorMapping) + + HtmlText( + modifier = modifier, + annotatedString = annotatedString, + color = color, + fontSize = fontSize, + fontStyle = fontStyle, + fontWeight = fontWeight, + fontFamily = fontFamily, + letterSpacing = letterSpacing, + textDecoration = textDecoration, + textAlign = textAlign, + lineHeight = lineHeight, + overflow = overflow, + softWrap = softWrap, + maxLines = maxLines, + inlineContent = inlineContent, + onTextLayout = onTextLayout, + style = style, + onUriClick = onUriClick + ) +} + +/** + * Simple Text composable to show the text with html styling from a String. + * Supported are: + * + * <b>Bold</b> + * + * <i>Italic</i> + * + * <u>Underlined</u> + * + * <strike>Strikethrough</strike> + * + * <a href="https://google.de">Link</a> + * + * @see androidx.compose.material.Text + * + */ +@Composable +fun HtmlText( + modifier: Modifier = Modifier, + annotatedString: AnnotatedString, + color: Color = Color.Unspecified, + fontSize: TextUnit = TextUnit.Unspecified, + fontStyle: FontStyle? = null, + fontWeight: FontWeight? = null, + fontFamily: FontFamily? = null, + letterSpacing: TextUnit = TextUnit.Unspecified, + textDecoration: TextDecoration? = null, + textAlign: TextAlign? = null, + lineHeight: TextUnit = TextUnit.Unspecified, + overflow: TextOverflow = TextOverflow.Clip, + softWrap: Boolean = true, + maxLines: Int = Int.MAX_VALUE, + inlineContent: Map = mapOf(), + onTextLayout: (TextLayoutResult) -> Unit = {}, + style: TextStyle = LocalTextStyle.current, + onUriClick: ((String) -> Unit)? = null, +) { + val clickable = + annotatedString.getStringAnnotations(0, annotatedString.length - 1).any { it.tag == "url" } + + val uriHandler = LocalUriHandler.current + val layoutResult = remember { mutableStateOf(null) } + + val urls = remember(layoutResult, annotatedString) { + annotatedString.getStringAnnotations("url", 0, annotatedString.lastIndex) + } + + Text( + modifier = modifier.then(if (clickable) Modifier + .pointerInput(Unit) { + detectTapGestures(onTap = { pos -> + layoutResult.value?.let { layoutResult -> + val position = layoutResult.getOffsetForPosition(pos) + annotatedString + .getStringAnnotations(position, position) + .firstOrNull() + ?.let { sa -> + if (sa.tag == "url") { // NON-NLS + val url = sa.item + onUriClick?.let { it(url) } ?: uriHandler.openUri(url) + } + } + } + }) + } + .semantics { + if (urls.size == 1) { + role = Role.Button + onClick("Link (${annotatedString.substring(urls[0].start, urls[0].end)}") { + val url = urls[0].item + onUriClick?.let { it(url) } ?: uriHandler.openUri(url) + true + } + } else { + customActions = urls.map { + CustomAccessibilityAction("Link (${annotatedString.substring(it.start, it.end)})") { + val url = it.item + onUriClick?.let { it(url) } ?: uriHandler.openUri(url) + true + } + } + } + } else Modifier), + text = annotatedString, + color = color, + fontSize = fontSize, + fontStyle = fontStyle, + fontWeight = fontWeight, + fontFamily = fontFamily, + letterSpacing = letterSpacing, + textDecoration = textDecoration, + textAlign = textAlign, + lineHeight = lineHeight, + overflow = overflow, + softWrap = softWrap, + maxLines = maxLines, + inlineContent = inlineContent, + onTextLayout = { + layoutResult.value = it + onTextLayout(it) + }, + style = style + ) +} + +fun Spanned.toAnnotatedString( + urlSpanStyle: SpanStyle = SpanStyle( + color = Color.Blue, + textDecoration = TextDecoration.Underline + ), + colorMapping: Map = emptyMap(), +): AnnotatedString { + return buildAnnotatedString { + append(this@toAnnotatedString.toString()) + val urlSpans = getSpans() + val styleSpans = getSpans() + val colorSpans = getSpans() + val underlineSpans = getSpans() + val strikethroughSpans = getSpans() + urlSpans.forEach { urlSpan -> + val start = getSpanStart(urlSpan) + val end = getSpanEnd(urlSpan) + addStyle(urlSpanStyle, start, end) + addStringAnnotation("url", urlSpan.url, start, end) // NON-NLS + } + colorSpans.forEach { colorSpan -> + val start = getSpanStart(colorSpan) + val end = getSpanEnd(colorSpan) + addStyle( + SpanStyle(color = colorMapping.getOrElse(Color(colorSpan.foregroundColor)) { Color(colorSpan.foregroundColor) }), + start, + end + ) + } + styleSpans.forEach { styleSpan -> + val start = getSpanStart(styleSpan) + val end = getSpanEnd(styleSpan) + when (styleSpan.style) { + Typeface.BOLD -> addStyle(SpanStyle(fontWeight = FontWeight.Bold), start, end) + Typeface.ITALIC -> addStyle(SpanStyle(fontStyle = FontStyle.Italic), start, end) + Typeface.BOLD_ITALIC -> addStyle( + SpanStyle(fontWeight = FontWeight.Bold, fontStyle = FontStyle.Italic), + start, + end + ) + } + } + underlineSpans.forEach { underlineSpan -> + val start = getSpanStart(underlineSpan) + val end = getSpanEnd(underlineSpan) + addStyle(SpanStyle(textDecoration = TextDecoration.Underline), start, end) + } + strikethroughSpans.forEach { strikethroughSpan -> + val start = getSpanStart(strikethroughSpan) + val end = getSpanEnd(strikethroughSpan) + addStyle(SpanStyle(textDecoration = TextDecoration.LineThrough), start, end) + } + } +} \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/billing/ui/InAppBillingHostActivity.kt b/vending-app/src/main/java/org/microg/vending/billing/ui/InAppBillingHostActivity.kt new file mode 100644 index 00000000..85c28a9c --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/billing/ui/InAppBillingHostActivity.kt @@ -0,0 +1,149 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.vending.billing.ui + +import android.accounts.Account +import android.content.Intent +import android.content.res.Configuration +import android.graphics.Rect +import android.os.Bundle +import android.util.Log +import android.view.Gravity +import android.view.MotionEvent +import android.view.View +import android.view.Window +import android.view.WindowManager +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.viewModels +import androidx.annotation.RequiresApi +import androidx.core.os.bundleOf +import androidx.lifecycle.lifecycleScope +import com.android.billingclient.api.BillingClient +import org.microg.vending.billing.ui.logic.NotificationEventId +import org.microg.vending.billing.ui.logic.InAppBillingViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.microg.vending.billing.ADD_PAYMENT_METHOD_URL +import org.microg.vending.billing.TAG + +private const val ADD_PAYMENT_REQUEST_CODE = 30002 + +@RequiresApi(21) +class InAppBillingHostActivity : ComponentActivity() { + private val inAppBillingViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "InAppBillingHostActivity.onCreate") + requestWindowFeature(Window.FEATURE_NO_TITLE) + super.onCreate(savedInstanceState) + lifecycleScope.launchWhenCreated { + initEventHandler() + } + loadView(savedInstanceState != null) + } + + private fun loadView(isRebuild: Boolean) { + if (!isRebuild) { + inAppBillingViewModel.startParams = intent.extras + lifecycleScope.launch(Dispatchers.IO) { + try { + inAppBillingViewModel.loadData(this@InAppBillingHostActivity) + } catch (e: Exception) { + if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "loadSheetUIViewData", e) + withContext(Dispatchers.Main) { + finishWithResult( + bundleOf( + "RESPONSE_CODE" to BillingClient.BillingResponseCode.DEVELOPER_ERROR, + "DEBUG_MESSAGE" to "init ui failed" + ) + ) + } + } + } + } + initWindow() + setContent { BillingUiPage(viewModel = inAppBillingViewModel) } + } + + private suspend fun initEventHandler() { + inAppBillingViewModel.event.collect { + when (it.id) { + NotificationEventId.FINISH -> finishWithResult(it.params) + NotificationEventId.OPEN_PAYMENT_METHOD_ACTIVITY -> { + val account = it.params.getParcelable("account") + val src = it.params.getString("src") + openPaymentMethodActivity(src, account) + } + } + } + } + + private fun initWindow() { + val lp = window.attributes + lp.width = getWindowWidth(this) + lp.height = WindowManager.LayoutParams.WRAP_CONTENT + lp.gravity = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL + window.attributes = lp + window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) + if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) + window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN + } + + private fun openPaymentMethodActivity(src: String?, account: Account?) { + val intent = Intent(this, PlayWebViewActivity::class.java) + intent.putExtra(KEY_WEB_VIEW_ACTION, WebViewAction.ADD_PAYMENT_METHOD.toString()) + intent.putExtra(KEY_WEB_VIEW_OPEN_URL, ADD_PAYMENT_METHOD_URL) + account?.let { + intent.putExtra(KEY_WEB_VIEW_ACCOUNT, account) + } + startActivityForResult(intent, ADD_PAYMENT_REQUEST_CODE) + } + + override fun onTouchEvent(event: MotionEvent?): Boolean { + return when (event?.action) { + MotionEvent.ACTION_UP -> { + val r = Rect(0, 0, 0, 0) + this.window.decorView.getHitRect(r) + val intersects: Boolean = r.contains(event.x.toInt(), event.y.toInt()) + if (!intersects) { + inAppBillingViewModel.close() + return true + } + super.onTouchEvent(event) + } + + else -> super.onTouchEvent(event) + } + } + + private fun finishWithResult(result: Bundle) { + if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "InAppBillingHostActivity.finishWithResult $result") + val resultIntent = Intent() + resultIntent.putExtras(result) + setResult(RESULT_OK, resultIntent) + finish() + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + when (requestCode) { + ADD_PAYMENT_REQUEST_CODE -> { + if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "add payment method resultCode: $resultCode, data: $data") + loadView(false) + } + + else -> { + super.onActivityResult(requestCode, resultCode, data) + finishWithResult( + bundleOf( + "RESPONSE_CODE" to BillingClient.BillingResponseCode.USER_CANCELED + ) + ) + } + } + } +} \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/billing/ui/PlayWebViewActivity.kt b/vending-app/src/main/java/org/microg/vending/billing/ui/PlayWebViewActivity.kt new file mode 100644 index 00000000..8f9e87d7 --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/billing/ui/PlayWebViewActivity.kt @@ -0,0 +1,104 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.vending.billing.ui + +import android.accounts.Account +import android.os.Bundle +import android.util.Log +import android.view.View +import android.view.Window +import android.webkit.WebView +import android.widget.ProgressBar +import android.widget.RelativeLayout +import androidx.activity.ComponentActivity +import org.microg.vending.billing.TAG + +private val ALLOWED_WEB_PREFIXES = setOf( + "https://play.google.com/accounts", + "https://play.google.com/store/paymentmethods", + "https://play.google.com/store/paymentmethods", + "https://pay.google.com/payments", + "https://payments.google.com/payments", +) + +enum class WebViewAction { + ADD_PAYMENT_METHOD, + UNKNOWN +} + +const val KEY_WEB_VIEW_ACTION = "key_web_view_action" +const val KEY_WEB_VIEW_OPEN_URL = "key_web_view_open_url" +const val KEY_WEB_VIEW_ACCOUNT = "key_web_view_account" + +class PlayWebViewActivity : ComponentActivity() { + private lateinit var webView: WebView + private lateinit var openUrl: String + private lateinit var action: WebViewAction + private var account: Account? = null + + private fun parseParams(): Boolean { + val actionStr = intent.getStringExtra(KEY_WEB_VIEW_ACTION) ?: return false + this.action = try { + WebViewAction.valueOf(actionStr) + } catch (e: Exception) { + if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "", e) + return false + } + if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "PlayWebView action:$action") + this.openUrl = intent.getStringExtra(KEY_WEB_VIEW_OPEN_URL) ?: return false + if (openUrl.isBlank()) + return false + this.account = intent.getParcelableExtra(KEY_WEB_VIEW_ACCOUNT) + return true + } + + private fun doAction() { + when (action) { + WebViewAction.ADD_PAYMENT_METHOD -> createWebView() + else -> { + if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "PlayWebView unknown action:$action") + finish() + } + } + } + + private fun createWebView() { + val layout = RelativeLayout(this) + layout.addView(ProgressBar(this).apply { + layoutParams = RelativeLayout.LayoutParams( + RelativeLayout.LayoutParams.WRAP_CONTENT, + RelativeLayout.LayoutParams.WRAP_CONTENT + ).apply { + addRule(RelativeLayout.CENTER_HORIZONTAL) + addRule(RelativeLayout.CENTER_VERTICAL) + } + isIndeterminate = true + }) + webView = WebView(this).apply { + layoutParams = RelativeLayout.LayoutParams( + RelativeLayout.LayoutParams.MATCH_PARENT, + RelativeLayout.LayoutParams.MATCH_PARENT + ) + visibility = View.INVISIBLE + } + layout.addView(webView) + setContentView(layout) + WebViewHelper(this, webView, ALLOWED_WEB_PREFIXES).openWebView(openUrl, account) + setResult(RESULT_OK) + } + + override fun onCreate(savedInstanceState: Bundle?) { + requestWindowFeature(Window.FEATURE_NO_TITLE) + val extras = intent?.extras?.also { it.keySet() } + if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "Invoked with ${intent.action} and extras $extras") + super.onCreate(savedInstanceState) + if (!parseParams()) { + finish() + return + } + doAction() + } +} \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/billing/ui/SheetUIPage.kt b/vending-app/src/main/java/org/microg/vending/billing/ui/SheetUIPage.kt new file mode 100644 index 00000000..9222fcc1 --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/billing/ui/SheetUIPage.kt @@ -0,0 +1,580 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.vending.billing.ui + +import android.util.Log +import androidx.activity.compose.BackHandler +import androidx.annotation.DrawableRes +import androidx.annotation.RequiresApi +import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi +import androidx.compose.animation.graphics.res.animatedVectorResource +import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter +import androidx.compose.animation.graphics.vector.AnimatedImageVector +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material.icons.filled.KeyboardArrowRight +import androidx.compose.material.icons.outlined.Info +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import com.android.vending.R +import com.google.accompanist.systemuicontroller.rememberSystemUiController +import org.microg.vending.billing.TAG +import org.microg.vending.billing.core.ui.* +import org.microg.vending.billing.ui.logic.BillingUiViewState +import org.microg.vending.billing.ui.logic.InAppBillingViewModel +import org.microg.vending.billing.ui.theme.InAppBillingTheme +import org.microg.vending.billing.ui.widgets.LoadingDialog +import org.microg.vending.billing.ui.widgets.PasswdInputDialog + +val LocalBillingUiViewState = + compositionLocalOf { error("No default value provided.") } + +@Composable +fun SetStatusBarColor(color: Color) { + val systemUiController = rememberSystemUiController() + SideEffect { + systemUiController.setSystemBarsColor(color) + } +} + +@RequiresApi(21) +@Composable +fun BillingUiPage(viewModel: InAppBillingViewModel) { + InAppBillingTheme { + SetStatusBarColor(MaterialTheme.colorScheme.background) + CompositionLocalProvider(LocalBillingUiViewState provides viewModel.billingUiViewState) { + BillingUiView(viewModel.billingUiViewState) + } + LoadingDialog(viewModel.loadingDialogVisible) + PasswdInputDialog(viewModel.passwdInputViewState) + } +} + +@Composable +private fun BillingUiView(viewState: BillingUiViewState) { + if (!viewState.visible) + return + BackHandler(true) { + if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "OnBackPressed") + } + Surface( + modifier = Modifier + .wrapContentHeight() + .wrapContentWidth(), + color = MaterialTheme.colorScheme.background + ) { + Column( + modifier = Modifier + ) { + UIComponents(viewState) + } + } +} + +@Composable +private fun UIComponents(viewState: BillingUiViewState) { + val uiComponents = viewState.showScreen.uiComponents ?: return + val headerComponents = uiComponents.headerComponents + val contentComponents = uiComponents.contentComponents + val footerComponents = uiComponents.footerComponents + val action = viewState.showScreen.action + if (headerComponents.isNotEmpty()) { + Column( + modifier = Modifier + .wrapContentHeight(Alignment.CenterVertically) + .wrapContentWidth() + ) { + for (component in headerComponents) { + if (component.uiInfo?.uiType == UIType.BILLING_PROFILE_MORE_OPTION_BUTTON_SHOW_HIDEABLE_INSTRUMENT) + continue + UIComponent(Modifier, component, viewState) + } + } + } + if (contentComponents.isNotEmpty() || footerComponents.isNotEmpty()) { + Column( + modifier = Modifier + .applyUITypePadding(viewState.showScreen.uiInfo?.uiType) + .wrapContentHeight(Alignment.CenterVertically) + .wrapContentWidth() + .verticalScroll(rememberScrollState()) + ) { + for (component in contentComponents) { + UIComponent( + Modifier, + component, + viewState + ) + } + if (viewState.showScreen.uiInfo?.uiType == UIType.PURCHASE_ERROR_SCREEN) { + Box( + modifier = Modifier + .padding(top = 40.dp) + ) + } + + for (component in footerComponents) { + UIComponent(Modifier, component, viewState) + } + } + } + + Box(modifier = Modifier.padding(top = 15.dp)) + if (action?.type == ActionType.DELAY && action.delay != null) { + viewState.onClickAction(action) + } +} + +@Composable +private fun UIComponent( + modifier: Modifier = Modifier, + component: BComponent, + viewState: BillingUiViewState +) { + Column( + modifier = modifier + .fillMaxWidth() + .applyViewInfo( + component.viewInfo + ) + .wrapContentHeight(align = Alignment.CenterVertically), + ) { + when (val type = component.viewType) { + ViewType.ICONTEXTCOMBINATIONVIEW -> IconTextCombinationView( + modifier = applyAlignment(Modifier, component.viewInfo), + component.iconTextCombinationView!! + ) + + ViewType.CLICKABLETEXTVIEW -> ClickableTextView( + modifier = applyAlignment(Modifier, component.viewInfo), + component.clickableTextView!! + ) + + ViewType.VIEWGROUP -> ViewGroup( + modifier = applyAlignment(Modifier, component.viewInfo), + component.viewGroup!! + ) + + ViewType.DIVIDERVIEW -> DividerView( + modifier = applyAlignment(Modifier, component.viewInfo) + ) + + ViewType.MODULOIMAGEVIEW -> ModuloImageView( + modifier = applyAlignment(Modifier, component.viewInfo), + component.moduloImageView!! + ) + + ViewType.BUTTONGROUPVIEW -> ButtonGroupView( + modifier = applyAlignment(Modifier, component.viewInfo), + viewState, + component.buttonGroupView!! + ) + + ViewType.INSTRUMENTITEMVIEW -> InstrumentItemView( + modifier = applyAlignment(Modifier, component.viewInfo), + viewState, + component.instrumentItemView!! + ) + + else -> Log.d(TAG, "invalid component type $type") + } + } +} + +@Composable +private fun PlayTextView( + modifier: Modifier = Modifier, + data: BPlayTextView +) { + val textColor = getColorByType(data.textInfo?.colorType) + ?: LocalContentColor.current + var textValue = "" + if (data.textSpan.isNotEmpty()) { + data.textSpan.forEach { + when (it.textSpanType) { + TextSpanType.BULLETSPAN -> textValue += "\u2022 " + else -> { + if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "Unknown TextSpan type") + } + } + } + } + val fontSize = getFontSizeByType(data.textInfo?.styleType) + textValue += data.text + HtmlText( + text = textValue, + color = textColor, + fontSize = fontSize, + textAlign = getTextAlignment(data.textInfo), + maxLines = data.textInfo?.maxLines ?: Int.MAX_VALUE, + modifier = modifier.applyViewInfo(data.viewInfo), + lineHeight = fontSize.times(1.1) + ) +} + +@Composable +private fun IconTextCombinationView( + modifier: Modifier = Modifier, + data: BIconTextCombinationView +) { + Column( + modifier = modifier + .wrapContentHeight() + .wrapContentWidth() + ) { + //角标 + data.badgeTextView?.let { + PlayTextView( + modifier = applyAlignment(Modifier, it.viewInfo), + data = it + ) + } + Row( + modifier = Modifier + .wrapContentHeight() + .wrapContentWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + data.headerImageView?.let { + ImageView( + data = it, + modifier = applyAlignment(Modifier, it.viewInfo), + ) + } + data.playTextView?.let { + PlayTextView(modifier = applyAlignment(Modifier, it.viewInfo), data = it) + } + if (data.middleTextViewList == null) + return + Column( + modifier = applyAlignment(Modifier, data.viewInfo) + .applyViewInfo(data.viewInfo) + .wrapContentHeight() + .wrapContentWidth() + ) { + for (singleLineTextView in data.middleTextViewList) { + Row( + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight() + ) { + singleLineTextView.playTextView1?.let { + PlayTextView( + applyAlignment( + Modifier, + it.viewInfo + ).wrapContentWidth(align = Alignment.CenterHorizontally), + data = it + ) + } + singleLineTextView.playTextView2?.let { + PlayTextView( + applyAlignment( + Modifier, + it.viewInfo + ).fillMaxWidth(), + data = it + ) + } + } + } + } + if (data.footerImageGroup?.imageViews?.isNotEmpty() == true) { + val viewInfo = data.footerImageGroup.viewInfo + val imageViews = data.footerImageGroup.imageViews + Spacer( + modifier = Modifier + .weight(1f) + .fillMaxHeight() + ) + Column( + modifier = applyAlignment(Modifier, viewInfo) + .applyViewInfo(viewInfo) + .wrapContentHeight() + .wrapContentWidth() + ) { + imageViews.forEach { + ImageView( + data = it, + modifier = applyAlignment(Modifier, it.viewInfo), + ) + } + } + } + } + } +} + +@Composable +private fun ClickableTextView( + modifier: Modifier = Modifier, + data: BClickableTextView +) { + val playTextView = data.playTextView ?: return + if (playTextView.text.isEmpty()) + return + PlayTextView(modifier, data = playTextView) +} + +@Composable +private fun InstrumentItemView( + modifier: Modifier = Modifier, + viewState: BillingUiViewState, + data: BInstrumentItemView +) { + var newModify = modifier + .wrapContentHeight() + .fillMaxWidth() + newModify = when (data.action?.uiInfo?.uiType) { + UIType.BILLING_PROFILE_EXISTING_INSTRUMENT, + UIType.BILLING_PROFILE_OPTION_ADD_PLAY_CREDIT, + UIType.BILLING_PROFILE_OPTION_REDEEM_CODE, + UIType.BILLING_PROFILE_OPTION_CREATE_INSTRUMENT -> { + newModify.clickable { + viewState.onClickAction(data.action) + } + } + + else -> newModify + } + + Row( + modifier = newModify + ) { + data.icon?.let { + ImageView( + data = it, + modifier = applyAlignment(Modifier, it.viewInfo) + .align(Alignment.CenterVertically), + size = DpSize(32.dp, 32.dp) + ) + } + if (data.text != null || data.tips != null) { + Column( + modifier = Modifier + .align(Alignment.CenterVertically) + .wrapContentSize(), + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Center + ) { + data.text?.let { + PlayTextView( + applyAlignment( + Modifier, + it.viewInfo + ).wrapContentSize(), + data = it + ) + } + data.tips?.let { + PlayTextView( + applyAlignment( + Modifier, + it.viewInfo + ).wrapContentSize(), + data = it + ) + } + data.extraInfo?.let { + PlayTextView( + applyAlignment( + Modifier, + it.viewInfo + ).wrapContentSize(), + data = it + ) + } + } + } + data.state?.let { + Spacer( + modifier = Modifier + .weight(1f) + .fillMaxHeight() + .background(Color.Red) + ) + ImageView( + data = it, + modifier = applyAlignment(Modifier, it.viewInfo) + .align(Alignment.CenterVertically), + size = DpSize(32.dp, 32.dp) + ) + } + } +} + +@Composable +private fun ButtonGroupView( + modifier: Modifier = Modifier, + viewState: BillingUiViewState, + data: BButtonGroupView +) { + if (data.buttonViewList.isNullOrEmpty()) + return + Row( + modifier = modifier + .wrapContentHeight(align = Alignment.CenterVertically) + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + data.buttonViewList.forEach { + Button( + onClick = { viewState.onClickAction(it.action) }, + shape = RoundedCornerShape(10), + modifier = modifier + .weight(1f) + .defaultMinSize(minWidth = 1.dp, minHeight = 1.dp), + contentPadding = PaddingValues(7.dp) + ) { + Text( + text = it.text + ) + } + } + } +} + +@Composable +private fun ImageView( + modifier: Modifier = Modifier, + data: BImageView, + size: DpSize? = null, +) { + ((if (isSystemInDarkTheme()) data.darkUrl else data.lightUrl) ?: data.darkUrl + ?: data.lightUrl)?.let { + AsyncImage( + modifier = modifier + .applyViewInfo(data.viewInfo) + .applySize(size), + model = it, + contentDescription = data.viewInfo?.contentDescription, + colorFilter = getColorFilter(data.imageInfo), + contentScale = getContentScale(data.imageInfo) ?: ContentScale.Fit + ) + } + data.animation?.let { + if (it.type == AnimationType.CHECK_MARK.value) { + AnimatedVector( + modifier = modifier + .applyViewInfo(data.viewInfo) + .applySize(size), + R.drawable.anim_check_mark + ) + } + } + data.iconView?.let { + when (it.type) { + 1 -> Icon( + Icons.Filled.CheckCircle, + modifier = modifier + .applyViewInfo(data.viewInfo) + .size(24.dp), + contentDescription = data.viewInfo?.contentDescription + ) + + 3 -> Icon( + Icons.Filled.KeyboardArrowRight, + modifier = modifier + .applyViewInfo(data.viewInfo) + .size(24.dp), + contentDescription = data.viewInfo?.contentDescription + ) + + 21 -> Icon( + Icons.Filled.ArrowBack, + modifier = modifier + .applyViewInfo(data.viewInfo) + .size(48.dp), + contentDescription = data.viewInfo?.contentDescription + ) + + 27 -> Icon( + Icons.Outlined.Info, + modifier = modifier + .applyViewInfo(data.viewInfo) + .size(11.dp), + contentDescription = data.viewInfo?.contentDescription + ) + + 99 -> Image( + painter = painterResource(R.drawable.google_play), + modifier = modifier + .applyViewInfo(data.viewInfo) + .applySize(size), + contentDescription = data.viewInfo?.contentDescription, + colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground) + ) + } + } +} + +@Composable +private fun ModuloImageView( + modifier: Modifier = Modifier, + data: BModuloImageView +) { + data.imageView?.let { + ImageView(modifier = modifier, data = it) + } +} + +@Composable +private fun ViewGroup( + modifier: Modifier = Modifier, + data: BViewGroup +) { + Row( + modifier = modifier + .wrapContentHeight() + .wrapContentHeight() + ) { + data.imageView2?.let { + ImageView(modifier = applyAlignment(Modifier, it.viewInfo), data = it) + } + data.playTextView?.let { + PlayTextView(modifier = applyAlignment(Modifier, it.viewInfo), data = it) + } + data.imageView3?.let { + Spacer(modifier = Modifier.weight(1f)) + ImageView(modifier = applyAlignment(Modifier, it.viewInfo), data = it) + } + } +} + +@Composable +private fun DividerView( + modifier: Modifier = Modifier +) { + Divider( + modifier = modifier + ) +} + +@OptIn(ExperimentalAnimationGraphicsApi::class) +@Composable +private fun AnimatedVector(modifier: Modifier = Modifier, @DrawableRes drawableId: Int) { + val image = AnimatedImageVector.animatedVectorResource(drawableId) + var atEnd by remember { mutableStateOf(false) } + Image( + painter = rememberAnimatedVectorPainter(image, atEnd), + contentDescription = "", + modifier = modifier + ) + atEnd = true +} \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/billing/ui/Utils.kt b/vending-app/src/main/java/org/microg/vending/billing/ui/Utils.kt new file mode 100644 index 00000000..b2932331 --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/billing/ui/Utils.kt @@ -0,0 +1,120 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.vending.billing.ui + +import android.content.Context +import android.content.res.Configuration +import android.util.Log +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.sp +import org.microg.vending.billing.TAG +import org.microg.vending.billing.core.ui.BGravity +import org.microg.vending.billing.core.ui.BImageInfo +import org.microg.vending.billing.core.ui.BTextInfo +import org.microg.vending.billing.core.ui.ColorType +import org.microg.vending.billing.core.ui.TextAlignmentType + +fun getWindowWidth(context: Context): Int { + val resources = context.resources + return when (resources.configuration.orientation) { + Configuration.ORIENTATION_PORTRAIT -> resources.displayMetrics.widthPixels + Configuration.ORIENTATION_LANDSCAPE -> (resources.displayMetrics.widthPixels * 0.6).toInt() + else -> resources.displayMetrics.widthPixels + } +} + +@Composable +fun DayNightColor(day: Long, night: Long): Color = if (isSystemInDarkTheme()) Color(night) else Color(day) + +// TODO: Manage colors in theme +@Composable +fun getColorByType(t: ColorType?): Color? { + if (t == null) + return null + return when (t) { + ColorType.BACKGROUND_PRIMARY -> MaterialTheme.colorScheme.background + ColorType.APPS_PRIMARY -> MaterialTheme.colorScheme.primary + ColorType.APPS_2 -> DayNightColor(0xff5f6368, 0xffcae2ff) + ColorType.APPS_3 -> DayNightColor(0xff5f6368, 0xff003b92) + ColorType.MUSIC_3 -> Color.Transparent + ColorType.TEXT_PRIMARY -> DayNightColor(0xff202124, 0xffe8eaed) + ColorType.TEXT_SECONDARY -> DayNightColor(0xff5f6368, 0xff9aa0a6) + ColorType.PRIMARY_BUTTON_LABEL_DISABLED -> DayNightColor(0xff5f6368, 0xff5b5e64) + ColorType.ERROR_COLOR_PRIMARY -> MaterialTheme.colorScheme.error + else -> { + if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "invalid color type $t") + Color.Transparent + } + } +} + +@Composable +fun getColorFilter(imageInfo: BImageInfo?): ColorFilter? { + if (imageInfo?.colorFilterValue != null) + return ColorFilter.tint(Color(imageInfo.colorFilterValue)) + if (imageInfo?.colorFilterType != null) { + return when (imageInfo.colorFilterType) { + 21, 3 -> ColorFilter.tint( + if (isSystemInDarkTheme()) Color(0xffe8eaed) else Color( + 0xff202124 + ) + ) + + else -> { + if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "Unknown color filter type: ${imageInfo.colorFilterType}") + null + } + } + } + return null +} + +fun getContentScale(imageInfo: BImageInfo?): ContentScale? { + return null +} + +fun getFontSizeByType(t: Int?): TextUnit { + return when (t) { + 10 -> 18.sp + 12 -> 16.sp + 14 -> 14.sp + 20 -> 14.sp + 21 -> 12.sp + null -> 14.sp + else -> { + if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "invalid font type $t") + 14.sp + } + } +} + +fun getTextAlignment(textInfo: BTextInfo?): TextAlign { + val gravitys = textInfo?.gravityList + if (gravitys?.containsAll(listOf(BGravity.LEFT, BGravity.START)) == true) + return TextAlign.Left + if (gravitys?.containsAll(listOf(BGravity.CENTER, BGravity.CENTER_HORIZONTAL)) == true) + return TextAlign.Center + if (gravitys?.containsAll(listOf(BGravity.RIGHT, BGravity.END)) == true) + return TextAlign.Right + return when (val t = textInfo?.textAlignmentType) { + TextAlignmentType.TEXT_ALIGNMENT_TEXT_END -> TextAlign.Right + TextAlignmentType.TEXT_ALIGNMENT_TEXT_START -> TextAlign.Left + TextAlignmentType.TEXT_ALIGNMENT_CENTER -> TextAlign.Center + TextAlignmentType.TEXT_ALIGNMENT_VIEW_END -> TextAlign.Right + null -> TextAlign.Left + else -> { + if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "invalid text alignment type $t") + TextAlign.Left + } + } +} \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/billing/ui/WebViewHelper.kt b/vending-app/src/main/java/org/microg/vending/billing/ui/WebViewHelper.kt new file mode 100644 index 00000000..e2a1237f --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/billing/ui/WebViewHelper.kt @@ -0,0 +1,117 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.vending.billing.ui + +import android.accounts.Account +import android.accounts.AccountManager +import android.content.Intent +import android.net.Uri +import android.util.Log +import android.view.View +import android.webkit.WebResourceRequest +import android.webkit.WebSettings +import android.webkit.WebView +import androidx.activity.ComponentActivity +import androidx.lifecycle.lifecycleScope +import androidx.webkit.WebResourceErrorCompat +import androidx.webkit.WebViewClientCompat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.microg.gms.profile.Build +import org.microg.vending.billing.TAG +import java.net.URLEncoder +import java.util.Locale +import java.util.concurrent.TimeUnit + +class WebViewHelper( + private val activity: ComponentActivity, + private val webView: WebView, + private val allowedPrefixes: Set = emptySet() +) { + fun openWebView(url: String, account: Account?) { + prepareWebViewSettings(webView.settings) + webView.webViewClient = object : WebViewClientCompat() { + override fun onReceivedError(view: WebView, request: WebResourceRequest, error: WebResourceErrorCompat) { + Log.e(TAG, "Error loading: $error") + } + + override fun onPageFinished(view: WebView?, url: String?) { + super.onPageFinished(view, url) + if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "onPageFinished $url") + webView.visibility = View.VISIBLE + } + + override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean { + if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "Navigating to $url") + // TODO: figure out to not embed page of third parties, but allow bank confirmations +// if (allowedPrefixes.isNotEmpty() && allowedPrefixes.none { url.startsWith(it) }) { +// try { +// activity.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)).apply { addCategory(Intent.CATEGORY_BROWSABLE) }) +// } catch (e: Exception) { +// Log.d(TAG, "Error forwarding to browser", e) +// } +// return true +// } + return false + } + } + + val urlWithLanguage: String = addLanguageParam(url) + if (account != null) { + activity.lifecycleScope.launch(Dispatchers.IO) { + openWebWithAccount(account, urlWithLanguage) + } + } else { + loadWebViewUrl(urlWithLanguage) + } + } + + private fun loadWebViewUrl(url: String) { + webView.loadUrl(url) + } + + private fun addLanguageParam(url: String): String { + val language = Locale.getDefault().language + return if (language.isNotEmpty()) { + Uri.parse(url).buildUpon().appendQueryParameter("hl", language).toString() + } else { + url + } + } + + private fun openWebWithAccount(account: Account, url: String) { + try { + val service = "weblogin:continue=" + URLEncoder.encode(url, "utf-8") + val accountManager: AccountManager = AccountManager.get(activity) + val future = accountManager.getAuthToken(account, service, null, null, null, null) + val bundle = future.getResult(20, TimeUnit.SECONDS) + val authUrl = bundle.getString(AccountManager.KEY_AUTHTOKEN) + ?: throw RuntimeException("authUrl is null") + if (authUrl.contains("WILL_NOT_SIGN_IN")) { + throw RuntimeException("Would not sign in") + } + if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "Opening $authUrl") + webView.post { + loadWebViewUrl(authUrl) + } + } catch (e: Exception) { + if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "Failed to get weblogin auth.", e) + activity.finish() + } + } + + private fun prepareWebViewSettings(settings: WebSettings) { + settings.javaScriptEnabled = true + settings.setSupportMultipleWindows(false) + settings.allowFileAccess = false + settings.databaseEnabled = false + settings.setNeedInitialFocus(false) + settings.useWideViewPort = false + settings.setSupportZoom(false) + settings.javaScriptCanOpenWindowsAutomatically = false + settings.userAgentString = Build.generateWebViewUserAgentString(settings.userAgentString) + } +} \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/billing/ui/logic/InAppBillingViewModel.kt b/vending-app/src/main/java/org/microg/vending/billing/ui/logic/InAppBillingViewModel.kt new file mode 100644 index 00000000..f0995101 --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/billing/ui/logic/InAppBillingViewModel.kt @@ -0,0 +1,310 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.vending.billing.ui.logic + +import android.content.Context +import android.os.Bundle +import android.util.Log +import androidx.annotation.RequiresApi +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.core.os.bundleOf +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.android.billingclient.api.BillingClient +import com.android.volley.VolleyError +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch +import org.microg.gms.utils.toHexString +import org.microg.vending.billing.* +import org.microg.vending.billing.core.ui.ActionType +import org.microg.vending.billing.core.ui.BAction +import org.microg.vending.billing.core.ui.UIType + +enum class NotificationEventId { + FINISH, + OPEN_PAYMENT_METHOD_ACTIVITY +} + +enum class ErrorMessageRef { + PASSWORD_ERROR, + NETWORK_ERROR, +} + +data class NotificationEvent( + val id: NotificationEventId, + val params: Bundle +) + +@RequiresApi(21) +class InAppBillingViewModel : ViewModel() { + private val _event = Channel() + val event = _event.receiveAsFlow() + var startParams: Bundle? = null + + var loadingDialogVisible by mutableStateOf(value = false) + private set + var billingUiViewState by mutableStateOf( + value = BillingUiViewState( + onClickAction = ::handleClickAction + ) + ) + private set + var passwdInputViewState by mutableStateOf( + value = PasswdInputViewState( + onDismissRequest = ::handlePasswdInputViewDismiss, + onButtonClicked = ::handlePasswdInput, + onCheckedChange = ::handlePasswdCheckedChange, + checked = !SettingsManager(ContextProvider.context).getAuthStatus() + ) + ) + private lateinit var lastBuyFlowResult: BuyFlowResult + + private fun finishWithResult(result: Bundle) { + viewModelScope.launch { + _event.send(NotificationEvent(NotificationEventId.FINISH, result)) + } + } + + private fun handlePasswdInputViewDismiss() { + if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "handlePasswdInputViewDismiss called") + passwdInputViewState = passwdInputViewState.copy(visible = false) + } + + private suspend fun submitBuyAction(authToken: String? = null) { + val param = startParams?.getString(KEY_IAP_SHEET_UI_PARAM) ?: return finishWithResult( + billingUiViewState.result + ) + val buyFlowResult = + InAppBillingServiceImpl.acquireRequest(ContextProvider.context, param, billingUiViewState.actionContextList, authToken) + handleBuyFlowResult(buyFlowResult) + } + + private suspend fun doAcquireRequest() { + val param = startParams?.getString(KEY_IAP_SHEET_UI_PARAM) ?: return finishWithResult( + billingUiViewState.result + ) + val buyFlowResult = InAppBillingServiceImpl.acquireRequest(ContextProvider.context, param, billingUiViewState.actionContextList) + handleBuyFlowResult(buyFlowResult) + } + + private fun handlePasswdInput(password: String) { + loadingDialogVisible = true + passwdInputViewState = passwdInputViewState.copy(visible = false) + billingUiViewState = billingUiViewState.copy(visible = false) + viewModelScope.launch(Dispatchers.IO) { + val param = startParams!!.getString(KEY_IAP_SHEET_UI_PARAM) + val (statusCode, encodedRapt) = try { + 200 to InAppBillingServiceImpl.requestAuthProofToken(ContextProvider.context, param!!, password) + } catch (e: VolleyError) { + Log.w(TAG, e) + e.networkResponse.statusCode to null + } catch (e: Exception) { + -1 to null + } + if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "requestAuthProofToken statusCode=$statusCode, encodedRapt=$encodedRapt") + if (encodedRapt.isNullOrEmpty()) { + loadingDialogVisible = false + val errMsg = when (statusCode) { + 400 -> ErrorMessageRef.PASSWORD_ERROR + else -> ErrorMessageRef.NETWORK_ERROR + } + passwdInputViewState = + passwdInputViewState.copy(visible = true, hasError = true, errMsg = errMsg) + billingUiViewState = billingUiViewState.copy(visible = true) + return@launch + } + if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "handleBuyButtonClicked encodedRapt: $encodedRapt") + SettingsManager(ContextProvider.context).setAuthStatus(!passwdInputViewState.checked) + submitBuyAction(authToken = encodedRapt) + } + } + + private fun handlePasswdCheckedChange(checked: Boolean) { + if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "handlePasswdCheckedChange checked: $checked") + passwdInputViewState = passwdInputViewState.copy(checked = checked) + } + + private fun handleBuyButtonClicked(action: BAction) { + val nextShowScreen = billingUiViewState.screenMap[action.screenId] + ?: return finishWithResult(billingUiViewState.result) + when (val uiType = nextShowScreen.uiInfo?.uiType) { + UIType.LOADING_SPINNER -> { + showLoading() + billingUiViewState.actionContextList.addAll(action.actionContext) + viewModelScope.launch(Dispatchers.IO) { + submitBuyAction() + } + } + + UIType.PURCHASE_AUTH_SCREEN -> { + if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "handleBuyButtonClicked need auth") + val account = + lastBuyFlowResult.account ?: return finishWithResult(billingUiViewState.result) + // TODO: Magic constants? These are protobufs! + billingUiViewState.actionContextList.add("ea010408011001b80301".decodeHex()) + billingUiViewState.actionContextList.add("0a020802b80301".decodeHex()) + passwdInputViewState = passwdInputViewState.copy( + visible = true, + label = account.name + ) + } + + else -> { + if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "handleBuyButtonClicked unknown next uiType: $uiType") + finishWithResult(billingUiViewState.result) + } + } + } + + private fun showLoading() { + loadingDialogVisible = true + passwdInputViewState = passwdInputViewState.copy(visible = false) + billingUiViewState = billingUiViewState.copy(visible = false) + } + + private fun handleClickAction(action: BAction?) { + if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "handleClickAction action: $action (contexts: ${action?.actionContext?.map { it.toHexString() }}") + when (action?.type) { + ActionType.SHOW -> { + when (action.uiInfo?.uiType) { + UIType.PURCHASE_CART_BUY_BUTTON -> handleBuyButtonClicked(action) + UIType.PURCHASE_CHANGE_SUBSCRIPTION_CONTINUE_BUTTON, + UIType.PURCHASE_PAYMENT_DECLINED_CONTINUE_BUTTON, + UIType.PURCHASE_CART_PAYMENT_OPTIONS_LINK, + UIType.PURCHASE_CART_CONTINUE_BUTTON, + UIType.BILLING_PROFILE_SCREEN_ABANDON -> { + if (action.screenId?.isNotBlank() == true) { + if (showScreen(action.screenId!!)) { + if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "showScreen ${action.screenId} success") + return + } + if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "showScreen ${action.screenId} false") + } + finishWithResult(billingUiViewState.result) + } + + UIType.BILLING_PROFILE_OPTION_CREATE_INSTRUMENT, + UIType.BILLING_PROFILE_OPTION_ADD_PLAY_CREDIT, + UIType.BILLING_PROFILE_OPTION_REDEEM_CODE -> { + viewModelScope.launch(Dispatchers.IO) { + showPaymentMethodPage("action") + } + } + + else -> finishWithResult(billingUiViewState.result) + } + } + + ActionType.DELAY -> { + viewModelScope.launch { + delay(action.delay!!.toLong()) + finishWithResult(billingUiViewState.result) + } + } + + else -> { + when (action?.uiInfo?.uiType) { + UIType.PURCHASE_CART_CONTINUE_BUTTON -> viewModelScope.launch(Dispatchers.IO) { + submitBuyAction() + } + + UIType.PURCHASE_SUCCESS_SCREEN_WITH_AUTH_CHOICES -> { + viewModelScope.launch { + delay(3000) + finishWithResult(billingUiViewState.result) + } + } + + UIType.BILLING_PROFILE_EXISTING_INSTRUMENT -> { + if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "switch payment method context: ${action.actionContext[0].toHexString()}") + showLoading() + billingUiViewState.actionContextList.addAll(action.actionContext) + viewModelScope.launch(Dispatchers.IO) { + doAcquireRequest() + } + } + + UIType.PURCHASE_CART_PAYMENT_OPTIONS_LINK -> { + if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "open payment option page context: ${action.actionContext[0].toHexString()}") + showLoading() + billingUiViewState.actionContextList.addAll(action.actionContext) + viewModelScope.launch(Dispatchers.IO) { + doAcquireRequest() + } + } + + else -> finishWithResult(billingUiViewState.result) + } + } + } + } + + private fun showScreen(screenId: String): Boolean { + val showScreen = billingUiViewState.screenMap[screenId] ?: return false + billingUiViewState = billingUiViewState.copy( + showScreen = showScreen, + visible = true + ) + loadingDialogVisible = false + return true + } + + private suspend fun showPaymentMethodPage(src: String): Boolean { + _event.send( + NotificationEvent( + NotificationEventId.OPEN_PAYMENT_METHOD_ACTIVITY, + bundleOf("account" to lastBuyFlowResult.account, "src" to src) + ) + ) + return true + } + + private suspend fun handleBuyFlowResult(buyFlowResult: BuyFlowResult) { + val failAction = suspend { + _event.send(NotificationEvent(NotificationEventId.FINISH, buyFlowResult.result)) + } + val action = buyFlowResult.acquireResult?.action ?: return failAction() + val screenMap = buyFlowResult.acquireResult.screenMap + val showScreen = screenMap[action.screenId] ?: return failAction() + if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "handleAcquireResult, showScreen:$showScreen result:${buyFlowResult.acquireResult}") + if (action.type != ActionType.SHOW) return failAction() + lastBuyFlowResult = buyFlowResult + billingUiViewState.screenMap.putAll(screenMap) + billingUiViewState = billingUiViewState.copy( + showScreen = showScreen, + result = buyFlowResult.result, + actionContextList = action.actionContext, + visible = true + ) + loadingDialogVisible = false + } + + private suspend fun doLoadSheetUIAction(context: Context, param: String) { + val buyFlowResult = InAppBillingServiceImpl.acquireRequest(context, param, firstRequest = true) + handleBuyFlowResult(buyFlowResult) + } + + suspend fun loadData(context: Context) { + val param = startParams?.getString(KEY_IAP_SHEET_UI_PARAM) + ?: throw RuntimeException("get action param failed") + if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "loadData param:$param") + showLoading() + doLoadSheetUIAction(context, param) + } + + fun close() { + val result = if (billingUiViewState.result.containsKey("INAPP_PURCHASE_DATA")) + billingUiViewState.result + else + resultBundle(BillingClient.BillingResponseCode.USER_CANCELED, "") + finishWithResult(result) + } +} \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/billing/ui/logic/Models.kt b/vending-app/src/main/java/org/microg/vending/billing/ui/logic/Models.kt new file mode 100644 index 00000000..bd9eff9b --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/billing/ui/logic/Models.kt @@ -0,0 +1,38 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.vending.billing.ui.logic + +import android.accounts.Account +import android.os.Bundle +import org.microg.vending.billing.core.ui.AcquireParsedResult +import org.microg.vending.billing.core.ui.BAction +import org.microg.vending.billing.core.ui.BScreen + +data class BillingUiViewState( + val visible: Boolean = false, + val screenMap: MutableMap = mutableMapOf(), + val showScreen: BScreen = BScreen(), + val result: Bundle = Bundle.EMPTY, + val actionContextList: MutableList = mutableListOf(), + val onClickAction: (BAction?) -> Unit +) + +data class PasswdInputViewState( + var visible: Boolean = false, + var label: String = "", + var hasError: Boolean = false, + var errMsg: ErrorMessageRef? = null, + var checked: Boolean = false, + val onButtonClicked: (passwd: String) -> Unit, + val onCheckedChange: (value: Boolean) -> Unit, + val onDismissRequest: () -> Unit +) + +data class BuyFlowResult( + val acquireResult: AcquireParsedResult?, + val account: Account?, + val result: Bundle +) \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/billing/ui/theme/Color.kt b/vending-app/src/main/java/org/microg/vending/billing/ui/theme/Color.kt new file mode 100644 index 00000000..fce6c400 --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/billing/ui/theme/Color.kt @@ -0,0 +1,13 @@ +package org.microg.vending.billing.ui.theme + +import androidx.compose.ui.graphics.Color + +// TODO: Use colors from Play + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260) \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/billing/ui/theme/Theme.kt b/vending-app/src/main/java/org/microg/vending/billing/ui/theme/Theme.kt new file mode 100644 index 00000000..ce404007 --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/billing/ui/theme/Theme.kt @@ -0,0 +1,78 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.vending.billing.ui.theme + +import android.app.Activity +import android.os.Build.VERSION.SDK_INT +import androidx.annotation.RequiresApi +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView +import androidx.core.view.WindowCompat + +// TODO: Use theme from Play + +private val DarkColorScheme = darkColorScheme( + primary = Purple80, + secondary = PurpleGrey80, + tertiary = Pink80 +) + +private val LightColorScheme = lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40 + + /* Other default colors to override + background = Color(0xFFFFFBFE), + surface = Color(0xFFFFFBFE), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.White, + onBackground = Color(0xFF1C1B1F), + onSurface = Color(0xFF1C1B1F), + */ +) +@RequiresApi(21) +@Composable +fun InAppBillingTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + dynamicColor: Boolean = true, + content: @Composable () -> Unit +) { + val colorScheme = when { + dynamicColor && SDK_INT >= 31 -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + val view = LocalView.current + if (!view.isInEditMode) { + SideEffect { + val window = (view.context as Activity).window + window.statusBarColor = colorScheme.primary.toArgb() + WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme + } + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/billing/ui/theme/Type.kt b/vending-app/src/main/java/org/microg/vending/billing/ui/theme/Type.kt new file mode 100644 index 00000000..99527cce --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/billing/ui/theme/Type.kt @@ -0,0 +1,34 @@ +package org.microg.vending.billing.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) + /* Other default text styles to override + titleLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + labelSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ) + */ +) \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/billing/ui/widgets/LoadingDialog.kt b/vending-app/src/main/java/org/microg/vending/billing/ui/widgets/LoadingDialog.kt new file mode 100644 index 00000000..46921742 --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/billing/ui/widgets/LoadingDialog.kt @@ -0,0 +1,36 @@ +package org.microg.vending.billing.ui.widgets + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.unit.dp + +@Composable +fun LoadingDialog(visible: Boolean) { + if (visible) { + BackHandler(enabled = true) { + + } + Box( + modifier = Modifier + .height(LocalConfiguration.current.screenHeightDp.dp.times(0.3f)) + .fillMaxWidth() + .background(MaterialTheme.colorScheme.background) + ) { + CircularProgressIndicator( + modifier = Modifier + .align(alignment = Alignment.Center), + color = MaterialTheme.colorScheme.primary + ) + } + } +} \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/billing/ui/widgets/PasswdInputDialog.kt b/vending-app/src/main/java/org/microg/vending/billing/ui/widgets/PasswdInputDialog.kt new file mode 100644 index 00000000..d7387ff6 --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/billing/ui/widgets/PasswdInputDialog.kt @@ -0,0 +1,182 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.vending.billing.ui.widgets + +import android.app.ActionBar +import android.view.Gravity +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Button +import androidx.compose.material3.Checkbox +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogWindowProvider +import org.microg.vending.billing.ui.HtmlText +import org.microg.vending.billing.ui.getWindowWidth +import org.microg.vending.billing.ui.logic.PasswdInputViewState +import com.android.vending.R +import org.microg.vending.billing.ui.logic.ErrorMessageRef + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PasswdInputDialog(viewState: PasswdInputViewState) { + if (viewState.visible) { + Dialog(onDismissRequest = viewState.onDismissRequest) { + val dialogWindow = (LocalView.current.parent as DialogWindowProvider).window + val lp = dialogWindow.attributes + lp.width = getWindowWidth(LocalContext.current) + lp.height = ActionBar.LayoutParams.WRAP_CONTENT + lp.gravity = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL + dialogWindow.attributes = lp + Surface( + modifier = Modifier + .wrapContentHeight() + .wrapContentWidth(), + color = MaterialTheme.colorScheme.background + ) { + Column( + Modifier + .background(MaterialTheme.colorScheme.background) + .wrapContentHeight() + .wrapContentWidth() + ) { + var inputText by remember { mutableStateOf("") } + val focusRequester = remember { FocusRequester() } + OutlinedTextField( + modifier = Modifier + .padding(top = 15.dp, start = 20.dp, end = 20.dp) + .fillMaxWidth() + .focusRequester(focusRequester), + value = inputText, + label = { Text(text = viewState.label) }, + placeholder = { Text(text = stringResource(R.string.tips_input_passwd)) }, + onValueChange = { + inputText = it + viewState.hasError = false + }, + visualTransformation = PasswordVisualTransformation(), + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Password, + imeAction = ImeAction.Done + ), + singleLine = true, + isError = viewState.hasError, + colors = TextFieldDefaults.outlinedTextFieldColors( + errorLabelColor = if (viewState.hasError) MaterialTheme.colorScheme.error else LocalContentColor.current, + errorBorderColor = if (viewState.hasError) MaterialTheme.colorScheme.error else LocalContentColor.current + ), + supportingText = { + if (viewState.hasError) { + Text( + text = when (viewState.errMsg) { + ErrorMessageRef.PASSWORD_ERROR -> stringResource(R.string.error_passwd) + ErrorMessageRef.NETWORK_ERROR -> stringResource(R.string.error_network) + null -> "" + }, + color = MaterialTheme.colorScheme.error + ) + } + } + ) + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } + Row( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(start = 15.dp, end = 20.dp, top = 8.dp, bottom = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox( + modifier = Modifier.align(Alignment.CenterVertically), + checked = viewState.checked, + onCheckedChange = { + viewState.onCheckedChange(it) + } + ) + Text( + text = stringResource(R.string.tips_remember_login_info), + modifier = Modifier + .padding(start = 14.dp) + .align(Alignment.CenterVertically) + .clickable { + viewState.onCheckedChange(!viewState.checked) + }, + fontSize = 14.sp + ) + } + Row( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(start = 20.dp, end = 20.dp, bottom = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + HtmlText( + text = "${stringResource(R.string.tips_forget_passwd)}", + fontSize = 14.sp + ) + HtmlText( + modifier = Modifier.padding(start = 5.dp), + text = "${stringResource(R.string.tips_more_details)}", + fontSize = 14.sp + ) + } + Button( + modifier = Modifier + .defaultMinSize(minWidth = 1.dp, minHeight = 1.dp) + .fillMaxWidth() + .wrapContentHeight() + .padding(start = 20.dp, end = 20.dp), + enabled = inputText.isNotBlank(), + onClick = { viewState.onButtonClicked(inputText) }, + shape = RoundedCornerShape(20), + contentPadding = PaddingValues(7.dp) + ) { + Text( + text = stringResource(R.string.text_verify_button) + ) + } + } + } + } + } +} \ No newline at end of file diff --git a/vending-app/src/main/proto/GooglePlay.proto b/vending-app/src/main/proto/GooglePlay.proto new file mode 100644 index 00000000..ec0b0b19 --- /dev/null +++ b/vending-app/src/main/proto/GooglePlay.proto @@ -0,0 +1,688 @@ +syntax = "proto3"; + +option java_package = "org.microg.vending.billing.proto"; +option java_multiple_files = true; + +import "Timestamp.proto"; + + +message SkuDetailsRequest { + int32 apiVersion = 1; + string package = 2; + string type = 3; + repeated string skuId = 4; + repeated DynamicSku dynamicSku = 5; + bool isWifi = 6; + SkuDetailsExtra skuDetailsExtra = 7; + string skuPackage = 8; + repeated OfferSku offerSkus = 9; + repeated MultiOfferSkuDetail multiOfferSkuDetail = 10; +} + +message AcquireRequest { + DocumentInfo documentInfo = 1; + ClientInfo clientInfo = 2; + bytes serverContextToken = 3; + repeated bytes actionContext = 4; + string clientTokenB64 = 5; + DeviceAuthInfo deviceAuthInfo = 8; + map authTokens = 9; + UnkMessage5 unknown12 = 12; + string deviceIDBase64 = 19; + string newAcquireCacheKey = 20; + string nonce = 22; + int32 theme = 25; + Timestamp ts = 31; +} + +message CKDocument { + DocId docId = 1; + oneof SkuOfferIdToken { + string token3 = 2; + string token14 = 4; + } + int32 unknown3 = 3; +} + +message DeviceAuthInfo { + bool canAuthenticate = 1; + bool isBiometricStrong = 2; + bool fingerprintValid = 3; + int32 desiredAuthMethod = 4; + int32 unknown5 = 5; + int32 authFrequency = 6; + bool unknown9 = 9; + bool userHasFop = 16; + map authParams = 18; + bool unknown20 = 20; + ItemColor itemColor = 26; + string droidGuardPayload = 30; +} + +message UnkMessage5 { + int32 unknown1 = 1; +} + +message ItemColor { + int32 androidAppsColor = 1; + int32 booksColor = 2; + int32 musicColor = 3; + int32 moviesColor = 4; + int32 newsStandColor = 5; +} + +message ClientInfo { + int32 apiVersion = 1; + string package = 2; + int32 versionCode = 3; + string signatureMD5 = 4; + repeated SkuParam skuParamList = 7; + int32 unknown8 = 8; + string installerPackage = 9; + bool unknown10 = 10; + bool unknown11 = 11; + UnkMessage1 unknown15 = 15; + oneof OldSkuPurchase { + string oldSkuPurchaseToken = 16; + string oldSkuPurchaseId = 17; + } + int32 versionCode1 = 18; +} + +message ClientToken { + Info1 info1 = 1; + Info2 info2 = 2; + message Info1 { + bytes unknown2 = 2; + string locale = 7; + int32 unknown8 = 8; + int64 gpVersionCode = 9; + DeviceInfo deviceInfo = 10; + string unknown11 = 11; + repeated string googleAccounts = 19; + } + message Info2 { + string unknown1 = 1; + int32 unknown3 = 3; + repeated int32 unknown4 = 4; + int32 unknown5 = 5; + } + message DeviceInfo { + string unknown3 = 3; + string device = 4; + int32 widthPixels = 5; + int32 heightPixels = 6; + float xdpi = 7; + float ydpi = 8; + string gpPackage = 9; + string gpVersionCode = 10; + string gpVersionName = 11; + EnvInfo envInfo = 12; + string callingPackage = 13; + string marketClientId = 14; + int32 unknown15 = 15; + int32 unknown16 = 16; + string simOperatorName = 17; + string groupIdLevel1 = 18; + int64 subscriberId = 19; + int32 unknown22 = 22; + int64 linkDownstreamBandwidth = 23; + int64 linkUpstreamBandwidth = 24; + bool isActiveNetworkMetered = 25; + int32 densityDpi = 28; + int32 unknown34 = 34; + int64 uptimeMillis = 35; + string timeZoneDisplayName = 36; + int32 unknown40 = 40; + } + + message EnvInfo { + DeviceData deviceData = 1; + OtherInfo otherInfo = 2; + } + + message DeviceData { + int32 unknown1 = 1; + string simOperatorName = 2; + string phoneDeviceId = 3; + string phoneDeviceId1 = 5; + string line1Number = 6; + int64 gsfId = 7; + string device = 9; + string product = 10; + string model = 11; + string manufacturer = 12; + string fingerprint = 13; + string release = 15; + string brand = 21; + string serial = 22; + bool isEmulator = 24; + } + + message OtherInfo { + repeated GPInfo gpInfo = 1; + int32 batteryLevel = 3; + int64 timeZoneOffset = 4; + Location location = 6; + bool isAdbEnabled = 7; + bool installNonMarketApps = 8; + string iso3Language = 9; + repeated string netAddress = 10; + string locale = 11; + string networkOperator = 14; + string simOperator = 15; + string language = 18; + string country = 19; + int32 phoneType = 20; + int64 uptimeMillis = 21; + string timeZoneDisplayName = 22; + int32 googleAccountCount = 23; + bool isUserAMonkey = 24; + bool isAudioWork = 25; + bool hasUsbFeature = 26; + bool isChanging = 27; + int32 brightness = 28; + } + message GPInfo { + string package = 1; + string versionCode = 2; + int64 lastUpdateTime = 3; + int64 firstInstallTime = 4; + string sourceDir = 5; + } + message Location { + double altitude = 1; + double latitude = 2; + double longitude = 3; + float accuracy = 4; + double time = 5; + bool isMock = 6; + } +} + +message UnkMessage1 { + oneof Type { + UnkMessage2 unknown1 = 1; + UnkMessage3 unknown2 = 2; + UnkMessage4 unknown3 = 3; + } +} + +message UnkMessage2 { + int32 unknown1 = 1; +} + +message UnkMessage3 { + int32 unknown1 = 1; +} + +message UnkMessage4 { + int32 unknown1 = 1; +} + +message SkuParam { + string name = 1; + string sv = 2; + bool bv = 3; + int64 i64v = 4; + repeated string svList = 5; +} + +message DocumentInfo { + DocId docId = 1; + int32 unknown2 = 2; + oneof SkuOfferIdToken { + string token3 = 3; + string token14 = 14; + } +} + +message AcknowledgePurchaseRequest { + string purchaseToken = 1; + string developerPayload = 2; +} + +message MultiOfferSkuDetail { + string key = 1; + oneof value { + string sv = 2; + bool bv = 3; + int64 iv = 4; + } +} + +message OfferSku { + string unknown1 = 1; + string unknown2 = 2; +} + +message SkuDetailsExtra { + string version = 1; +} + +message DynamicSku { + string unknown1 = 1; + string unknown2 = 2; + string unknown3 = 3; +} + +message ResponseWrapper { + Payload payload = 1; + ServerCommands commands = 2; + repeated PreFetch preFetch = 3; + ServerMetadata serverMetadata = 5; + bytes serverLogsCookie = 9; +} + +message Payload { + ConsumePurchaseResponse consumePurchaseResponse = 30; + PurchaseHistoryResponse purchaseHistoryResponse = 67; + SkuDetailsResponse skuDetailsResponse = 82; + AcquireResponse acquireResponse = 94; + AcknowledgePurchaseResponse acknowledgePurchaseResponse = 140; +} + +message AcquireResponse { + map screen = 1; + AcquireResult acquireResult = 3; + bytes serverContextToken = 4; + Action action = 8; + bool needClear = 11; +} + +message AcquireResult { + repeated PurchaseItem purchaseItem = 3; + OwnedPurchase ownedPurchase = 8; + string signature = 9; + PurchaseResponse purchaseResponse = 10; +} + +message OwnedPurchase { + repeated PurchaseItem purchaseItem = 1; +} + +message PurchaseResponse { + bool isSuccessful = 1; + ResponseBundle responseBundle = 2; +} + +message ResponseBundle { + repeated BundleItem bundleItem = 1; +} + +message BundleItem { + string key = 1; + oneof value { + string sv = 2; + bool bv = 3; + int64 i64v = 4; + int32 i32v = 5; + BundleStringList sList = 6; + } +} + +message BundleStringList { + repeated string value = 1; +} + +message Screen { + UIInfo uiInfo = 1; + Action action = 5; + UiComponents uiComponents = 175996169; +} + +message UiComponents { + repeated ContentComponent contentComponent1 = 1; + repeated ContentComponent contentComponent2 = 2; + repeated FooterComponent footerComponent = 3; +} + +message ContentComponent { + UIInfo uiInfo = 1; + ViewInfo viewInfo = 2; + string tag = 4; + oneof UiComponent { + ClickableTextView clickableTextView = 20; + ViewGroup viewGroup = 21; + DividerView dividerView = 23; + InstrumentItemView instrumentItemView = 26; + ModuloImageView moduloImageView = 27; + IconTextCombinationView iconTextCombinationView = 37; + ButtonGroupView buttonGroupView = 57; + } +} + +message InstrumentItemView { + ImageView icon = 1; + PlayTextView text = 2; + PlayTextView tips = 3; + ImageView state = 5; + Action action = 6; + PlayTextView extraInfo = 7; +} + +message ModuloImageView { + ImageView imageView = 1; + Action action = 2; +} + +message DividerView { +} + +message ClickableTextView { + PlayTextView playTextView = 1; + Action action = 2; +} + +message IconView { + int32 type = 1; + string text = 2; +} + +message ImageInfo { + oneof ColorFilter{ + int32 value = 1; + int32 valueType = 3; + } + int32 modeType = 2; + int32 scaleType = 5; +} + +message ImageView { + ThumbnailImageView thumbnailImageView = 1; + ViewInfo viewInfo = 2; + ImageInfo imageInfo = 4; + IconView iconView = 5; + oneof AnimationType{ + Animation animation = 6; + } +} + +message Animation { + int32 type = 1; + int32 repeatCount = 2; +} + +message ViewGroup { + ImageView imageView1 = 1; + ImageView imageView2 = 2; + ImageView imageView3 = 3; + ImageView imageView4 = 4; + PlayTextView playTextView = 5; +} + +message ViewInfo { + string tag = 1; + float widthValue = 2; + float heightValue = 3; + float startMargin = 4; + float topMargin = 5; + float endMargin = 6; + float bottomMargin = 7; + float startPadding = 8; + float topPadding = 9; + float endPadding = 10; + float bottomPadding = 11; + int32 backgroundColor = 12; + int32 backgroundColorType = 37; + string contentDescription = 14; + Action action = 20; + repeated int32 gravity = 22; + int32 widthTypedValue = 23; + int32 heightTypedValue = 24; + int32 visibilityType = 29; + int32 borderColorType = 30; + int32 startMarginType = 41; + int32 topMarginType = 42; + int32 endMarginType = 43; + int32 bottomMarginType = 44; + int32 startPaddingType = 45; + int32 topPaddingType = 46; + int32 endPaddingType = 47; + int32 bottomPaddingType = 48; +} + +message ThumbnailImageView { + string lightUrl = 5; + string darkUrl = 28; +} + +message ImageGroup { + repeated ImageView imageView = 1; + ViewInfo viewInfo = 2; +} + +message IconTextCombinationView { + ImageView headerImageView = 1; + PlayTextView playTextView = 2; + repeated SingleLineTextView singleLineTextView = 5; + ViewInfo viewInfo = 6; + PlayTextView badgeTextView = 9; + ImageGroup footerImageGroup = 12; +} + +message SingleLineTextView { + PlayTextView playTextView1 = 1; + PlayTextView playTextView2 = 2; +} + +message Dimension { + int32 unitType = 1; + float unitValue = 2; +} + +message BulletSpan { + Dimension gapWidth = 1; +} + +message TextSpan { + oneof Span { + BulletSpan bulletSpan = 4; + } +} + +message PlayTextView { + oneof TextData { + string text = 1; + int32 textType = 10; + } + bool isHtml = 2; + ViewInfo viewInfo = 3; + TextInfo textInfo = 4; + repeated TextSpan textSpan = 7; +} + +message TextInfo { + oneof TextColor { + int32 textColorValue = 2; + int32 textColorType = 39; + } + int32 maxLines = 6; + repeated int32 gravity = 17; + int32 textAlignmentType = 36; + int32 styleType = 41; +} + +message FooterComponent { + UIInfo uiInfo = 1; + ViewInfo viewInfo = 2; + string tag = 5; + oneof UiComponent { + ButtonGroupView buttonGroupView = 22; + DividerView dividerView = 24; + IconTextCombinationView iconTextCombinationView = 25; + } +} + +message ButtonGroupView { + NewButtonView newButtonView = 6; +} + +message NewButtonView { + ButtonView buttonView = 1; + ButtonView buttonView2 = 2; +} + +message ButtonView { + oneof TextData { + string text = 1; + int32 fixedTextType = 9; + } + Action action = 2; + ViewInfo viewInfo = 4; +} + +message UIInfo { + int32 uiType = 1; + bytes context = 2; + int32 classType = 5; +} + +message NavigateToPage { + string id = 1; + string from = 2; + Action action = 3; +} + +message Action { + TimerAction timerAction = 3; + ShowAction showAction = 4; + bytes actionContext = 7; + NavigateToPage navigateToPage = 8; + ViewClickAction viewClickAction = 10; + OptionalAction optionalAction = 19; + ActionExt actionExt = 148814548; +} + +message OptionalAction { + repeated int32 unknown1 = 1; + Action action1 = 2; + Action action2 = 3; +} + +message ViewClickAction { + UIInfo uiInfo = 2; + Action action = 3; +} + +message TimerAction { + ResponseBundle responseBundle = 1; + bool isSuccessful = 2; + int32 delay = 3; + string url = 4; +} + +message ShowAction { + string screenId = 1; + Action action = 7; + Action action1 = 8; +} + +message ExtAction { + Action action = 1; + DroidGuardMap droidGuardMap = 20; +} + +message IABX { + repeated SkuParam skuParam = 1; +} + +message DroidGuardMap { + map map = 1; + int32 type = 2; +} + +message ActionExt { + ExtAction extAction = 1; +} + +message AcknowledgePurchaseResponse { + PurchaseItem purchaseItem = 1; + FailedResponse failedResponse = 2; +} + +message SkuDetailsResponse { + repeated SkuDetails details = 1; + bool unknown2 = 2; + FailedResponse failedResponse = 4; + repeated SkuInfo skuInfo = 6; +} + +message SkuInfo { + repeated SkuItem skuItem = 1; +} + +message SkuItem { + DocId docId = 1; + string unknown2 = 2; + string token = 3; +} + +message DocId { + string backendDocId = 1; + int32 type = 2; + int32 backend = 3; +} + +message SkuDetails { + string skuDetails = 1; + SkuInfo skuInfo = 4; +} + +message ConsumePurchaseResponse { + PurchaseItem purchaseItem = 1; + FailedResponse failedResponse = 3; +} + +message PurchaseItem { + repeated PurchaseItemData purchaseItemData = 4; +} + +message PurchaseItemData { + DocId docId = 1; + SubsPurchase subsPurchase = 6; + InAppPurchase inAppPurchase = 7; +} + +message InAppPurchase { + string jsonData = 1; + string signature = 2; +} + +message SubsPurchase { + int64 startAt = 1; + int64 expireAt = 2; + string jsonData = 5; + string signature = 6; +} + +message FailedResponse { + int32 statusCode = 1; + string msg = 2; +} + +message PurchaseHistoryResponse { + repeated string productId = 1; + repeated string purchaseJson = 2; + repeated string signature = 3; + string continuationToken = 4; + FailedResponse failedResponse = 5; +} + +message ServerCommands { + bool clearCache = 1; + string displayErrorMessage = 2; + string logErrorStacktrace = 3; +} + +message PreFetch { + string url = 1; + ResponseWrapper response = 2; + string etag = 3; + int64 ttl = 4; + int64 softTtl = 5; +} + +message ServerMetadata { + int64 latencyMillis = 1; +} \ No newline at end of file diff --git a/vending-app/src/main/proto/Timestamp.proto b/vending-app/src/main/proto/Timestamp.proto new file mode 100644 index 00000000..fa98206e --- /dev/null +++ b/vending-app/src/main/proto/Timestamp.proto @@ -0,0 +1,33 @@ +syntax = "proto2"; + +option java_package = "com.android.vending"; +option java_multiple_files = true; + +message TimestampContainer { + optional TimestampContainer1Wrapper container1Wrapper = 3; + optional TimestampContainer2 container2 = 6; +} + +message TimestampContainer1Wrapper { + optional string androidId = 1; + optional TimestampContainer1 container = 2; +} + +message TimestampContainer1 { + optional string timestamp = 1; + optional Timestamp wrapper = 2; +} + +message Timestamp { + optional uint64 seconds = 1; + optional uint32 nanos = 2; +} + +message TimestampContainer2 { + optional TimestampWrapper wrapper = 1; + optional Timestamp timestamp = 2; +} + +message TimestampWrapper { + optional Timestamp timestamp = 1; +} \ No newline at end of file diff --git a/vending-app/src/main/res/drawable-v21/anim_check_mark.xml b/vending-app/src/main/res/drawable-v21/anim_check_mark.xml new file mode 100644 index 00000000..d513ac2c --- /dev/null +++ b/vending-app/src/main/res/drawable-v21/anim_check_mark.xml @@ -0,0 +1,178 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vending-app/src/main/res/drawable-v21/google_play.xml b/vending-app/src/main/res/drawable-v21/google_play.xml new file mode 100644 index 00000000..98e1eda4 --- /dev/null +++ b/vending-app/src/main/res/drawable-v21/google_play.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/vending-app/src/main/res/layout/activiy_confirm_purchase.xml b/vending-app/src/main/res/layout/activiy_confirm_purchase.xml new file mode 100644 index 00000000..9da912c7 --- /dev/null +++ b/vending-app/src/main/res/layout/activiy_confirm_purchase.xml @@ -0,0 +1,13 @@ + + + +