Initial commit

This commit is contained in:
MM20
2021-09-18 23:37:52 +02:00
commit 749e4e3073
938 changed files with 50475 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
/build
*/**/msal_auth_config.json
+49
View File
@@ -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"))
}
View File
+21
View File
@@ -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
+41
View File
@@ -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"
}
}
]
}
+20
View File
@@ -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"
}
}
]
}