Benoit Marty
2 years ago
10 changed files with 442 additions and 2 deletions
@ -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) |
||||
} |
||||
} |
@ -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 |
||||
} |
||||
} |
@ -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") } |
||||
} |
||||
} |
@ -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)) |
||||
} |
||||
} |
@ -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() |
||||
} |
@ -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) |
||||
} |
||||
} |
@ -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…
Reference in new issue