Browse Source

Import some classes from Element into :libraries:androidutils

test/jme/compound-poc
Benoit Marty 2 years ago
parent
commit
d8be158078
  1. 5
      gradle/libs.versions.toml
  2. 1
      libraries/androidutils/build.gradle.kts
  3. 1
      libraries/androidutils/src/main/AndroidManifest.xml
  4. 42
      libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/compat/Compat.kt
  5. 30
      libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/intent/PendingIntentCompat.kt
  6. 38
      libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/network/WifiDetector.kt
  7. 31
      libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/CopyToClipboardUseCase.kt
  8. 221
      libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/SystemUtils.kt
  9. 50
      libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/throttler/FirstThrottler.kt
  10. 25
      libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/uri/UriExtensions.kt

5
gradle/libs.versions.toml

@ -16,7 +16,7 @@ datastore = "1.0.0" @@ -16,7 +16,7 @@ datastore = "1.0.0"
constraintlayout = "2.1.4"
recyclerview = "1.3.0"
lifecycle = "2.5.1"
activity_compose = "1.6.1"
activity = "1.6.1"
startup = "1.1.1"
# Compose
@ -70,7 +70,8 @@ androidx_lifecycle_process = { module = "androidx.lifecycle:lifecycle-process", @@ -70,7 +70,8 @@ androidx_lifecycle_process = { module = "androidx.lifecycle:lifecycle-process",
androidx_splash = "androidx.core:core-splashscreen:1.0.0"
androidx_security_crypto = "androidx.security:security-crypto:1.0.0"
androidx_activity_compose = { module = "androidx.activity:activity-compose", version.ref = "activity_compose" }
androidx_activity_activity = { module = "androidx.activity:activity", version.ref = "activity" }
androidx_activity_compose = { module = "androidx.activity:activity-compose", version.ref = "activity" }
androidx_startup = { module = "androidx.startup:startup-runtime", version.ref = "startup" }
androidx_compose_bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose_bom" }

1
libraries/androidutils/build.gradle.kts

@ -26,5 +26,6 @@ android { @@ -26,5 +26,6 @@ android {
dependencies {
implementation(libs.timber)
implementation(libs.androidx.corektx)
implementation(libs.androidx.activity.activity)
implementation(projects.libraries.core)
}

1
libraries/androidutils/src/main/AndroidManifest.xml

@ -17,4 +17,5 @@ @@ -17,4 +17,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
</manifest>

42
libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/compat/Compat.kt

@ -0,0 +1,42 @@ @@ -0,0 +1,42 @@
/*
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
*
* 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 io.element.android.libraries.androidutils.compat
import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.os.Build
fun PackageManager.getApplicationInfoCompat(packageName: String, flags: Int): ApplicationInfo {
return when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> getApplicationInfo(
packageName,
PackageManager.ApplicationInfoFlags.of(flags.toLong())
)
else -> @Suppress("DEPRECATION") getApplicationInfo(packageName, flags)
}
}
fun PackageManager.getPackageInfoCompat(packageName: String, flags: Int): PackageInfo {
return when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> getPackageInfo(
packageName,
PackageManager.PackageInfoFlags.of(flags.toLong())
)
else -> @Suppress("DEPRECATION") getPackageInfo(packageName, flags)
}
}

30
libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/intent/PendingIntentCompat.kt

@ -0,0 +1,30 @@ @@ -0,0 +1,30 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* 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 io.element.android.libraries.androidutils.intent
import android.app.PendingIntent
import android.os.Build
object PendingIntentCompat {
const val FLAG_IMMUTABLE = PendingIntent.FLAG_IMMUTABLE
val FLAG_MUTABLE = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_MUTABLE
} else {
0
}
}

38
libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/network/WifiDetector.kt

@ -0,0 +1,38 @@ @@ -0,0 +1,38 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* 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 io.element.android.libraries.androidutils.network
import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import androidx.core.content.getSystemService
import io.element.android.libraries.core.bool.orFalse
import timber.log.Timber
class WifiDetector(
context: Context
) {
private val connectivityManager = context.getSystemService<ConnectivityManager>()!!
fun isConnectedToWifi(): Boolean {
return connectivityManager.activeNetwork
?.let { connectivityManager.getNetworkCapabilities(it) }
?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
.orFalse()
.also { Timber.d("isConnected to WiFi: $it") }
}
}

31
libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/CopyToClipboardUseCase.kt

@ -0,0 +1,31 @@ @@ -0,0 +1,31 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* 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 io.element.android.libraries.androidutils.system
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import androidx.core.content.getSystemService
class CopyToClipboardUseCase(
private val context: Context,
) {
fun execute(text: CharSequence) {
context.getSystemService<ClipboardManager>()
?.setPrimaryClip(ClipData.newPlainText("", text))
}
}

221
libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/SystemUtils.kt

@ -0,0 +1,221 @@ @@ -0,0 +1,221 @@
/*
* Copyright 2018 New Vector Ltd
*
* 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 io.element.android.libraries.androidutils.system
import android.annotation.TargetApi
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.PowerManager
import android.provider.Settings
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.ChecksSdkIntAtLeast
import androidx.annotation.RequiresApi
import androidx.core.content.getSystemService
import io.element.android.libraries.androidutils.compat.getApplicationInfoCompat
/**
* Tells if the application ignores battery optimizations.
*
* Ignoring them allows the app to run in background to make background sync with the homeserver.
* This user option appears on Android M but Android O enforces its usage and kills apps not
* authorised by the user to run in background.
*
* @return true if battery optimisations are ignored
*/
fun Context.isIgnoringBatteryOptimizations(): Boolean {
// no issue before Android M, battery optimisations did not exist
return getSystemService<PowerManager>()?.isIgnoringBatteryOptimizations(packageName) == true
}
fun Context.isAirplaneModeOn(): Boolean {
return Settings.Global.getInt(contentResolver, Settings.Global.AIRPLANE_MODE_ON, 0) != 0
}
fun Context.isAnimationEnabled(): Boolean {
return Settings.Global.getFloat(contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE, 1f) != 0f
}
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.O)
fun supportNotificationChannels() = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
/**
* Return the application label of the provided package. If not found, the package is returned.
*/
fun Context.getApplicationLabel(packageName: String): String {
return try {
val ai = packageManager.getApplicationInfoCompat(packageName, 0)
packageManager.getApplicationLabel(ai).toString()
} catch (e: PackageManager.NameNotFoundException) {
packageName
}
}
/**
* display the system dialog for granting this permission. If previously granted, the
* system will not show it (so you should call this method).
*
* Note: If the user finally does not grant the permission, PushManager.isBackgroundSyncAllowed()
* will return false and the notification privacy will fallback to "LOW_DETAIL".
*/
fun requestDisablingBatteryOptimization(activity: Activity, activityResultLauncher: ActivityResultLauncher<Intent>) {
val intent = Intent()
intent.action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
intent.data = Uri.parse("package:" + activity.packageName)
activityResultLauncher.launch(intent)
}
// ==============================================================================================================
// Clipboard helper
// ==============================================================================================================
/**
* Copy a text to the clipboard, and display a Toast when done.
*
* @param context the context
* @param text the text to copy
* @param toastMessage content of the toast message as a String resource. Null for no toast
*/
fun copyToClipboard(
context: Context,
text: CharSequence,
toastMessage: String? = null
) {
CopyToClipboardUseCase(context).execute(text)
toastMessage?.let { context.toast(it) }
}
/**
* Shows notification settings for the current app.
* In android O will directly opens the notification settings, in lower version it will show the App settings
*/
fun startNotificationSettingsIntent(context: Context, activityResultLauncher: ActivityResultLauncher<Intent>) {
val intent = Intent()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
intent.action = Settings.ACTION_APP_NOTIFICATION_SETTINGS
intent.putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)
} else {
intent.action = Settings.ACTION_APP_NOTIFICATION_SETTINGS
intent.putExtra("app_package", context.packageName)
intent.putExtra("app_uid", context.applicationInfo?.uid)
}
activityResultLauncher.launch(intent)
}
/**
* Shows notification system settings for the given channel id.
*/
@TargetApi(Build.VERSION_CODES.O)
fun startNotificationChannelSettingsIntent(activity: Activity, channelID: String) {
if (!supportNotificationChannels()) return
val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS).apply {
putExtra(Settings.EXTRA_APP_PACKAGE, activity.packageName)
putExtra(Settings.EXTRA_CHANNEL_ID, channelID)
}
activity.startActivity(intent)
}
fun startAddGoogleAccountIntent(
context: Context,
activityResultLauncher: ActivityResultLauncher<Intent>,
noActivityFoundMessage: String,
) {
val intent = Intent(Settings.ACTION_ADD_ACCOUNT)
intent.putExtra(Settings.EXTRA_ACCOUNT_TYPES, arrayOf("com.google"))
try {
activityResultLauncher.launch(intent)
} catch (activityNotFoundException: ActivityNotFoundException) {
context.toast(noActivityFoundMessage)
}
}
@RequiresApi(Build.VERSION_CODES.O)
fun startInstallFromSourceIntent(
context: Context,
activityResultLauncher: ActivityResultLauncher<Intent>,
noActivityFoundMessage: String,
) {
val intent = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES)
.setData(Uri.parse(String.format("package:%s", context.packageName)))
try {
activityResultLauncher.launch(intent)
} catch (activityNotFoundException: ActivityNotFoundException) {
context.toast(noActivityFoundMessage)
}
}
fun startSharePlainTextIntent(
context: Context,
activityResultLauncher: ActivityResultLauncher<Intent>?,
chooserTitle: String?,
text: String,
subject: String? = null,
extraTitle: String? = null,
noActivityFoundMessage: String,
) {
val share = Intent(Intent.ACTION_SEND)
share.type = "text/plain"
share.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT)
// Add data to the intent, the receiving app will decide what to do with it.
share.putExtra(Intent.EXTRA_SUBJECT, subject)
share.putExtra(Intent.EXTRA_TEXT, text)
extraTitle?.let {
share.putExtra(Intent.EXTRA_TITLE, it)
}
val intent = Intent.createChooser(share, chooserTitle)
try {
if (activityResultLauncher != null) {
activityResultLauncher.launch(intent)
} else {
context.startActivity(intent)
}
} catch (activityNotFoundException: ActivityNotFoundException) {
context.toast(noActivityFoundMessage)
}
}
fun startImportTextFromFileIntent(
context: Context,
activityResultLauncher: ActivityResultLauncher<Intent>,
noActivityFoundMessage: String,
) {
val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
type = "text/plain"
}
try {
activityResultLauncher.launch(intent)
} catch (activityNotFoundException: ActivityNotFoundException) {
context.toast(noActivityFoundMessage)
}
}
// Not in KTX anymore
fun Context.toast(resId: Int) {
Toast.makeText(this, resId, Toast.LENGTH_SHORT).show()
}
// Not in KTX anymore
fun Context.toast(message: String) {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
}

50
libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/throttler/FirstThrottler.kt

@ -0,0 +1,50 @@ @@ -0,0 +1,50 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* 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 io.element.android.libraries.androidutils.throttler
import android.os.SystemClock
/**
* Simple ThrottleFirst
* See https://raw.githubusercontent.com/wiki/ReactiveX/RxJava/images/rx-operators/throttleFirst.png
*/
class FirstThrottler(private val minimumInterval: Long = 800) {
private var lastDate = 0L
sealed class CanHandlerResult {
object Yes : CanHandlerResult()
data class No(val shouldWaitMillis: Long) : CanHandlerResult()
fun waitMillis(): Long {
return when (this) {
Yes -> 0
is No -> shouldWaitMillis
}
}
}
fun canHandle(): CanHandlerResult {
val now = SystemClock.elapsedRealtime()
val delaySinceLast = now - lastDate
if (delaySinceLast > minimumInterval) {
lastDate = now
return CanHandlerResult.Yes
}
// Too soon
return CanHandlerResult.No(minimumInterval - delaySinceLast)
}
}

25
libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/uri/UriExtensions.kt

@ -0,0 +1,25 @@ @@ -0,0 +1,25 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* 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 io.element.android.libraries.androidutils.uri
import android.net.Uri
const val IGNORED_SCHEMA = "ignored"
fun Uri.isIgnored() = scheme == IGNORED_SCHEMA
fun createIgnoredUri(path: String): Uri = Uri.parse("$IGNORED_SCHEMA://$path")
Loading…
Cancel
Save