Skip to content
GitLab
Menu
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
e
os
AccountManager
Commits
d9dba28d
Commit
d9dba28d
authored
Jul 01, 2022
by
narinder Rana
Browse files
Merge branch '516-impl-google-account-setup' into '55-Update_accountManager_with_upstream'
impl goggle account setup See merge request
!52
parents
5ff8ec5a
aef7d48c
Changes
17
Expand all
Hide whitespace changes
Inline
Side-by-side
app/build.gradle
View file @
d9dba28d
...
...
@@ -26,6 +26,10 @@ android {
buildConfigField
"String"
,
"userAgent"
,
"\"DAVx5\""
manifestPlaceholders
=
[
'appAuthRedirectScheme'
:
'com.googleusercontent.apps.628867657910-7ade6gut5rhabdgjq6k4rln9i1u9ppca'
]
testInstrumentationRunner
"at.bitfire.davdroid.CustomTestRunner"
kapt
{
...
...
@@ -151,7 +155,8 @@ dependencies {
implementation
"org.apache.commons:commons-lang3:${versions.commonsLang}"
//noinspection GradleDependency
implementation
"org.apache.commons:commons-text:${versions.commonsText}"
//google auth
implementation
'net.openid:appauth:0.7.0'
// for tests
androidTestImplementation
"com.google.dagger:hilt-android-testing:${versions.hilt}"
kaptAndroidTest
"com.google.dagger:hilt-android-compiler:${versions.hilt}"
...
...
app/src/main/AndroidManifest.xml
View file @
d9dba28d
...
...
@@ -280,6 +280,71 @@
android:resource=
"@xml/debug_paths"
/>
</provider>
<!-- account type "Google" -->
<service
android:name=
".syncadapter.GoogleAccountAuthenticatorService"
android:exported=
"false"
>
<intent-filter>
<action
android:name=
"android.accounts.AccountAuthenticator"
/>
</intent-filter>
<meta-data
android:name=
"android.accounts.AccountAuthenticator"
android:resource=
"@xml/google_account_authenticator"
/>
</service>
<!-- account type "eelo" -->
<service
android:name=
".syncadapter.EeloAccountAuthenticatorService"
android:exported=
"false"
>
<intent-filter>
<action
android:name=
"android.accounts.AccountAuthenticator"
/>
</intent-filter>
<meta-data
android:name=
"android.accounts.AccountAuthenticator"
android:resource=
"@xml/eelo_account_authenticator"
/>
</service>
<!-- Callback from authentication screen -->
<activity
android:name=
"net.openid.appauth.RedirectUriReceiverActivity"
>
<!--
filter which captures custom scheme based redirects, which are used by the intermediary page when
the browser used for authentication cannot directly invoke this app. The source for the redirect
page can be found in web/oauth2redirect.html
-->
<intent-filter>
<action
android:name=
"android.intent.action.VIEW"
/>
<category
android:name=
"android.intent.category.DEFAULT"
/>
<category
android:name=
"android.intent.category.BROWSABLE"
/>
<data
android:scheme=
"net.openid.appauth.demo"
/>
<data
android:scheme=
"com.googleusercontent.apps.100496780587-pbiu5eudcjm6cge2phduc6mt8mgbsmsr"
/>
</intent-filter>
<!--
for up-to-date Chrome browsers we can intercept the OAuth2 callback directly, if a secure assertion
is found on the site matching this app:
https://appauth.demo-app.io/.well-known/assetlinks.json
The source for this file can also be found in
web/.well-known/assetlinks.json
-->
<intent-filter
android:autoVerify=
"true"
>
<action
android:name=
"android.intent.action.VIEW"
/>
<category
android:name=
"android.intent.category.DEFAULT"
/>
<category
android:name=
"android.intent.category.BROWSABLE"
/>
<data
android:host=
"appauth.demo-app.io"
android:path=
"/oauth2redirect"
android:scheme=
"https"
/>
</intent-filter>
</activity>
</application>
<!-- package visiblity – which apps do we need to see? -->
...
...
app/src/main/java/at/bitfire/davdroid/authorization/IdentityProvider.java
0 → 100644
View file @
d9dba28d
package
at.bitfire.davdroid.authorization
;
/*
* Copyright 2015 The AppAuth Authors. All Rights Reserved.
*
* 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.
*/
import
android.content.Context
;
import
android.content.res.Resources
;
import
android.net.Uri
;
import
androidx.annotation.NonNull
;
import
androidx.annotation.Nullable
;
import
androidx.annotation.StringRes
;
import
foundation.e.accountmanager.R
;
import
net.openid.appauth.AuthorizationServiceConfiguration
;
import
net.openid.appauth.AuthorizationServiceConfiguration.RetrieveConfigurationCallback
;
import
java.util.ArrayList
;
import
java.util.Arrays
;
import
java.util.List
;
/**
* An abstraction of identity providers, containing all necessary info for the demo app.
*/
public
class
IdentityProvider
{
/**
* Value used to indicate that a configured property is not specified or required.
*/
public
static
final
int
NOT_SPECIFIED
=
-
1
;
public
static
final
IdentityProvider
GOOGLE
=
new
IdentityProvider
(
"Google"
,
R
.
string
.
google_discovery_uri
,
NOT_SPECIFIED
,
// auth endpoint is discovered
NOT_SPECIFIED
,
// token endpoint is discovered
R
.
string
.
google_client_id
,
NOT_SPECIFIED
,
// client secret is not required for Google
R
.
string
.
google_auth_redirect_uri
,
R
.
string
.
google_scope_string
,
R
.
string
.
google_name
);
public
static
final
List
<
IdentityProvider
>
PROVIDERS
=
Arrays
.
asList
(
GOOGLE
);
public
static
List
<
IdentityProvider
>
getEnabledProviders
(
Context
context
)
{
ArrayList
<
IdentityProvider
>
providers
=
new
ArrayList
<>();
for
(
IdentityProvider
provider
:
PROVIDERS
)
{
provider
.
readConfiguration
(
context
);
providers
.
add
(
provider
);
}
return
providers
;
}
@NonNull
public
final
String
name
;
@StringRes
public
final
int
buttonContentDescriptionRes
;
@StringRes
private
final
int
mDiscoveryEndpointRes
;
@StringRes
private
final
int
mAuthEndpointRes
;
@StringRes
private
final
int
mTokenEndpointRes
;
@StringRes
private
final
int
mClientIdRes
;
@StringRes
private
final
int
mClientSecretRes
;
@StringRes
private
final
int
mRedirectUriRes
;
@StringRes
private
final
int
mScopeRes
;
private
boolean
mConfigurationRead
=
false
;
private
Uri
mDiscoveryEndpoint
;
private
Uri
mAuthEndpoint
;
private
Uri
mTokenEndpoint
;
private
String
mClientId
;
private
String
mClientSecret
;
private
Uri
mRedirectUri
;
private
String
mScope
;
IdentityProvider
(
@NonNull
String
name
,
@StringRes
int
discoveryEndpointRes
,
@StringRes
int
authEndpointRes
,
@StringRes
int
tokenEndpointRes
,
@StringRes
int
clientIdRes
,
@StringRes
int
clientSecretRes
,
@StringRes
int
redirectUriRes
,
@StringRes
int
scopeRes
,
@StringRes
int
buttonContentDescriptionRes
)
{
if
(!
isSpecified
(
discoveryEndpointRes
)
&&
!
isSpecified
(
authEndpointRes
)
&&
!
isSpecified
(
tokenEndpointRes
))
{
throw
new
IllegalArgumentException
(
"the discovery endpoint or the auth and token endpoints must be specified"
);
}
this
.
name
=
name
;
this
.
mDiscoveryEndpointRes
=
discoveryEndpointRes
;
this
.
mAuthEndpointRes
=
authEndpointRes
;
this
.
mTokenEndpointRes
=
tokenEndpointRes
;
this
.
mClientIdRes
=
checkSpecified
(
clientIdRes
,
"clientIdRes"
);
this
.
mClientSecretRes
=
clientSecretRes
;
this
.
mRedirectUriRes
=
checkSpecified
(
redirectUriRes
,
"redirectUriRes"
);
this
.
mScopeRes
=
checkSpecified
(
scopeRes
,
"scopeRes"
);
this
.
buttonContentDescriptionRes
=
checkSpecified
(
buttonContentDescriptionRes
,
"buttonContentDescriptionRes"
);
}
/**
* This must be called before any of the getters will function.
*/
public
void
readConfiguration
(
Context
context
)
{
if
(
mConfigurationRead
)
{
return
;
}
Resources
res
=
context
.
getResources
();
mDiscoveryEndpoint
=
isSpecified
(
mDiscoveryEndpointRes
)
?
getUriResource
(
res
,
mDiscoveryEndpointRes
,
"discoveryEndpointRes"
)
:
null
;
mAuthEndpoint
=
isSpecified
(
mAuthEndpointRes
)
?
getUriResource
(
res
,
mAuthEndpointRes
,
"authEndpointRes"
)
:
null
;
mTokenEndpoint
=
isSpecified
(
mTokenEndpointRes
)
?
getUriResource
(
res
,
mTokenEndpointRes
,
"tokenEndpointRes"
)
:
null
;
mClientId
=
res
.
getString
(
mClientIdRes
);
mClientSecret
=
isSpecified
(
mClientSecretRes
)
?
res
.
getString
(
mClientSecretRes
)
:
null
;
mRedirectUri
=
getUriResource
(
res
,
mRedirectUriRes
,
"mRedirectUriRes"
);
mScope
=
res
.
getString
(
mScopeRes
);
mConfigurationRead
=
true
;
}
private
void
checkConfigurationRead
()
{
if
(!
mConfigurationRead
)
{
throw
new
IllegalStateException
(
"Configuration not read"
);
}
}
@Nullable
public
Uri
getDiscoveryEndpoint
()
{
checkConfigurationRead
();
return
mDiscoveryEndpoint
;
}
@Nullable
public
Uri
getAuthEndpoint
()
{
checkConfigurationRead
();
return
mAuthEndpoint
;
}
@Nullable
public
Uri
getTokenEndpoint
()
{
checkConfigurationRead
();
return
mTokenEndpoint
;
}
@NonNull
public
String
getClientId
()
{
checkConfigurationRead
();
return
mClientId
;
}
@Nullable
public
String
getClientSecret
()
{
checkConfigurationRead
();
return
mClientSecret
;
}
@NonNull
public
Uri
getRedirectUri
()
{
checkConfigurationRead
();
return
mRedirectUri
;
}
@NonNull
public
String
getScope
()
{
checkConfigurationRead
();
return
mScope
;
}
public
void
retrieveConfig
(
Context
context
,
RetrieveConfigurationCallback
callback
)
{
readConfiguration
(
context
);
if
(
getDiscoveryEndpoint
()
!=
null
)
{
AuthorizationServiceConfiguration
.
fetchFromUrl
(
mDiscoveryEndpoint
,
callback
);
}
else
{
AuthorizationServiceConfiguration
config
=
new
AuthorizationServiceConfiguration
(
mAuthEndpoint
,
mTokenEndpoint
,
null
);
callback
.
onFetchConfigurationCompleted
(
config
,
null
);
}
}
private
static
boolean
isSpecified
(
int
value
)
{
return
value
!=
NOT_SPECIFIED
;
}
private
static
int
checkSpecified
(
int
value
,
String
valueName
)
{
if
(
value
==
NOT_SPECIFIED
)
{
throw
new
IllegalArgumentException
(
valueName
+
" must be specified"
);
}
return
value
;
}
private
static
Uri
getUriResource
(
Resources
res
,
@StringRes
int
resId
,
String
resName
)
{
return
Uri
.
parse
(
res
.
getString
(
resId
));
}
}
app/src/main/java/at/bitfire/davdroid/db/Credentials.kt
View file @
d9dba28d
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
/*
* Copyright © Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package
at.bitfire.davdroid.db
package
foundation.e.accountmanager.model
data class
Credentials
(
val
userName
:
String
?
=
null
,
val
password
:
String
?
=
null
,
val
certificateAlias
:
String
?
=
null
class
Credentials
(
val
userName
:
String
?
=
null
,
val
password
:
String
?
=
null
,
val
accessToken
:
String
?
=
null
,
val
refreshToken
:
String
?
=
null
,
val
certificateAlias
:
String
?
=
null
)
{
override
fun
toString
():
String
{
val
maskedPassword
=
"*****"
.
takeIf
{
password
!=
null
}
return
"Credentials(userName=$userName, password=$maskedPassword, certificateAlias=$certificateAlias)"
enum
class
Type
{
UsernamePassword
,
OAuth
,
ClientCertificate
}
}
\ No newline at end of file
val
type
:
Type
init
{
type
=
when
{
!
certificateAlias
.
isNullOrEmpty
()
->
Type
.
ClientCertificate
!
userName
.
isNullOrEmpty
()
&&
!
accessToken
.
isNullOrEmpty
()
&&
!
refreshToken
.
isNullOrEmpty
()
->
Type
.
OAuth
!
userName
.
isNullOrEmpty
()
&&
!
password
.
isNullOrEmpty
()
->
Type
.
UsernamePassword
else
->
throw
IllegalArgumentException
(
"Invalid account type/credentials"
)
}
}
override
fun
toString
()
=
"Credentials(type=$type, userName=$userName, certificateAlias=$certificateAlias)"
}
app/src/main/java/at/bitfire/davdroid/settings/AccountSettings.kt
View file @
d9dba28d
This diff is collapsed.
Click to expand it.
app/src/main/java/at/bitfire/davdroid/syncadapter/EeloAccountAuthenticatorService.kt
0 → 100644
View file @
d9dba28d
/*
* Copyright © Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package
at.bitfire.davdroid.syncadapter
import
android.accounts.*
import
android.app.Service
import
android.content.Context
import
android.content.Intent
import
android.database.DatabaseUtils
import
android.os.Bundle
import
androidx.annotation.WorkerThread
import
at.bitfire.davdroid.db.AppDatabase
import
at.bitfire.davdroid.log.Logger
import
at.bitfire.davdroid.resource.LocalAddressBook
import
at.bitfire.davdroid.ui.setup.LoginActivity
import
foundation.e.accountmanager.R
import
java.util.*
import
java.util.logging.Level
import
kotlin.concurrent.thread
/**
* Account authenticator for the eelo account type.
*
* Gets started when an eelo account is removed, too, so it also watches for account removals
* and contains the corresponding cleanup code.
*/
class
EeloAccountAuthenticatorService
:
Service
(),
OnAccountsUpdateListener
{
companion
object
{
fun
cleanupAccounts
(
context
:
Context
)
{
Logger
.
log
.
info
(
"Cleaning up orphaned accounts"
)
val
accountManager
=
AccountManager
.
get
(
context
)
val
accountNames
=
accountManager
.
getAccountsByType
(
context
.
getString
(
R
.
string
.
account_type
))
.
map
{
it
.
name
}
// delete orphaned address book accounts
accountManager
.
getAccountsByType
(
context
.
getString
(
R
.
string
.
account_type_address_book
))
.
map
{
LocalAddressBook
(
context
,
it
,
null
)
}
.
forEach
{
try
{
if
(!
accountNames
.
contains
(
it
.
mainAccount
.
name
))
it
.
delete
()
}
catch
(
e
:
Exception
)
{
Logger
.
log
.
log
(
Level
.
SEVERE
,
"Couldn't delete address book account"
,
e
)
}
}
// delete orphaned services in DB
val
db
=
AppDatabase
.
getInstance
(
context
)
val
serviceDao
=
db
.
serviceDao
()
if
(
accountNames
.
isEmpty
())
serviceDao
.
deleteAll
()
else
serviceDao
.
deleteExceptAccounts
(
accountNames
.
toTypedArray
())
}
}
private
lateinit
var
accountManager
:
AccountManager
private
lateinit
var
accountAuthenticator
:
AccountAuthenticator
override
fun
onCreate
()
{
accountManager
=
AccountManager
.
get
(
this
)
accountManager
.
addOnAccountsUpdatedListener
(
this
,
null
,
true
)
accountAuthenticator
=
AccountAuthenticator
(
this
)
}
override
fun
onDestroy
()
{
super
.
onDestroy
()
accountManager
.
removeOnAccountsUpdatedListener
(
this
)
}
override
fun
onBind
(
intent
:
Intent
?)
=
accountAuthenticator
.
iBinder
.
takeIf
{
intent
?.
action
==
android
.
accounts
.
AccountManager
.
ACTION_AUTHENTICATOR_INTENT
}
override
fun
onAccountsUpdated
(
accounts
:
Array
<
out
Account
>?)
{
thread
{
cleanupAccounts
(
this
)
}
}
private
class
AccountAuthenticator
(
val
context
:
Context
)
:
AbstractAccountAuthenticator
(
context
)
{
override
fun
addAccount
(
response
:
AccountAuthenticatorResponse
?,
accountType
:
String
?,
authTokenType
:
String
?,
requiredFeatures
:
Array
<
String
>?,
options
:
Bundle
?):
Bundle
{
val
intent
=
Intent
(
context
,
LoginActivity
::
class
.
java
)
intent
.
putExtra
(
AccountManager
.
KEY_ACCOUNT_AUTHENTICATOR_RESPONSE
,
response
)
intent
.
putExtra
(
LoginActivity
.
SETUP_ACCOUNT_PROVIDER_TYPE
,
LoginActivity
.
ACCOUNT_PROVIDER_EELO
)
val
bundle
=
Bundle
(
1
)
bundle
.
putParcelable
(
AccountManager
.
KEY_INTENT
,
intent
)
return
bundle
}
override
fun
editProperties
(
response
:
AccountAuthenticatorResponse
?,
accountType
:
String
?)
=
null
override
fun
getAuthTokenLabel
(
p0
:
String
?)
=
null
override
fun
confirmCredentials
(
p0
:
AccountAuthenticatorResponse
?,
p1
:
Account
?,
p2
:
Bundle
?)
=
null
override
fun
updateCredentials
(
p0
:
AccountAuthenticatorResponse
?,
p1
:
Account
?,
p2
:
String
?,
p3
:
Bundle
?)
=
null
override
fun
getAuthToken
(
p0
:
AccountAuthenticatorResponse
?,
p1
:
Account
?,
p2
:
String
?,
p3
:
Bundle
?)
=
null
override
fun
hasFeatures
(
p0
:
AccountAuthenticatorResponse
?,
p1
:
Account
?,
p2
:
Array
<
out
String
>?)
=
null
}
}
app/src/main/java/at/bitfire/davdroid/syncadapter/GoogleAccountAuthenticatorService.kt
0 → 100644
View file @
d9dba28d
/*
* Copyright © Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
import
android.accounts.*
import
android.app.Service
import
android.content.Context
import
android.content.Intent
import
android.database.DatabaseUtils
import
android.os.Bundle
import
androidx.annotation.WorkerThread
import
at.bitfire.davdroid.db.AppDatabase
import
at.bitfire.davdroid.log.Logger
import
at.bitfire.davdroid.resource.LocalAddressBook
import
at.bitfire.davdroid.ui.setup.LoginActivity
import
foundation.e.accountmanager.R
import
java.util.*
import
java.util.logging.Level
import
kotlin.concurrent.thread
/**
* Account authenticator for the Google account type.
*
* Gets started when a Google account is removed, too, so it also watches for account removals
* and contains the corresponding cleanup code.
*/
class
GoogleAccountAuthenticatorService
:
Service
(),
OnAccountsUpdateListener
{
companion
object
{
fun
cleanupAccounts
(
context
:
Context
)
{
Logger
.
log
.
info
(
"Cleaning up orphaned accounts"
)
val
accountManager
=
AccountManager
.
get
(
context
)
val
accountNames
=
accountManager
.
getAccountsByType
(
context
.
getString
(
R
.
string
.
account_type
))
.
map
{
it
.
name
}
// delete orphaned address book accounts
accountManager
.
getAccountsByType
(
context
.
getString
(
R
.
string
.
account_type_address_book
))
.
map
{
LocalAddressBook
(
context
,
it
,
null
)
}
.
forEach
{
try
{
if
(!
accountNames
.
contains
(
it
.
mainAccount
.
name
))
it
.
delete
()
}
catch
(
e
:
Exception
)
{
Logger
.
log
.
log
(
Level
.
SEVERE
,
"Couldn't delete address book account"
,
e
)
}
}
// delete orphaned services in DB
val
db
=
AppDatabase
.
getInstance
(
context
)