624 lines
24 KiB
Kotlin
624 lines
24 KiB
Kotlin
/*
|
|
* Twidere - Twitter client for Android
|
|
*
|
|
* Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
package org.mariotaku.twidere.util
|
|
|
|
import android.accounts.AccountManager
|
|
import android.annotation.SuppressLint
|
|
import android.app.ActionBar
|
|
import android.app.Activity
|
|
import android.content.*
|
|
import android.content.pm.ActivityInfo
|
|
import android.content.pm.PackageManager
|
|
import android.content.pm.ResolveInfo
|
|
import android.graphics.Rect
|
|
import android.graphics.drawable.Drawable
|
|
import android.net.ConnectivityManager
|
|
import android.net.Uri
|
|
import android.nfc.NfcAdapter
|
|
import android.nfc.NfcAdapter.CreateNdefMessageCallback
|
|
import android.os.BatteryManager
|
|
import android.os.Bundle
|
|
import androidx.annotation.DrawableRes
|
|
import androidx.annotation.StringRes
|
|
import androidx.core.net.ConnectivityManagerCompat
|
|
import androidx.core.view.GravityCompat
|
|
import androidx.core.view.accessibility.AccessibilityEventCompat
|
|
import androidx.appcompat.app.AppCompatActivity
|
|
import android.text.TextUtils
|
|
import android.text.format.DateFormat
|
|
import android.text.format.DateUtils
|
|
import android.util.Log
|
|
import android.util.TypedValue
|
|
import android.view.Gravity
|
|
import android.view.KeyCharacterMap
|
|
import android.view.KeyEvent
|
|
import android.view.View
|
|
import android.view.accessibility.AccessibilityEvent
|
|
import android.view.accessibility.AccessibilityManager
|
|
import android.widget.Toast
|
|
import org.mariotaku.kpreferences.get
|
|
import org.mariotaku.ktextension.getNullableTypedArray
|
|
import org.mariotaku.ktextension.toLocalizedString
|
|
import org.mariotaku.pickncrop.library.PNCUtils
|
|
import org.mariotaku.sqliteqb.library.AllColumns
|
|
import org.mariotaku.sqliteqb.library.Columns
|
|
import org.mariotaku.sqliteqb.library.Columns.Column
|
|
import org.mariotaku.sqliteqb.library.Expression
|
|
import org.mariotaku.sqliteqb.library.Selectable
|
|
import org.mariotaku.twidere.R
|
|
import org.mariotaku.twidere.TwidereConstants.LOGTAG
|
|
import org.mariotaku.twidere.TwidereConstants.METADATA_KEY_EXTENSION_USE_JSON
|
|
import org.mariotaku.twidere.TwidereConstants.SHARED_PREFERENCES_NAME
|
|
import org.mariotaku.twidere.TwidereConstants.TAB_CODE_DIRECT_MESSAGES
|
|
import org.mariotaku.twidere.TwidereConstants.TAB_CODE_HOME_TIMELINE
|
|
import org.mariotaku.twidere.TwidereConstants.TAB_CODE_NOTIFICATIONS_TIMELINE
|
|
import org.mariotaku.twidere.annotation.CustomTabType
|
|
import org.mariotaku.twidere.annotation.ProfileImageSize
|
|
import org.mariotaku.twidere.constant.CompatibilityConstants.EXTRA_ACCOUNT_ID
|
|
import org.mariotaku.twidere.constant.IntentConstants.EXTRA_ACCOUNT_KEY
|
|
import org.mariotaku.twidere.constant.IntentConstants.EXTRA_ACCOUNT_KEYS
|
|
import org.mariotaku.twidere.constant.IntentConstants.INTENT_ACTION_PEBBLE_NOTIFICATION
|
|
import org.mariotaku.twidere.constant.SharedPreferenceConstants.*
|
|
import org.mariotaku.twidere.constant.bandwidthSavingModeKey
|
|
import org.mariotaku.twidere.constant.defaultAccountKey
|
|
import org.mariotaku.twidere.constant.mediaPreviewKey
|
|
import org.mariotaku.twidere.model.ParcelableStatus
|
|
import org.mariotaku.twidere.model.ParcelableUserMention
|
|
import org.mariotaku.twidere.model.PebbleMessage
|
|
import org.mariotaku.twidere.model.UserKey
|
|
import org.mariotaku.twidere.model.util.AccountUtils
|
|
import org.mariotaku.twidere.provider.TwidereDataStore.CachedUsers
|
|
import org.mariotaku.twidere.util.TwidereLinkify.PATTERN_TWITTER_PROFILE_IMAGES
|
|
import org.mariotaku.twidere.view.TabPagerIndicator
|
|
import java.io.File
|
|
import java.io.InputStream
|
|
import java.io.OutputStream
|
|
import java.util.*
|
|
import java.util.regex.Pattern
|
|
|
|
object Utils {
|
|
|
|
private val PATTERN_XML_RESOURCE_IDENTIFIER = Pattern.compile("res/xml/([\\w_]+)\\.xml")
|
|
private val PATTERN_RESOURCE_IDENTIFIER = Pattern.compile("@([\\w_]+)/([\\w_]+)")
|
|
|
|
private val HOME_TABS_URI_MATCHER = UriMatcher(UriMatcher.NO_MATCH)
|
|
|
|
init {
|
|
|
|
HOME_TABS_URI_MATCHER.addURI(CustomTabType.HOME_TIMELINE, null, TAB_CODE_HOME_TIMELINE)
|
|
HOME_TABS_URI_MATCHER.addURI(CustomTabType.NOTIFICATIONS_TIMELINE, null, TAB_CODE_NOTIFICATIONS_TIMELINE)
|
|
HOME_TABS_URI_MATCHER.addURI(CustomTabType.DIRECT_MESSAGES, null, TAB_CODE_DIRECT_MESSAGES)
|
|
}
|
|
|
|
|
|
fun announceForAccessibilityCompat(context: Context, view: View, text: CharSequence,
|
|
cls: Class<*>) {
|
|
val accessibilityManager = context
|
|
.getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager
|
|
if (!accessibilityManager.isEnabled) return
|
|
// Prior to SDK 16, announcements could only be made through FOCUSED
|
|
// events. Jelly Bean (SDK 16) added support for speaking text verbatim
|
|
// using the ANNOUNCEMENT event type.
|
|
val eventType: Int = AccessibilityEventCompat.TYPE_ANNOUNCEMENT
|
|
|
|
// Construct an accessibility event with the minimum recommended
|
|
// attributes. An event without a class name or package may be dropped.
|
|
val event = AccessibilityEvent.obtain(eventType)
|
|
event.text.add(text)
|
|
event.className = cls.name
|
|
event.packageName = context.packageName
|
|
event.setSource(view)
|
|
|
|
// Sends the event directly through the accessibility manager. If your
|
|
// application only targets SDK 14+, you should just call
|
|
// getParent().requestSendAccessibilityEvent(this, event);
|
|
accessibilityManager.sendAccessibilityEvent(event)
|
|
}
|
|
|
|
fun deleteMedia(context: Context, uri: Uri): Boolean {
|
|
return try {
|
|
PNCUtils.deleteMedia(context, uri)
|
|
} catch (e: SecurityException) {
|
|
false
|
|
}
|
|
|
|
}
|
|
|
|
fun sanitizeMimeType(contentType: String?): String? {
|
|
if (contentType == null) return null
|
|
when (contentType) {
|
|
"image/jpg" -> return "image/jpeg"
|
|
}
|
|
return contentType
|
|
}
|
|
|
|
fun createStatusShareIntent(context: Context, status: ParcelableStatus): Intent {
|
|
val intent = Intent(Intent.ACTION_SEND)
|
|
intent.type = "text/plain"
|
|
intent.putExtra(Intent.EXTRA_SUBJECT, IntentUtils.getStatusShareSubject(context, status))
|
|
intent.putExtra(Intent.EXTRA_TEXT, IntentUtils.getStatusShareText(context, status))
|
|
intent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
|
return intent
|
|
}
|
|
|
|
fun getAccountKeys(context: Context, args: Bundle?): Array<UserKey>? {
|
|
if (args == null) return null
|
|
when {
|
|
args.containsKey(EXTRA_ACCOUNT_KEYS) -> {
|
|
return args.getNullableTypedArray(EXTRA_ACCOUNT_KEYS)
|
|
}
|
|
args.containsKey(EXTRA_ACCOUNT_KEY) -> {
|
|
val accountKey = args.getParcelable<UserKey>(EXTRA_ACCOUNT_KEY) ?: return emptyArray()
|
|
return arrayOf(accountKey)
|
|
}
|
|
args.containsKey(EXTRA_ACCOUNT_ID) -> {
|
|
val accountId = args.get(EXTRA_ACCOUNT_ID).toString()
|
|
try {
|
|
if (java.lang.Long.parseLong(accountId) <= 0) return null
|
|
} catch (e: NumberFormatException) {
|
|
// Ignore
|
|
}
|
|
|
|
val accountKey = DataStoreUtils.findAccountKey(context, accountId)
|
|
args.putParcelable(EXTRA_ACCOUNT_KEY, accountKey)
|
|
if (accountKey == null) return arrayOf(UserKey(accountId, null))
|
|
return arrayOf(accountKey)
|
|
}
|
|
else -> return null
|
|
}
|
|
}
|
|
|
|
fun getAccountKey(context: Context, args: Bundle?): UserKey? {
|
|
return getAccountKeys(context, args)?.firstOrNull()
|
|
}
|
|
|
|
fun getReadPositionTagWithAccount(tag: String, accountKey: UserKey?): String {
|
|
if (accountKey == null) return tag
|
|
return "$accountKey:$tag"
|
|
}
|
|
|
|
fun formatSameDayTime(context: Context, timestamp: Long): String? {
|
|
if (DateUtils.isToday(timestamp))
|
|
return DateUtils.formatDateTime(context, timestamp,
|
|
if (DateFormat.is24HourFormat(context))
|
|
DateUtils.FORMAT_SHOW_TIME or DateUtils.FORMAT_24HOUR
|
|
else
|
|
DateUtils.FORMAT_SHOW_TIME or DateUtils.FORMAT_12HOUR)
|
|
return DateUtils.formatDateTime(context, timestamp, DateUtils.FORMAT_SHOW_DATE)
|
|
}
|
|
|
|
fun formatToLongTimeString(context: Context, timestamp: Long): String? {
|
|
val formatFlags = DateUtils.FORMAT_NO_NOON_MIDNIGHT or DateUtils.FORMAT_ABBREV_ALL or
|
|
DateUtils.FORMAT_CAP_AMPM or DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_TIME
|
|
return DateUtils.formatDateTime(context, timestamp, formatFlags)
|
|
}
|
|
|
|
fun isComposeNowSupported(context: Context): Boolean {
|
|
return hasNavBar(context)
|
|
}
|
|
|
|
fun setLastSeen(context: Context, entities: Array<ParcelableUserMention>?, time: Long): Boolean {
|
|
if (entities == null) return false
|
|
var result = false
|
|
for (entity in entities) {
|
|
result = result or setLastSeen(context, entity.key, time)
|
|
}
|
|
return result
|
|
}
|
|
|
|
fun setLastSeen(context: Context, userKey: UserKey, time: Long): Boolean {
|
|
val cr = context.contentResolver
|
|
val values = ContentValues()
|
|
if (time > 0) {
|
|
values.put(CachedUsers.LAST_SEEN, time)
|
|
} else {
|
|
// Zero or negative value means remove last seen
|
|
values.putNull(CachedUsers.LAST_SEEN)
|
|
}
|
|
val where = Expression.equalsArgs(CachedUsers.USER_KEY).sql
|
|
val selectionArgs = arrayOf(userKey.toString())
|
|
return cr.update(CachedUsers.CONTENT_URI, values, where, selectionArgs) > 0
|
|
}
|
|
|
|
|
|
fun getColumnsFromProjection(projection: Array<String>?): Selectable {
|
|
if (projection == null) return AllColumns()
|
|
val length = projection.size
|
|
val columns = arrayOfNulls<Column>(length)
|
|
for (i in 0 until length) {
|
|
columns[i] = Column(projection[i])
|
|
}
|
|
return Columns(*columns)
|
|
}
|
|
|
|
fun getDefaultAccountKey(context: Context): UserKey? {
|
|
val prefs = context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE)
|
|
val accountKey = prefs[defaultAccountKey]
|
|
val accountKeys = DataStoreUtils.getAccountKeys(context)
|
|
|
|
return accountKeys.find { it.maybeEquals(accountKey) } ?: run {
|
|
if (!accountKeys.contains(accountKey)) {
|
|
return@run accountKeys.firstOrNull()
|
|
}
|
|
return@run null
|
|
}
|
|
}
|
|
|
|
fun getInternalCacheDir(context: Context?, cacheDirName: String): File {
|
|
if (context == null) throw NullPointerException()
|
|
val cacheDir = File(context.cacheDir, cacheDirName)
|
|
if (cacheDir.isDirectory || cacheDir.mkdirs()) return cacheDir
|
|
return File(context.cacheDir, cacheDirName)
|
|
}
|
|
|
|
fun getExternalCacheDir(context: Context, cacheDirName: String, sizeInBytes: Long): File? {
|
|
val externalCacheDir = try {
|
|
context.externalCacheDir
|
|
} catch (e: SecurityException) {
|
|
null
|
|
} ?: return null
|
|
val cacheDir = File(externalCacheDir, cacheDirName)
|
|
if (sizeInBytes > 0 && externalCacheDir.freeSpace < sizeInBytes / 10) {
|
|
// Less then 10% space available
|
|
return null
|
|
}
|
|
if (cacheDir.isDirectory || cacheDir.mkdirs()) return cacheDir
|
|
return null
|
|
}
|
|
|
|
fun getLocalizedNumber(locale: Locale, number: Number): String {
|
|
return number.toLocalizedString(locale)
|
|
}
|
|
|
|
fun getMatchedNicknameKeys(str: String, manager: UserColorNameManager): Array<String> {
|
|
if (str.isEmpty()) return emptyArray()
|
|
return manager.nicknames.filter { (_, value) ->
|
|
val valueStr = value?.toString()?.takeIf(String::isNotEmpty) ?: return@filter false
|
|
return@filter valueStr.startsWith(str, ignoreCase = true)
|
|
}.map { it.key }.toTypedArray()
|
|
}
|
|
|
|
fun getOriginalTwitterProfileImage(url: String): String {
|
|
val matcher = PATTERN_TWITTER_PROFILE_IMAGES.matcher(url)
|
|
if (matcher.matches())
|
|
return matcher.replaceFirst("$1$2/profile_images/$3/$4$6")
|
|
return url
|
|
}
|
|
|
|
fun getQuoteStatus(context: Context?, status: ParcelableStatus): String? {
|
|
if (context == null) return null
|
|
var quoteFormat: String = context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE)
|
|
.getString(KEY_QUOTE_FORMAT, DEFAULT_QUOTE_FORMAT).orEmpty()
|
|
if (TextUtils.isEmpty(quoteFormat)) {
|
|
quoteFormat = DEFAULT_QUOTE_FORMAT
|
|
}
|
|
var result = quoteFormat.replace(FORMAT_PATTERN_LINK, LinkCreator.getStatusWebLink(status).toString())
|
|
result = result.replace(FORMAT_PATTERN_NAME, status.user_screen_name)
|
|
result = result.replace(FORMAT_PATTERN_TEXT, status.text_plain)
|
|
return result
|
|
}
|
|
|
|
fun getResId(context: Context?, string: String?): Int {
|
|
if (context == null || string == null) return 0
|
|
var m = PATTERN_RESOURCE_IDENTIFIER.matcher(string)
|
|
val res = context.resources
|
|
if (m.matches()) return res.getIdentifier(m.group(2), m.group(1), context.packageName)
|
|
m = PATTERN_XML_RESOURCE_IDENTIFIER.matcher(string)
|
|
if (m.matches()) return res.getIdentifier(m.group(1), "xml", context.packageName)
|
|
return 0
|
|
}
|
|
|
|
|
|
fun getTabDisplayOption(context: Context): String {
|
|
val defaultOption = context.getString(R.string.default_tab_display_option)
|
|
val prefs = context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE)
|
|
return prefs.getString(KEY_TAB_DISPLAY_OPTION, defaultOption) ?: defaultOption
|
|
}
|
|
|
|
fun getTabDisplayOptionInt(context: Context): Int {
|
|
return getTabDisplayOptionInt(getTabDisplayOption(context))
|
|
}
|
|
|
|
fun getTabDisplayOptionInt(option: String): Int {
|
|
if (VALUE_TAB_DISPLAY_OPTION_ICON == option)
|
|
return TabPagerIndicator.DisplayOption.ICON
|
|
else if (VALUE_TAB_DISPLAY_OPTION_LABEL == option)
|
|
return TabPagerIndicator.DisplayOption.LABEL
|
|
return TabPagerIndicator.DisplayOption.BOTH
|
|
}
|
|
|
|
fun hasNavBar(context: Context): Boolean {
|
|
val resources = context.resources ?: return false
|
|
val id = resources.getIdentifier("config_showNavigationBar", "bool", "android")
|
|
return if (id > 0) {
|
|
resources.getBoolean(id)
|
|
} else {
|
|
// Check for keys
|
|
!KeyCharacterMap.deviceHasKey(KeyEvent.KEYCODE_BACK) && !KeyCharacterMap.deviceHasKey(KeyEvent.KEYCODE_HOME)
|
|
}
|
|
}
|
|
|
|
|
|
fun getTwitterProfileImageOfSize(url: String, size: String): String {
|
|
if (ProfileImageSize.ORIGINAL == size) {
|
|
return getOriginalTwitterProfileImage(url)
|
|
}
|
|
val matcher = PATTERN_TWITTER_PROFILE_IMAGES.matcher(url)
|
|
if (matcher.matches()) {
|
|
return matcher.replaceFirst("$1$2/profile_images/$3/$4_$size$6")
|
|
}
|
|
return url
|
|
}
|
|
|
|
@DrawableRes
|
|
fun getUserTypeIconRes(isVerified: Boolean, isProtected: Boolean): Int {
|
|
if (isVerified)
|
|
return R.drawable.ic_user_type_verified
|
|
else if (isProtected) return R.drawable.ic_user_type_protected
|
|
return 0
|
|
}
|
|
|
|
@StringRes
|
|
fun getUserTypeDescriptionRes(isVerified: Boolean, isProtected: Boolean): Int {
|
|
if (isVerified)
|
|
return R.string.user_type_verified
|
|
else if (isProtected) return R.string.user_type_protected
|
|
return 0
|
|
}
|
|
|
|
fun isBatteryOkay(context: Context?): Boolean {
|
|
if (context == null) return false
|
|
val app = context.applicationContext
|
|
val filter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
|
|
val intent: Intent?
|
|
try {
|
|
intent = app.registerReceiver(null, filter)
|
|
} catch (e: Exception) {
|
|
return false
|
|
}
|
|
|
|
if (intent == null) return false
|
|
val plugged = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) != 0
|
|
val level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0).toFloat()
|
|
val scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, 100).toFloat()
|
|
return plugged || level / scale > 0.15f
|
|
}
|
|
|
|
fun isMyAccount(context: Context, accountKey: UserKey): Boolean {
|
|
val am = AccountManager.get(context)
|
|
return AccountUtils.findByAccountKey(am, accountKey) != null
|
|
}
|
|
|
|
fun isMyAccount(context: Context, screenName: String): Boolean {
|
|
val am = AccountManager.get(context)
|
|
return AccountUtils.findByScreenName(am, screenName) != null
|
|
}
|
|
|
|
fun isMyRetweet(status: ParcelableStatus?): Boolean {
|
|
return status != null && isMyRetweet(status.account_key, status.retweeted_by_user_key,
|
|
status.my_retweet_id)
|
|
}
|
|
|
|
fun isMyRetweet(accountKey: UserKey, retweetedByKey: UserKey?, myRetweetId: String?): Boolean {
|
|
return accountKey == retweetedByKey || myRetweetId != null
|
|
}
|
|
|
|
fun matchTabCode(uri: Uri): Int {
|
|
return HOME_TABS_URI_MATCHER.match(uri)
|
|
}
|
|
|
|
|
|
@CustomTabType
|
|
fun matchTabType(uri: Uri): String? {
|
|
return getTabType(matchTabCode(uri))
|
|
}
|
|
|
|
@CustomTabType
|
|
fun getTabType(code: Int): String? {
|
|
when (code) {
|
|
TAB_CODE_HOME_TIMELINE -> {
|
|
return CustomTabType.HOME_TIMELINE
|
|
}
|
|
TAB_CODE_NOTIFICATIONS_TIMELINE -> {
|
|
return CustomTabType.NOTIFICATIONS_TIMELINE
|
|
}
|
|
TAB_CODE_DIRECT_MESSAGES -> {
|
|
return CustomTabType.DIRECT_MESSAGES
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
|
|
fun setNdefPushMessageCallback(activity: Activity, callback: CreateNdefMessageCallback): Boolean {
|
|
try {
|
|
val adapter = NfcAdapter.getDefaultAdapter(activity) ?: return false
|
|
adapter.setNdefPushMessageCallback(callback, activity)
|
|
return true
|
|
} catch (e: SecurityException) {
|
|
Log.w(LOGTAG, e)
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
fun getInsetsTopWithoutActionBarHeight(context: Context, top: Int): Int {
|
|
val actionBarHeight: Int = when (context) {
|
|
is AppCompatActivity -> {
|
|
getActionBarHeight(context.supportActionBar)
|
|
}
|
|
is Activity -> {
|
|
getActionBarHeight(context.actionBar)
|
|
}
|
|
else -> {
|
|
return top
|
|
}
|
|
}
|
|
if (actionBarHeight > top) {
|
|
return top
|
|
}
|
|
return top - actionBarHeight
|
|
}
|
|
|
|
fun restartActivity(activity: Activity?) {
|
|
if (activity == null) return
|
|
val enterAnim = android.R.anim.fade_in
|
|
val exitAnim = android.R.anim.fade_out
|
|
activity.finish()
|
|
activity.overridePendingTransition(enterAnim, exitAnim)
|
|
activity.startActivity(activity.intent)
|
|
activity.overridePendingTransition(enterAnim, exitAnim)
|
|
}
|
|
|
|
internal fun isMyStatus(status: ParcelableStatus): Boolean {
|
|
if (isMyRetweet(status)) return true
|
|
return status.account_key.maybeEquals(status.user_key)
|
|
}
|
|
|
|
fun showMenuItemToast(v: View, text: CharSequence, isBottomBar: Boolean) {
|
|
val screenPos = IntArray(2)
|
|
val displayFrame = Rect()
|
|
v.getLocationOnScreen(screenPos)
|
|
v.getWindowVisibleDisplayFrame(displayFrame)
|
|
val width = v.width
|
|
val height = v.height
|
|
val screenWidth = v.resources.displayMetrics.widthPixels
|
|
val cheatSheet = Toast.makeText(v.context.applicationContext, text, Toast.LENGTH_SHORT)
|
|
if (isBottomBar) {
|
|
// Show along the bottom center
|
|
cheatSheet.setGravity(Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL, 0, height)
|
|
} else {
|
|
// Show along the top; follow action buttons
|
|
cheatSheet.setGravity(Gravity.TOP or GravityCompat.END, screenWidth - screenPos[0] - width / 2, height)
|
|
}
|
|
cheatSheet.show()
|
|
}
|
|
|
|
internal fun getMetadataDrawable(pm: PackageManager?, info: ActivityInfo?, key: String?): Drawable? {
|
|
if (pm == null || info == null || info.metaData == null || key == null || !info.metaData.containsKey(key))
|
|
return null
|
|
return pm.getDrawable(info.packageName, info.metaData.getInt(key), info.applicationInfo)
|
|
}
|
|
|
|
internal fun isExtensionUseJSON(info: ResolveInfo?): Boolean {
|
|
if (info?.activityInfo == null) return false
|
|
val activityInfo = info.activityInfo
|
|
if (activityInfo.metaData != null && activityInfo.metaData.containsKey(METADATA_KEY_EXTENSION_USE_JSON))
|
|
return activityInfo.metaData.getBoolean(METADATA_KEY_EXTENSION_USE_JSON)
|
|
val appInfo = activityInfo.applicationInfo ?: return false
|
|
return appInfo.metaData != null && appInfo.metaData.getBoolean(METADATA_KEY_EXTENSION_USE_JSON, false)
|
|
}
|
|
|
|
fun getActionBarHeight(actionBar: ActionBar?): Int {
|
|
if (actionBar == null) return 0
|
|
val context = actionBar.themedContext
|
|
val tv = TypedValue()
|
|
val height = actionBar.height
|
|
if (height > 0) return height
|
|
if (context.theme.resolveAttribute(android.R.attr.actionBarSize, tv, true)) {
|
|
return TypedValue.complexToDimensionPixelSize(tv.data, context.resources.displayMetrics)
|
|
}
|
|
return 0
|
|
}
|
|
|
|
fun getActionBarHeight(actionBar: androidx.appcompat.app.ActionBar?): Int {
|
|
if (actionBar == null) return 0
|
|
val context = actionBar.themedContext
|
|
val tv = TypedValue()
|
|
val height = actionBar.height
|
|
if (height > 0) return height
|
|
if (context.theme.resolveAttribute(R.attr.actionBarSize, tv, true)) {
|
|
return TypedValue.complexToDimensionPixelSize(tv.data, context.resources.displayMetrics)
|
|
}
|
|
return 0
|
|
}
|
|
|
|
fun getNotificationId(baseId: Int, accountKey: UserKey?): Int {
|
|
var result = baseId
|
|
result = 31 * result + (accountKey?.hashCode() ?: 0)
|
|
return result
|
|
}
|
|
|
|
@SuppressLint("InlinedApi")
|
|
fun isCharging(context: Context?): Boolean {
|
|
if (context == null) return false
|
|
val intent = context.registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED)) ?: return false
|
|
val plugged = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1)
|
|
return plugged == BatteryManager.BATTERY_PLUGGED_AC
|
|
|| plugged == BatteryManager.BATTERY_PLUGGED_USB
|
|
|| plugged == BatteryManager.BATTERY_PLUGGED_WIRELESS
|
|
}
|
|
|
|
fun isMediaPreviewEnabled(context: Context, preferences: SharedPreferences): Boolean {
|
|
if (!preferences[mediaPreviewKey])
|
|
return false
|
|
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
|
return !ConnectivityManagerCompat.isActiveNetworkMetered(cm) || !preferences[bandwidthSavingModeKey]
|
|
}
|
|
|
|
/**
|
|
* Send Notifications to Pebble smart watches
|
|
|
|
* @param context Context
|
|
* *
|
|
* @param title String
|
|
* *
|
|
* @param message String
|
|
*/
|
|
fun sendPebbleNotification(context: Context, title: String?, message: String) {
|
|
|
|
val appName: String = if (title == null) {
|
|
context.getString(R.string.app_name)
|
|
} else {
|
|
"${context.getString(R.string.app_name)} - $title"
|
|
}
|
|
|
|
if (TextUtils.isEmpty(message)) return
|
|
val prefs = context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE)
|
|
|
|
if (prefs.getBoolean(KEY_PEBBLE_NOTIFICATIONS, false)) {
|
|
|
|
val messages = ArrayList<PebbleMessage>()
|
|
messages.add(PebbleMessage(appName, message))
|
|
|
|
val intent = Intent(INTENT_ACTION_PEBBLE_NOTIFICATION)
|
|
intent.putExtra("messageType", "PEBBLE_ALERT")
|
|
intent.putExtra("sender", appName)
|
|
intent.putExtra("notificationData", JsonSerializer.serializeList(messages, PebbleMessage::class.java))
|
|
|
|
context.applicationContext.sendBroadcast(intent)
|
|
}
|
|
|
|
}
|
|
|
|
fun copyStream(input: InputStream, output: OutputStream, length: Int) {
|
|
val buffer = ByteArray(1024)
|
|
var bytesRead = 0
|
|
do {
|
|
val read = input.read(buffer)
|
|
if (read == -1) {
|
|
break
|
|
}
|
|
output.write(buffer, 0, read)
|
|
bytesRead += read
|
|
} while (bytesRead <= length)
|
|
}
|
|
}
|