Initial commit
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
/build
|
||||
*/**/msal_auth_config.json
|
||||
@@ -0,0 +1,49 @@
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
id("kotlin-android")
|
||||
id("kotlin-android-extensions")
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk = sdk.versions.compileSdk.get().toInt()
|
||||
|
||||
defaultConfig {
|
||||
minSdk = sdk.versions.minSdk.get().toInt()
|
||||
targetSdk = sdk.versions.targetSdk.get().toInt()
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
consumerProguardFiles("consumer-rules.pro")
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.bundles.kotlin)
|
||||
implementation(libs.androidx.core)
|
||||
implementation(libs.androidx.appcompat)
|
||||
|
||||
implementation(libs.microsoft.identity)
|
||||
implementation(libs.microsoft.graph)
|
||||
implementation(libs.guava)
|
||||
|
||||
implementation(project(":crashreporter"))
|
||||
implementation(project(":preferences"))
|
||||
}
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.kts.kts.kts.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -0,0 +1,41 @@
|
||||
# :ms-services
|
||||
|
||||
⚠️ Depends on non-free external services.
|
||||
|
||||
This module manages API calls to Microsoft APIs and connected Microsoft accounts.
|
||||
|
||||
## Configuration
|
||||
|
||||
This module requires additional configuration in order to work properly. You can skip this step but
|
||||
then Microsoft API related features (e.g. OneDrive search) won't be available.
|
||||
|
||||
In order to use Microsoft Graph APIs, you need to setup a new project in the Microsoft Azure Portal first.
|
||||
|
||||
1. Open the [Microsoft Azure Portal](https://portal.azure.com)
|
||||
1. Create a new project.
|
||||
1. Search for Azure Active Directory
|
||||
1. On the left side, select App registrations
|
||||
1. Add a new registration
|
||||
1. Supported account types: Personal Microsoft Accounts only
|
||||
1. Add an authentication platform
|
||||
1. Go to Authentication
|
||||
1. Add a platform > Android
|
||||
1. Enter the debug package name (de.mm20.launcher2.debug) and the signature hash of your debug key
|
||||
1. You can use the following command to generate the signature hash:
|
||||
`keytool -exportcert -alias androiddebugkey -keystore ~/.android/debug.keystore | openssl sha1 -binary | openssl base64`
|
||||
1. Click Configure > Done
|
||||
1. In the newly created Android section, click on Add URI
|
||||
1. Add package name (de.mm20.launcher2.release) and signature hash of your release key
|
||||
1. Download the client details
|
||||
1. In the debug client row, click on View
|
||||
1. Copy the JSON below MSAL Configuration to ./src/debug/res/raw/msal_auth_config.json
|
||||
1. Repeat the previous step for the release config
|
||||
1. Add the required scopes
|
||||
1. Go to API permissions
|
||||
1. Add a permission
|
||||
1. Select Microsoft Graph > Delegated permissions
|
||||
1. Tick the following scopes:
|
||||
- Files.Read.All
|
||||
- User.Read
|
||||
1. Click Add permissions
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"client_id" : "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
||||
"authorization_user_agent" : "DEFAULT",
|
||||
"account_mode": "SINGLE",
|
||||
"redirect_uri" : "msauth://de.mm20.launcher2.debug/xxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||
"authorities" : [
|
||||
{
|
||||
"type": "AAD",
|
||||
"audience": {
|
||||
"type": "PersonalMicrosoftAccount"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="de.mm20.launcher2.msservices">
|
||||
|
||||
<application>
|
||||
<activity
|
||||
android:name="com.microsoft.identity.client.BrowserTabActivity"
|
||||
android:exported="true">
|
||||
<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:host="${applicationId}"
|
||||
android:scheme="msauth" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -0,0 +1,39 @@
|
||||
package de.mm20.launcher2.msservices
|
||||
|
||||
import com.microsoft.graph.extensions.DriveItem as MSDriveItem
|
||||
|
||||
data class DriveItem(
|
||||
val id : String,
|
||||
val label : String,
|
||||
val mimeType : String,
|
||||
val size: Long,
|
||||
val isDirectory : Boolean,
|
||||
val webUrl: String,
|
||||
val meta: DriveItemMeta
|
||||
) {
|
||||
companion object {
|
||||
fun fromApiDriveItem(driveItem: MSDriveItem) : DriveItem? {
|
||||
return DriveItem(
|
||||
id = driveItem.id ?: return null,
|
||||
label = driveItem.name ?: return null,
|
||||
mimeType = driveItem.file?.mimeType ?: "inode/directory",
|
||||
size = driveItem.size ?: 0,
|
||||
isDirectory = driveItem.file == null,
|
||||
webUrl = driveItem.webUrl ?: return null,
|
||||
meta = DriveItemMeta(
|
||||
owner = driveItem.shared?.owner?.user?.displayName,
|
||||
createdBy = driveItem.createdBy?.user?.displayName,
|
||||
width = driveItem.image?.width ?: driveItem.video?.width,
|
||||
height = driveItem.image?.height ?: driveItem.video?.height
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class DriveItemMeta(
|
||||
val owner: String?,
|
||||
val createdBy: String?,
|
||||
val width: Int?,
|
||||
val height: Int?
|
||||
)
|
||||
@@ -0,0 +1,195 @@
|
||||
package de.mm20.launcher2.msservices
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.core.content.edit
|
||||
import com.microsoft.graph.core.ClientException
|
||||
import com.microsoft.graph.core.DefaultClientConfig
|
||||
import com.microsoft.graph.extensions.GraphServiceClient
|
||||
import com.microsoft.graph.extensions.IGraphServiceClient
|
||||
import com.microsoft.graph.http.GraphServiceException
|
||||
import com.microsoft.identity.client.AuthenticationCallback
|
||||
import com.microsoft.identity.client.IAuthenticationResult
|
||||
import com.microsoft.identity.client.ISingleAccountPublicClientApplication
|
||||
import com.microsoft.identity.client.PublicClientApplication
|
||||
import com.microsoft.identity.client.exception.MsalException
|
||||
import de.mm20.launcher2.crashreporter.CrashReporter
|
||||
import de.mm20.launcher2.preferences.LauncherPreferences
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.net.URLEncoder
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
class MicrosoftGraphApiHelper(val context: Context) {
|
||||
|
||||
private var accessToken: String? = null
|
||||
private val client: IGraphServiceClient
|
||||
private var clientApplication: ISingleAccountPublicClientApplication? = null
|
||||
|
||||
init {
|
||||
client = GraphServiceClient
|
||||
.Builder()
|
||||
.fromConfig(DefaultClientConfig.createWithAuthenticationProvider {
|
||||
it.addHeader("Authorization", "Bearer $accessToken")
|
||||
})
|
||||
.buildClient()
|
||||
}
|
||||
|
||||
private suspend fun getClientApplication(): ISingleAccountPublicClientApplication? {
|
||||
val resId = getConfigResId()
|
||||
if (resId == 0) return null
|
||||
if (clientApplication == null) {
|
||||
clientApplication = withContext(Dispatchers.IO) {
|
||||
PublicClientApplication.createSingleAccountPublicClientApplication(
|
||||
context.applicationContext,
|
||||
resId
|
||||
)
|
||||
}
|
||||
}
|
||||
return clientApplication!!
|
||||
}
|
||||
|
||||
private suspend fun acquireAccessToken(): Boolean {
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val application = getClientApplication() ?: return@withContext null
|
||||
val authority = application.configuration.defaultAuthority.authorityURL.toString()
|
||||
application.acquireTokenSilent(SCOPES, authority)
|
||||
} catch (e: MsalException) {
|
||||
CrashReporter.logException(e)
|
||||
logout()
|
||||
null
|
||||
} catch (e: ClientException) {
|
||||
CrashReporter.logException(e)
|
||||
null
|
||||
}
|
||||
}
|
||||
accessToken = result?.accessToken
|
||||
return result != null
|
||||
}
|
||||
|
||||
suspend fun login(context: Activity) {
|
||||
val clientApplication = getClientApplication() ?: return
|
||||
suspendCoroutine<IAuthenticationResult?> {
|
||||
clientApplication.signIn(context, "", SCOPES, object : AuthenticationCallback {
|
||||
override fun onSuccess(authenticationResult: IAuthenticationResult?) {
|
||||
accessToken = authenticationResult?.accessToken
|
||||
LauncherPreferences.instance.searchOneDrive = true
|
||||
it.resume(authenticationResult)
|
||||
}
|
||||
|
||||
override fun onCancel() {
|
||||
it.resume(null)
|
||||
}
|
||||
|
||||
override fun onError(exception: MsalException?) {
|
||||
if (exception != null) Log.e("MM20", exception.stackTraceToString())
|
||||
it.resume(null)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
loadAccountName()
|
||||
}
|
||||
|
||||
suspend fun logout() {
|
||||
accessToken = null
|
||||
LauncherPreferences.instance.searchOneDrive = false
|
||||
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit {
|
||||
putString(PREF_ACCOUNT_NAME, null)
|
||||
}
|
||||
withContext(Dispatchers.IO) { getClientApplication()?.signOut() }
|
||||
}
|
||||
|
||||
suspend fun getUser(): MsUser? {
|
||||
val name = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).getString(
|
||||
PREF_ACCOUNT_NAME,
|
||||
null
|
||||
) ?: loadAccountName()
|
||||
|
||||
|
||||
return name?.let {
|
||||
MsUser(name = it)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadAccountName(): String? {
|
||||
if (!isLoggedIn()) return null
|
||||
if (!acquireAccessToken()) return null
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val user = client.me.buildRequest().get() ?: return@withContext null
|
||||
val name = user.displayName ?: user.mail ?: "Microsoft User"
|
||||
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit {
|
||||
putString(PREF_ACCOUNT_NAME, name)
|
||||
}
|
||||
return@withContext name
|
||||
} catch (e: GraphServiceException) {
|
||||
CrashReporter.logException(e)
|
||||
logout()
|
||||
} catch (e: ClientException) {
|
||||
CrashReporter.logException(e)
|
||||
}
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun isLoggedIn(): Boolean {
|
||||
return withContext(Dispatchers.IO) {
|
||||
getClientApplication()?.currentAccount?.currentAccount != null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
suspend fun queryOneDriveFiles(query: String): List<DriveItem>? {
|
||||
if (!acquireAccessToken()) return null
|
||||
return try {
|
||||
withContext(Dispatchers.IO) {
|
||||
client.me.drive.getSearch(
|
||||
URLEncoder.encode(query.replace("'", "''"), "utf8")
|
||||
)
|
||||
.buildRequest()
|
||||
.select("id,name,file,size,video,image,webUrl,shared,createdBy")
|
||||
.top(10)
|
||||
.get()
|
||||
?.currentPage
|
||||
?.mapNotNull { DriveItem.fromApiDriveItem(it) }
|
||||
}
|
||||
} catch (e: GraphServiceException) {
|
||||
CrashReporter.logException(e)
|
||||
null
|
||||
} catch (e: ClientException) {
|
||||
CrashReporter.logException(e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun isAvailable(): Boolean {
|
||||
return getConfigResId() != 0
|
||||
}
|
||||
|
||||
private fun getConfigResId(): Int {
|
||||
return context.resources.getIdentifier("msal_auth_config", "raw", context.packageName)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private lateinit var instance: MicrosoftGraphApiHelper
|
||||
|
||||
fun getInstance(context: Context): MicrosoftGraphApiHelper {
|
||||
if (!Companion::instance.isInitialized) instance =
|
||||
MicrosoftGraphApiHelper(context.applicationContext)
|
||||
return instance
|
||||
}
|
||||
|
||||
private val SCOPES = arrayOf(
|
||||
"User.Read",
|
||||
"Files.Read.All"
|
||||
)
|
||||
|
||||
const val PREFS = "ms-account"
|
||||
const val PREF_ACCOUNT_NAME = "name"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package de.mm20.launcher2.msservices
|
||||
|
||||
data class MsUser(
|
||||
val name: String
|
||||
)
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"client_id" : "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
||||
"authorization_user_agent" : "DEFAULT",
|
||||
"account_mode": "SINGLE",
|
||||
"redirect_uri" : "msauth://de.mm20.launcher2.release/xxxxxxxxxxxxxxxxxx",
|
||||
"authorities" : [
|
||||
{
|
||||
"type": "AAD",
|
||||
"audience": {
|
||||
"type": "PersonalMicrosoftAccount"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user