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
*/**/g_services.json
+55
View File
@@ -0,0 +1,55 @@
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.androidx.browser)
implementation(libs.bundles.androidx.lifecycle)
implementation(libs.google.auth)
implementation(libs.google.apiclient)
implementation(libs.google.drive)
implementation(libs.google.oauth2)
implementation(libs.bundles.materialdialogs)
implementation(project(":i18n"))
implementation(project(":crashreporter"))
}
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.
#
# 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
+30
View File
@@ -0,0 +1,30 @@
# :g-services
⚠️ Depends on non-free external services.
This module manages API calls to Google APIs and connected Google accounts.
## Configuration
This module requires additional configuration in order to work properly. You can skip this step but
then Google API related features (e.g. Google Drive search) won't be available.
In order to use Google APIs, you need to setup a new project in the Google Cloud Console first.
1. Open the [Google Cloud Console](https://console.cloud.google.com)
1. Create a new project.
1. Enable the Drive API:
1. Go to APIs & Services > Library and search for the Google Drive API.
1. Enable this API for your project.
1. Create a new Oauth 2.0 client (you need to do this twice, for debug builds and for release builds)
1. Go to APIs & Services > Credentials
1. Click on Create Credentials > OAuth client ID
1. Choose application type Android
1. Enter the package name (de.mm20.launcher2.debug for debug builds or de.mm20.launcher2.release for release builds)
1. Enter the SHA-1 certificate fingerprint of your APK signing key
1. Click create
1. Download the client config file (repeat this step for both the debug and the release client)
1. On the APIs & Services > Credentials page, find your OAuth client in the list under OAuth 2.0 Client IDs.
1. Click the download icon to download a client_config.json
1. Place this file under src/debug/res/raw/g_services.json or src/release/res/raw/g_services.json
@@ -0,0 +1 @@
{"installed":{"client_id":"xxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com","project_id":"xxxxx-xxxxxxxxxxxxx","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","redirect_uris":["urn:ietf:wg:oauth:2.0:oob","http://localhost"]}}
+21
View File
@@ -0,0 +1,21 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="de.mm20.launcher2.gservices">
<application>
<activity
android:name="de.mm20.launcher2.gservices.GoogleAuthRedirectActivity"
android:theme="@style/GoogleSigninTheme"
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:path="/google-auth-redirect"
android:scheme="${applicationId}" />
</intent-filter>
</activity>
</application>
</manifest>
@@ -0,0 +1,40 @@
package de.mm20.launcher2.gservices
import com.google.api.services.drive.model.File
import java.util.*
data class DriveFile(
val fileId : String,
val label: String,
val size: Long,
val mimeType : String,
val isDirectory : Boolean,
val directoryColor: String?,
val viewUri: String,
val metadata: DriveFileMeta
) {
companion object {
fun fromApiDriveFile(file: File): DriveFile {
return DriveFile(
fileId = file.id,
label = file.name,
size = file.getSize() ?: 0,
isDirectory = file.mimeType == "application/vnd.google-apps.folder",
mimeType = file.mimeType,
metadata = DriveFileMeta(
owners = file.owners?.map { it.displayName ?: it.emailAddress ?: "" } ?: emptyList(),
width = file.imageMediaMetadata?.width ?: file.videoMediaMetadata?.width,
height = file.imageMediaMetadata?.height ?: file.videoMediaMetadata?.height
),
directoryColor = file.folderColorRgb?.toLowerCase(Locale.ROOT),
viewUri = file.webViewLink ?: ""
)
}
}
}
data class DriveFileMeta(
val owners : List<String>,
val width: Int?,
val height: Int?
)
@@ -0,0 +1,5 @@
package de.mm20.launcher2.gservices
data class GoogleAccount(
val name: String
)
@@ -0,0 +1,222 @@
package de.mm20.launcher2.gservices
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.browser.customtabs.*
import androidx.core.content.edit
import com.google.api.client.auth.oauth2.Credential
import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow
import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets
import com.google.api.client.http.HttpRequestInitializer
import com.google.api.client.http.javanet.NetHttpTransport
import com.google.api.client.json.gson.GsonFactory
import com.google.api.client.util.store.FileDataStoreFactory
import com.google.api.services.drive.Drive
import com.google.api.services.oauth2.Oauth2
import de.mm20.launcher2.crashreporter.CrashReporter
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.IOException
class GoogleApiHelper private constructor(private val context: Context) {
val transport by lazy {
NetHttpTransport()
}
suspend fun queryGDriveFiles(query: String): List<DriveFile> {
val requestInitializer = getRequestInitializer() ?: return emptyList()
val jsonFactory = GsonFactory.getDefaultInstance()
return withContext(Dispatchers.IO) {
try {
val drive =
Drive.Builder(transport, jsonFactory, requestInitializer).build()
val request = drive.files().list()
request.q = "name contains '${query.replace("'", "")}'"
request.pageSize = 10
request.fields =
"files(id, webViewLink, size, name, mimeType, owners, imageMediaMetadata, videoMediaMetadata, folderColorRgb)"
request.corpora = "user"
val response = request.execute()
val files = response.files ?: return@withContext emptyList()
files.map { DriveFile.fromApiDriveFile(it) }
} catch (e: IOException) {
emptyList()
} catch (e: Error) {
emptyList()
}
}
}
private suspend fun getCredential(): Credential? {
val authFlow = getAuthFlow() ?: return null
return withContext(Dispatchers.IO) {
val credential: Credential? = authFlow.loadCredential(USER_ID)
if ((credential?.expiresInSeconds ?: 0) < 5 * 60) {
try {
if (credential?.refreshToken() == false) return@withContext null
} catch (e: IOException) {
CrashReporter.logException(e)
}
}
return@withContext credential
}
}
private suspend fun getRequestInitializer(): HttpRequestInitializer? {
val credential = getCredential() ?: return null
return HttpRequestInitializer { request ->
credential.initialize(request)
request?.connectTimeout = 5000
request?.readTimeout = 10000
}
}
suspend fun getAccount(): GoogleAccount? {
val name = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).getString(
PREF_ACCOUNT_NAME,
null
) ?: loadAccountName()
return name?.let {
GoogleAccount(name = it)
}
}
fun isAvailable(): Boolean {
return getConfigResId() != 0
}
private fun getConfigResId(): Int {
return context.resources.getIdentifier("g_services", "raw", context.packageName)
}
private fun getAuthFlow(): GoogleAuthorizationCodeFlow? {
val configResId = getConfigResId()
if (configResId == 0) return null
val jsonFactory = GsonFactory.getDefaultInstance()
return GoogleAuthorizationCodeFlow.Builder(
NetHttpTransport(),
jsonFactory,
GoogleClientSecrets.load(
jsonFactory,
context.resources.openRawResource(configResId).reader()
),
SCOPES
)
.setCredentialDataStore(
FileDataStoreFactory(context.filesDir).getDataStore(
"google_signin"
)
)
.build()
}
fun login(activity: Activity) {
val authFlow = getAuthFlow() ?: return
val url = authFlow
.newAuthorizationUrl()
.setRedirectUri(getRedirectUri())
.toString()
val themeColor = 0xFF4285f4.toInt()
val customTabsIntent = CustomTabsIntent
.Builder()
.setDefaultColorSchemeParams(
CustomTabColorSchemeParams.Builder()
.setToolbarColor(themeColor)
.setNavigationBarColor(themeColor)
.build()
)
.build()
callingActivity = activity.javaClass
customTabsIntent.intent.flags = Intent.FLAG_ACTIVITY_NO_HISTORY
customTabsIntent.launchUrl(activity, Uri.parse(url))
}
suspend fun finishAuthFlow(activity: Activity, code: String) {
val authFlow = getAuthFlow() ?: return
withContext(Dispatchers.IO) {
val tokenResponse = try {
authFlow.newTokenRequest(code).setRedirectUri(getRedirectUri()).execute()
} catch (e: IOException) {
CrashReporter.logException(e)
return@withContext
}
authFlow.createAndStoreCredential(tokenResponse, USER_ID)
}
loadAccountName()
returnToPreviousActivity(activity)
}
fun cancelAuthFlow(activity: Activity) {
returnToPreviousActivity(activity)
}
private fun returnToPreviousActivity(activity: Activity) {
val intent = Intent(activity, callingActivity)
callingActivity = null
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
activity.startActivity(intent)
}
private suspend fun loadAccountName(): String? {
val requestInitializer = getRequestInitializer() ?: return null
val jsonFactory = GsonFactory.getDefaultInstance()
val oauth2 = Oauth2.Builder(transport, jsonFactory, requestInitializer).build()
try {
val meResponse = withContext(Dispatchers.IO) {
oauth2.userinfo().v2().me().get().execute()
}
if (meResponse != null) {
val name = meResponse.name
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit {
putString(PREF_ACCOUNT_NAME, name)
}
return name
}
} catch (e: IOException) {
CrashReporter.logException(e)
}
return null
}
fun logout() {
val authFlow = getAuthFlow() ?: return
authFlow.credentialDataStore.clear()
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit {
putString(PREF_ACCOUNT_NAME, null)
}
}
private fun getRedirectUri(): String {
return "${context.packageName}:/google-auth-redirect"
}
companion object {
private lateinit var instance: GoogleApiHelper
fun getInstance(context: Context): GoogleApiHelper {
if (!::instance.isInitialized) instance = GoogleApiHelper(context.applicationContext)
return instance
}
val SCOPES = setOf("https://www.googleapis.com/auth/drive", "profile")
const val USER_ID = "google-user"
const val PREFS = "google-account"
const val PREF_ACCOUNT_NAME = "name"
private var callingActivity: Class<Activity>? = null
}
}
@@ -0,0 +1,24 @@
package de.mm20.launcher2.gservices
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
class GoogleAuthRedirectActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val gServiceHelper = GoogleApiHelper.getInstance(this)
val code = intent.data?.getQueryParameter("code")
if (code == null) {
gServiceHelper.cancelAuthFlow(this)
finish()
}
else {
lifecycleScope.launch {
gServiceHelper.finishAuthFlow(this@GoogleAuthRedirectActivity, code)
finish()
}
}
}
}
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="GoogleSigninTheme" parent="@style/Theme.AppCompat.NoActionBar">
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowIsFloating">true</item>
</style>
</resources>
@@ -0,0 +1 @@
{"installed":{"client_id":"xxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com","project_id":"xxxxx-xxxxxxxxxxxxx","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","redirect_uris":["urn:ietf:wg:oauth:2.0:oob","http://localhost"]}}