Convert util/{HttpHeaderLink,PairedList,TimestampUtils,ThemeUtils} to Kotlin (#3046)

* Fix off-by-one error in HttpHeaderLink

Link headers with multiple URLs with multiple parameters were being parsed
incorrectly.

Detected by adding unit tests ahead of converting to Kotlin.

* Convert util/HttpHeaderLink from Java to Kotlin

* Convert util/ThemeUtils from Java to Kotlin

* Convert util/TimestampUtils from Java to Kotlin

* Add tests for PairedList

* Convert util/PairedList from Java to Kotlin

* Implement feedback from PR

* Relicense as GPL
This commit is contained in:
Nik Clayton 2022-12-31 13:01:35 +01:00 committed by GitHub
parent 0def7e7230
commit 22834431ca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 624 additions and 510 deletions

View File

@ -21,6 +21,7 @@ import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.os.Bundle;
import android.util.Log;
import android.view.MenuItem;
@ -35,6 +36,7 @@ import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.preference.PreferenceManager;
import com.google.android.material.color.MaterialColors;
import com.google.android.material.snackbar.Snackbar;
import com.keylesspalace.tusky.adapter.AccountSelectionAdapter;
import com.keylesspalace.tusky.components.login.LoginActivity;
@ -77,7 +79,7 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
/* set the taskdescription programmatically, the theme would turn it blue */
String appName = getString(R.string.app_name);
Bitmap appIcon = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher);
int recentsBackgroundColor = ThemeUtils.getColor(this, R.attr.colorSurface);
int recentsBackgroundColor = MaterialColors.getColor(this, R.attr.colorSurface, Color.BLACK);
setTaskDescription(new ActivityManager.TaskDescription(appName, appIcon, recentsBackgroundColor));

View File

@ -38,12 +38,12 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import at.connyduck.sparkbutton.helpers.Utils
import com.google.android.material.color.MaterialColors
import com.google.android.material.snackbar.Snackbar
import com.keylesspalace.tusky.databinding.ActivityListsBinding
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.entity.MastoList
import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.onTextChanged
import com.keylesspalace.tusky.util.show
@ -244,8 +244,8 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
return LayoutInflater.from(parent.context).inflate(R.layout.item_list, parent, false)
.let(this::ListViewHolder)
.apply {
val iconColor = MaterialColors.getColor(nameTextView, android.R.attr.textColorTertiary)
val context = nameTextView.context
val iconColor = ThemeUtils.getColor(context, android.R.attr.textColorTertiary)
val icon = IconicsDrawable(context, GoogleMaterial.Icon.gmd_list).apply { sizeDp = 20; colorInt = iconColor }
nameTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(icon, null, null, null)

View File

@ -52,6 +52,7 @@ import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.target.FixedSizeDrawable
import com.bumptech.glide.request.transition.Transition
import com.google.android.material.color.MaterialColors
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayout.OnTabSelectedListener
import com.google.android.material.tabs.TabLayoutMediator
@ -84,9 +85,9 @@ import com.keylesspalace.tusky.interfaces.ReselectableFragment
import com.keylesspalace.tusky.pager.MainPagerAdapter
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.usecase.LogoutUsecase
import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.deleteStaleCachedMedia
import com.keylesspalace.tusky.util.emojify
import com.keylesspalace.tusky.util.getDimension
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.updateShortcut
@ -241,7 +242,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM)
icon = IconicsDrawable(this@MainActivity, GoogleMaterial.Icon.gmd_search).apply {
sizeDp = 20
colorInt = ThemeUtils.getColor(this@MainActivity, android.R.attr.textColorPrimary)
colorInt = MaterialColors.getColor(binding.mainToolbar, android.R.attr.textColorPrimary)
}
setOnMenuItemClickListener {
startActivity(SearchActivity.getIntent(this@MainActivity))
@ -409,7 +410,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
}
header.accountHeaderBackground.setColorFilter(getColor(R.color.headerBackgroundFilter))
header.accountHeaderBackground.setBackgroundColor(ThemeUtils.getColor(this, R.attr.colorBackgroundAccent))
header.accountHeaderBackground.setBackgroundColor(MaterialColors.getColor(header, R.attr.colorBackgroundAccent))
val animateAvatars = preferences.getBoolean("animateGifAvatars", false)
DrawerImageLoader.init(object : AbstractDrawerImageLoader() {
@ -505,8 +506,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
startActivityWithSlideInAnimation(AnnouncementsActivity.newIntent(context))
}
badgeStyle = BadgeStyle().apply {
textColor = ColorHolder.fromColor(ThemeUtils.getColor(this@MainActivity, R.attr.colorOnPrimary))
color = ColorHolder.fromColor(ThemeUtils.getColor(this@MainActivity, R.attr.colorPrimary))
textColor = ColorHolder.fromColor(MaterialColors.getColor(binding.mainDrawer, R.attr.colorOnPrimary))
color = ColorHolder.fromColor(MaterialColors.getColor(binding.mainDrawer, R.attr.colorPrimary))
}
},
DividerDrawerItem(),
@ -573,9 +574,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
}
private fun setupTabs(selectNotificationTab: Boolean) {
val activeTabLayout = if (preferences.getString("mainNavPosition", "top") == "bottom") {
val actionBarSize = ThemeUtils.getDimension(this, R.attr.actionBarSize)
val actionBarSize = getDimension(this, R.attr.actionBarSize)
val fabMargin = resources.getDimensionPixelSize(R.dimen.fabMargin)
(binding.composeButton.layoutParams as CoordinatorLayout.LayoutParams).bottomMargin = actionBarSize + fabMargin
binding.topNav.hide()

View File

@ -22,8 +22,9 @@ import androidx.work.WorkManager
import autodispose2.AutoDisposePlugins
import com.keylesspalace.tusky.components.notifications.NotificationWorkerFactory
import com.keylesspalace.tusky.di.AppInjector
import com.keylesspalace.tusky.util.APP_THEME_DEFAULT
import com.keylesspalace.tusky.util.LocaleManager
import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.setAppNightMode
import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector
import de.c1710.filemojicompat_defaults.DefaultEmojiPackList
@ -72,8 +73,8 @@ class TuskyApplication : Application(), HasAndroidInjector {
EmojiPackHelper.init(this, DefaultEmojiPackList.get(this), allowPackImports = false)
// init night mode
val theme = preferences.getString("appTheme", ThemeUtils.APP_THEME_DEFAULT)
ThemeUtils.setAppNightMode(theme)
val theme = preferences.getString("appTheme", APP_THEME_DEFAULT)
setAppNightMode(theme)
localeManager.setLocale()

View File

@ -21,7 +21,7 @@ import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.TextView
import com.keylesspalace.tusky.util.ThemeUtils
import com.google.android.material.color.MaterialColors
import com.keylesspalace.tusky.util.getTuskyDisplayName
import com.keylesspalace.tusky.util.modernLanguageCode
import java.util.Locale
@ -29,7 +29,7 @@ import java.util.Locale
class LocaleAdapter(context: Context, resource: Int, locales: List<Locale>) : ArrayAdapter<Locale>(context, resource, locales) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
return (super.getView(position, convertView, parent) as TextView).apply {
setTextColor(ThemeUtils.getColor(context, android.R.attr.textColorTertiary))
setTextColor(MaterialColors.getColor(this, android.R.attr.textColorTertiary))
typeface = Typeface.DEFAULT_BOLD
text = super.getItem(position)?.modernLanguageCode?.uppercase()
}
@ -37,7 +37,7 @@ class LocaleAdapter(context: Context, resource: Int, locales: List<Locale>) : Ar
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
return (super.getDropDownView(position, convertView, parent) as TextView).apply {
setTextColor(ThemeUtils.getColor(context, android.R.attr.textColorTertiary))
setTextColor(MaterialColors.getColor(this, android.R.attr.textColorTertiary))
text = super.getItem(position)?.getTuskyDisplayName(context)
}
}

View File

@ -24,8 +24,8 @@ import com.keylesspalace.tusky.adapter.NotificationsAdapter.NotificationActionLi
import com.keylesspalace.tusky.databinding.ItemReportNotificationBinding
import com.keylesspalace.tusky.entity.Report
import com.keylesspalace.tusky.entity.TimelineAccount
import com.keylesspalace.tusky.util.TimestampUtils
import com.keylesspalace.tusky.util.emojify
import com.keylesspalace.tusky.util.getRelativeTimeSpanString
import com.keylesspalace.tusky.util.loadAvatar
import com.keylesspalace.tusky.util.unicodeWrap
import java.util.Date
@ -41,7 +41,7 @@ class ReportNotificationViewHolder(
binding.notificationTopText.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null)
binding.notificationTopText.text = itemView.context.getString(R.string.notification_header_report_format, reporterName, reporteeName)
binding.notificationSummary.text = itemView.context.getString(R.string.notification_summary_report_format, TimestampUtils.getRelativeTimeSpanString(itemView.context, report.createdAt.time, Date().time), report.status_ids?.size ?: 0)
binding.notificationSummary.text = itemView.context.getString(R.string.notification_summary_report_format, getRelativeTimeSpanString(itemView.context, report.createdAt.time, Date().time), report.status_ids?.size ?: 0)
binding.notificationCategory.text = getTranslatedCategory(itemView.context, report.category)
// Fancy avatar inset

View File

@ -33,6 +33,7 @@ import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.RequestBuilder;
import com.google.android.material.button.MaterialButton;
import com.google.android.material.color.MaterialColors;
import com.google.android.material.imageview.ShapeableImageView;
import com.google.android.material.shape.CornerFamily;
import com.google.android.material.shape.ShapeAppearanceModel;
@ -53,7 +54,6 @@ import com.keylesspalace.tusky.util.CustomEmojiHelper;
import com.keylesspalace.tusky.util.ImageLoadingHelper;
import com.keylesspalace.tusky.util.LinkHelper;
import com.keylesspalace.tusky.util.StatusDisplayOptions;
import com.keylesspalace.tusky.util.ThemeUtils;
import com.keylesspalace.tusky.util.TimestampUtils;
import com.keylesspalace.tusky.util.TouchDelegateHelper;
import com.keylesspalace.tusky.view.MediaPreviewImageView;
@ -170,7 +170,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
this.avatarRadius36dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_36dp);
this.avatarRadius24dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_24dp);
mediaPreviewUnloaded = new ColorDrawable(ThemeUtils.getColor(itemView.getContext(), R.attr.colorBackgroundAccent));
mediaPreviewUnloaded = new ColorDrawable(MaterialColors.getColor(itemView, R.attr.colorBackgroundAccent));
TouchDelegateHelper.expandTouchSizeToFillRow((ViewGroup) itemView, CollectionsKt.listOfNotNull(replyButton, reblogButton, favouriteButton, bookmarkButton, moreButton));
}

View File

@ -23,6 +23,7 @@ import androidx.core.view.size
import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
import com.google.android.material.chip.Chip
import com.google.android.material.color.MaterialColors
import com.keylesspalace.tusky.HASHTAG
import com.keylesspalace.tusky.LIST
import com.keylesspalace.tusky.R
@ -30,8 +31,8 @@ import com.keylesspalace.tusky.TabData
import com.keylesspalace.tusky.databinding.ItemTabPreferenceBinding
import com.keylesspalace.tusky.databinding.ItemTabPreferenceSmallBinding
import com.keylesspalace.tusky.util.BindingHolder
import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.setDrawableTint
import com.keylesspalace.tusky.util.show
interface ItemInteractionListener {
@ -101,7 +102,7 @@ class TabAdapter(
listener.onTabRemoved(holder.bindingAdapterPosition)
}
binding.removeButton.isEnabled = removeButtonEnabled
ThemeUtils.setDrawableTint(
setDrawableTint(
holder.itemView.context,
binding.removeButton.drawable,
(if (removeButtonEnabled) android.R.attr.textColorTertiary else R.attr.textColorDisabled)
@ -120,7 +121,7 @@ class TabAdapter(
val chip = binding.chipGroup.getChildAt(i).takeUnless { it.id == R.id.actionChip } as Chip?
?: Chip(context).apply {
binding.chipGroup.addView(this, binding.chipGroup.size - 1)
chipIconTint = ColorStateList.valueOf(ThemeUtils.getColor(context, android.R.attr.textColorPrimary))
chipIconTint = ColorStateList.valueOf(MaterialColors.getColor(this, android.R.attr.textColorPrimary))
}
chip.text = arg

View File

@ -41,6 +41,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.viewpager2.widget.MarginPageTransformer
import com.bumptech.glide.Glide
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.color.MaterialColors
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.shape.MaterialShapeDrawable
import com.google.android.material.shape.ShapeAppearanceModel
@ -70,7 +71,6 @@ import com.keylesspalace.tusky.util.DefaultTextWatcher
import com.keylesspalace.tusky.util.Error
import com.keylesspalace.tusky.util.Loading
import com.keylesspalace.tusky.util.Success
import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.emojify
import com.keylesspalace.tusky.util.getDomain
import com.keylesspalace.tusky.util.hide
@ -172,9 +172,9 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
* Load colors and dimensions from resources
*/
private fun loadResources() {
toolbarColor = ThemeUtils.getColor(this, R.attr.colorSurface)
toolbarColor = MaterialColors.getColor(this, R.attr.colorSurface, Color.BLACK)
statusBarColorTransparent = getColor(R.color.transparent_statusbar_background)
statusBarColorOpaque = ThemeUtils.getColor(this, R.attr.colorPrimaryDark)
statusBarColorOpaque = MaterialColors.getColor(this, R.attr.colorPrimaryDark, Color.BLACK)
avatarSize = resources.getDimension(R.dimen.account_activity_avatar_size)
titleVisibleHeight = resources.getDimensionPixelSize(R.dimen.account_activity_scroll_title_visible_height)
}

View File

@ -11,11 +11,11 @@ import androidx.core.view.setPadding
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import com.bumptech.glide.Glide
import com.google.android.material.color.MaterialColors
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.databinding.ItemAccountMediaBinding
import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.util.BindingHolder
import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.decodeBlurHash
import com.keylesspalace.tusky.util.getFormattedDescription
import com.keylesspalace.tusky.util.hide
@ -40,7 +40,7 @@ class AccountMediaGridAdapter(
}
) {
private val baseItemBackgroundColor = ThemeUtils.getColor(context, R.attr.colorSurface)
private val baseItemBackgroundColor = MaterialColors.getColor(context, R.attr.colorSurface, Color.BLACK)
private val videoIndicator = AppCompatResources.getDrawable(context, R.drawable.ic_play_indicator)
private val mediaHiddenDrawable = AppCompatResources.getDrawable(context, R.drawable.ic_hide_media_24dp)

View File

@ -63,6 +63,7 @@ import com.canhub.cropper.CropImage
import com.canhub.cropper.CropImageContract
import com.canhub.cropper.options
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.color.MaterialColors
import com.google.android.material.snackbar.Snackbar
import com.keylesspalace.tusky.BaseActivity
import com.keylesspalace.tusky.BuildConfig
@ -86,8 +87,8 @@ import com.keylesspalace.tusky.entity.Emoji
import com.keylesspalace.tusky.entity.NewPoll
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.APP_THEME_DEFAULT
import com.keylesspalace.tusky.util.PickMediaFiles
import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.afterTextChanged
import com.keylesspalace.tusky.util.getInitialLanguage
import com.keylesspalace.tusky.util.getLocaleList
@ -97,6 +98,7 @@ import com.keylesspalace.tusky.util.highlightSpans
import com.keylesspalace.tusky.util.loadAvatar
import com.keylesspalace.tusky.util.modernLanguageCode
import com.keylesspalace.tusky.util.onTextChanged
import com.keylesspalace.tusky.util.setDrawableTint
import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.util.visible
@ -206,7 +208,7 @@ class ComposeActivity :
accountManager.setActiveAccount(accountId)
}
val theme = preferences.getString("appTheme", ThemeUtils.APP_THEME_DEFAULT)
val theme = preferences.getString("appTheme", APP_THEME_DEFAULT)
if (theme == "black") {
setTheme(R.style.TuskyDialogActivityBlackTheme)
}
@ -341,7 +343,7 @@ class ComposeActivity :
binding.composeReplyView.text = getString(R.string.replying_to, replyingStatusAuthor)
val arrowDownIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_arrow_drop_down).apply { sizeDp = 12 }
ThemeUtils.setDrawableTint(this, arrowDownIcon, android.R.attr.textColorTertiary)
setDrawableTint(this, arrowDownIcon, android.R.attr.textColorTertiary)
binding.composeReplyView.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, arrowDownIcon, null)
binding.composeReplyView.setOnClickListener {
@ -354,7 +356,7 @@ class ComposeActivity :
binding.composeReplyContentView.show()
val arrowUpIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_arrow_drop_up).apply { sizeDp = 12 }
ThemeUtils.setDrawableTint(this, arrowUpIcon, android.R.attr.textColorTertiary)
setDrawableTint(this, arrowUpIcon, android.R.attr.textColorTertiary)
binding.composeReplyView.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, arrowUpIcon, null)
}
}
@ -501,7 +503,7 @@ class ComposeActivity :
displayTransientMessage(R.string.hint_media_description_missing)
}
val textColor = ThemeUtils.getColor(this, android.R.attr.textColorTertiary)
val textColor = MaterialColors.getColor(binding.root, android.R.attr.textColorTertiary)
val cameraIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_camera_alt).apply { colorInt = textColor; sizeDp = 18 }
binding.actionPhotoTake.setCompoundDrawablesRelativeWithIntrinsicBounds(cameraIcon, null, null, null)
@ -688,7 +690,7 @@ class ComposeActivity :
getColor(R.color.tusky_blue)
} else {
binding.composeHideMediaButton.setImageResource(R.drawable.ic_eye_24dp)
ThemeUtils.getColor(this, android.R.attr.textColorTertiary)
MaterialColors.getColor(binding.composeHideMediaButton, android.R.attr.textColorTertiary)
}
}
binding.composeHideMediaButton.drawable.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)
@ -710,7 +712,7 @@ class ComposeActivity :
enableButton(binding.composeScheduleButton, clickable = false, colorActive = false)
} else {
@ColorInt val color = if (binding.composeScheduleView.time == null) {
ThemeUtils.getColor(this, android.R.attr.textColorTertiary)
MaterialColors.getColor(binding.composeScheduleButton, android.R.attr.textColorTertiary)
} else {
getColor(R.color.tusky_blue)
}
@ -906,7 +908,7 @@ class ComposeActivity :
val textColor = if (remainingLength < 0) {
getColor(R.color.tusky_red)
} else {
ThemeUtils.getColor(this, android.R.attr.textColorTertiary)
MaterialColors.getColor(binding.composeCharactersLeftView, android.R.attr.textColorTertiary)
}
binding.composeCharactersLeftView.setTextColor(textColor)
}
@ -1007,7 +1009,7 @@ class ComposeActivity :
private fun enableButton(button: ImageButton, clickable: Boolean, colorActive: Boolean) {
button.isEnabled = clickable
ThemeUtils.setDrawableTint(
setDrawableTint(
this, button.drawable,
if (colorActive) android.R.attr.textColorTertiary
else R.attr.textColorDisabled
@ -1016,8 +1018,8 @@ class ComposeActivity :
private fun enablePollButton(enable: Boolean) {
binding.addPollTextActionTextView.isEnabled = enable
val textColor = ThemeUtils.getColor(
this,
val textColor = MaterialColors.getColor(
binding.addPollTextActionTextView,
if (enable) android.R.attr.textColorTertiary
else R.attr.textColorDisabled
)
@ -1077,7 +1079,7 @@ class ComposeActivity :
} else {
binding.composeContentWarningBar.hide()
binding.composeEditField.requestFocus()
ThemeUtils.getColor(this, android.R.attr.textColorTertiary)
MaterialColors.getColor(binding.composeContentWarningButton, android.R.attr.textColorTertiary)
}
binding.composeContentWarningButton.drawable.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)
}

View File

@ -16,11 +16,13 @@
package com.keylesspalace.tusky.components.preference
import android.content.Intent
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.util.Log
import androidx.annotation.DrawableRes
import androidx.preference.PreferenceFragmentCompat
import com.google.android.material.color.MaterialColors
import com.google.android.material.snackbar.Snackbar
import com.keylesspalace.tusky.AccountListActivity
import com.keylesspalace.tusky.BaseActivity
@ -47,7 +49,6 @@ import com.keylesspalace.tusky.settings.makePreferenceScreen
import com.keylesspalace.tusky.settings.preference
import com.keylesspalace.tusky.settings.preferenceCategory
import com.keylesspalace.tusky.settings.switchPreference
import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.getInitialLanguage
import com.keylesspalace.tusky.util.getLocaleList
import com.keylesspalace.tusky.util.getTuskyDisplayName
@ -80,7 +81,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
setTitle(R.string.pref_title_edit_notification_settings)
icon = IconicsDrawable(context, GoogleMaterial.Icon.gmd_notifications).apply {
sizeRes = R.dimen.preference_icon_size
colorInt = ThemeUtils.getColor(context, R.attr.iconColor)
colorInt = MaterialColors.getColor(context, R.attr.iconColor, Color.BLACK)
}
setOnPreferenceClickListener {
openNotificationPrefs()
@ -135,7 +136,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
setTitle(R.string.action_view_blocks)
icon = IconicsDrawable(context, GoogleMaterial.Icon.gmd_block).apply {
sizeRes = R.dimen.preference_icon_size
colorInt = ThemeUtils.getColor(context, R.attr.iconColor)
colorInt = MaterialColors.getColor(context, R.attr.iconColor, Color.BLACK)
}
setOnPreferenceClickListener {
val intent = Intent(context, AccountListActivity::class.java)

View File

@ -31,8 +31,9 @@ import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
import com.keylesspalace.tusky.databinding.ActivityPreferencesBinding
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.APP_THEME_DEFAULT
import com.keylesspalace.tusky.util.getNonNullString
import com.keylesspalace.tusky.util.setAppNightMode
import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector
import javax.inject.Inject
@ -124,9 +125,9 @@ class PreferencesActivity :
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
when (key) {
"appTheme" -> {
val theme = sharedPreferences.getNonNullString("appTheme", ThemeUtils.APP_THEME_DEFAULT)
val theme = sharedPreferences.getNonNullString("appTheme", APP_THEME_DEFAULT)
Log.d("activeTheme", theme)
ThemeUtils.setAppNightMode(theme)
setAppNightMode(theme)
restartActivitiesOnBackPressedCallback.isEnabled = true
this.restartCurrentActivity()

View File

@ -31,8 +31,8 @@ import com.keylesspalace.tusky.util.StatusDisplayOptions
import com.keylesspalace.tusky.util.StatusViewHelper
import com.keylesspalace.tusky.util.StatusViewHelper.Companion.COLLAPSE_INPUT_FILTER
import com.keylesspalace.tusky.util.StatusViewHelper.Companion.NO_INPUT_FILTER
import com.keylesspalace.tusky.util.TimestampUtils
import com.keylesspalace.tusky.util.emojify
import com.keylesspalace.tusky.util.getRelativeTimeSpanString
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.setClickableMentions
import com.keylesspalace.tusky.util.setClickableText
@ -161,7 +161,7 @@ class StatusViewHolder(
binding.timestampInfo.text = if (createdAt != null) {
val then = createdAt.time
val now = System.currentTimeMillis()
TimestampUtils.getRelativeTimeSpanString(binding.timestampInfo.context, then, now)
getRelativeTimeSpanString(binding.timestampInfo.context, then, now)
} else {
// unknown minutes~
"?m"

View File

@ -915,11 +915,11 @@ public class NotificationsFragment extends SFragment implements
private void onFetchNotificationsSuccess(List<Notification> notifications, String linkHeader,
FetchEnd fetchEnd, int pos) {
List<HttpHeaderLink> links = HttpHeaderLink.parse(linkHeader);
HttpHeaderLink next = HttpHeaderLink.findByRelationType(links, "next");
List<HttpHeaderLink> links = HttpHeaderLink.Companion.parse(linkHeader);
HttpHeaderLink next = HttpHeaderLink.Companion.findByRelationType(links, "next");
String fromId = null;
if (next != null) {
fromId = next.uri.getQueryParameter("max_id");
fromId = next.getUri().getQueryParameter("max_id");
}
switch (fetchEnd) {

View File

@ -1,162 +0,0 @@
/* Written in 2017 by Andrew Dawson
*
* To the extent possible under law, the author(s) have dedicated all copyright and related and
* neighboring rights to this software to the public domain worldwide. This software is distributed
* without any warranty.
*
* You should have received a copy of the CC0 Public Domain Dedication along with this software.
* If not, see <http://creativecommons.org/publicdomain/zero/1.0/>. */
package com.keylesspalace.tusky.util;
import android.net.Uri;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
/**
* Represents one link and its parameters from the link header of an HTTP message.
*
* @see <a href="https://tools.ietf.org/html/rfc5988">RFC5988</a>
*/
public class HttpHeaderLink {
private static class Parameter {
public String name;
public String value;
}
private List<Parameter> parameters;
public Uri uri;
private HttpHeaderLink(String uri) {
this.uri = Uri.parse(uri);
this.parameters = new ArrayList<>();
}
private static int findAny(String s, int fromIndex, char[] set) {
for (int i = fromIndex; i < s.length(); i++) {
char c = s.charAt(i);
for (char member : set) {
if (c == member) {
return i;
}
}
}
return -1;
}
private static int findEndOfQuotedString(String line, int start) {
for (int i = start; i < line.length(); i++) {
char c = line.charAt(i);
if (c == '\\') {
i += 1;
} else if (c == '"') {
return i;
}
}
return -1;
}
private static class ValueResult {
String value;
int end;
ValueResult() {
end = -1;
}
void setValue(String value) {
value = value.trim();
if (!value.isEmpty()) {
this.value = value;
}
}
}
private static ValueResult parseValue(String line, int start) {
ValueResult result = new ValueResult();
int foundIndex = findAny(line, start, new char[] {';', ',', '"'});
if (foundIndex == -1) {
result.setValue(line.substring(start));
return result;
}
char c = line.charAt(foundIndex);
if (c == ';' || c == ',') {
result.end = foundIndex;
result.setValue(line.substring(start, foundIndex));
return result;
} else {
int quoteEnd = findEndOfQuotedString(line, foundIndex + 1);
if (quoteEnd == -1) {
quoteEnd = line.length();
}
result.end = quoteEnd;
result.setValue(line.substring(foundIndex + 1, quoteEnd));
return result;
}
}
private static int parseParameters(String line, int start, HttpHeaderLink link) {
for (int i = start; i < line.length(); i++) {
int foundIndex = findAny(line, i, new char[] {'=', ','});
if (foundIndex == -1) {
return -1;
} else if (line.charAt(foundIndex) == ',') {
return foundIndex;
}
Parameter parameter = new Parameter();
parameter.name = line.substring(line.indexOf(';', i) + 1, foundIndex).trim();
link.parameters.add(parameter);
ValueResult result = parseValue(line, foundIndex);
parameter.value = result.value;
if (result.end == -1) {
return -1;
} else {
i = result.end;
}
}
return -1;
}
/**
* @param line the entire link header, not including the initial "Link:"
* @return all links found in the header
*/
public static List<HttpHeaderLink> parse(@Nullable String line) {
List<HttpHeaderLink> linkList = new ArrayList<>();
if (line != null) {
for (int i = 0; i < line.length(); i++) {
int uriEnd = line.indexOf('>', i);
String uri = line.substring(line.indexOf('<', i) + 1, uriEnd);
HttpHeaderLink link = new HttpHeaderLink(uri);
linkList.add(link);
int parseEnd = parseParameters(line, uriEnd, link);
if (parseEnd == -1) {
break;
} else {
i = parseEnd;
}
}
}
return linkList;
}
/**
* @param links intended to be those returned by parse()
* @param relationType of the parameter "rel", commonly "next" or "prev"
* @return the link matching the given relation type
*/
@Nullable
public static HttpHeaderLink findByRelationType(List<HttpHeaderLink> links,
String relationType) {
for (HttpHeaderLink link : links) {
for (Parameter parameter : link.parameters) {
if (parameter.name.equals("rel") && parameter.value.equals(relationType)) {
return link;
}
}
}
return null;
}
}

View File

@ -0,0 +1,135 @@
/* Copyright 2022 Tusky Contributors
*
* This file is a part of Tusky.
*
* 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.
*
* Tusky 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 Tusky; if not,
* see <http://www.gnu.org/licenses>.
*/
package com.keylesspalace.tusky.util
import android.net.Uri
import androidx.annotation.VisibleForTesting
import androidx.core.net.toUri
/**
* Represents one link and its parameters from the link header of an HTTP message.
*
* @see [RFC5988](https://tools.ietf.org/html/rfc5988)
*/
class HttpHeaderLink @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) constructor(
uri: String
) {
data class Parameter(val name: String, val value: String?)
private val parameters: MutableList<Parameter> = ArrayList()
val uri: Uri = uri.toUri()
private data class ValueResult(val value: String, val end: Int = -1)
companion object {
private fun findEndOfQuotedString(line: String, start: Int): Int {
var i = start
while (i < line.length) {
val c = line[i]
if (c == '\\') {
i += 1
} else if (c == '"') {
return i
}
i++
}
return -1
}
private fun parseValue(line: String, start: Int): ValueResult {
val foundIndex = line.indexOfAny(charArrayOf(';', ',', '"'), start, false)
if (foundIndex == -1) {
return ValueResult(line.substring(start).trim())
}
val c = line[foundIndex]
return if (c == ';' || c == ',') {
ValueResult(line.substring(start, foundIndex).trim(), foundIndex)
} else {
var quoteEnd = findEndOfQuotedString(line, foundIndex + 1)
if (quoteEnd == -1) {
quoteEnd = line.length
}
ValueResult(line.substring(foundIndex + 1, quoteEnd).trim(), quoteEnd)
}
}
private fun parseParameters(line: String, start: Int, link: HttpHeaderLink): Int {
var i = start
while (i < line.length) {
val foundIndex = line.indexOfAny(charArrayOf('=', ','), i, false)
if (foundIndex == -1) {
return -1
} else if (line[foundIndex] == ',') {
return foundIndex
}
val name = line.substring(line.indexOf(';', i) + 1, foundIndex).trim()
val result = parseValue(line, foundIndex)
val value = result.value
val parameter = Parameter(name, value)
link.parameters.add(parameter)
i = if (result.end == -1) {
return -1
} else {
result.end
}
}
return -1
}
/**
* @param line the entire link header, not including the initial "Link:"
* @return all links found in the header
*/
fun parse(line: String?): List<HttpHeaderLink> {
val links: MutableList<HttpHeaderLink> = mutableListOf()
line ?: return links
var i = 0
while (i < line.length) {
val uriEnd = line.indexOf('>', i)
val uri = line.substring(line.indexOf('<', i) + 1, uriEnd)
val link = HttpHeaderLink(uri)
links.add(link)
val parseEnd = parseParameters(line, uriEnd, link)
i = if (parseEnd == -1) {
break
} else {
parseEnd
}
i++
}
return links
}
/**
* @param links intended to be those returned by parse()
* @param relationType of the parameter "rel", commonly "next" or "prev"
* @return the link matching the given relation type
*/
fun findByRelationType(
links: List<HttpHeaderLink>,
relationType: String
): HttpHeaderLink? {
return links.find { link ->
link.parameters.any { parameter ->
parameter.name == "rel" && parameter.value == relationType
}
}
}
}
}

View File

@ -16,7 +16,9 @@
package com.keylesspalace.tusky.util
import android.content.Context
import android.graphics.Color
import androidx.annotation.Px
import com.google.android.material.color.MaterialColors
import com.keylesspalace.tusky.R
import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
@ -26,6 +28,6 @@ import com.mikepenz.iconics.utils.sizePx
fun makeIcon(context: Context, icon: GoogleMaterial.Icon, @Px iconSize: Int): IconicsDrawable {
return IconicsDrawable(context, icon).apply {
sizePx = iconSize
colorInt = ThemeUtils.getColor(context, R.attr.iconColor)
colorInt = MaterialColors.getColor(context, R.attr.iconColor, Color.BLACK)
}
}

View File

@ -19,6 +19,7 @@ package com.keylesspalace.tusky.util
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.net.Uri
import android.text.SpannableStringBuilder
import android.text.Spanned
@ -33,6 +34,7 @@ import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.net.toUri
import androidx.preference.PreferenceManager
import com.google.android.material.color.MaterialColors
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.entity.HashTag
import com.keylesspalace.tusky.entity.Status.Mention
@ -251,9 +253,9 @@ private fun openLinkInBrowser(uri: Uri?, context: Context) {
* @param context context
*/
private fun openLinkInCustomTab(uri: Uri, context: Context) {
val toolbarColor = ThemeUtils.getColor(context, R.attr.colorSurface)
val navigationbarColor = ThemeUtils.getColor(context, android.R.attr.navigationBarColor)
val navigationbarDividerColor = ThemeUtils.getColor(context, R.attr.dividerColor)
val toolbarColor = MaterialColors.getColor(context, R.attr.colorSurface, Color.BLACK)
val navigationbarColor = MaterialColors.getColor(context, android.R.attr.navigationBarColor, Color.BLACK)
val navigationbarDividerColor = MaterialColors.getColor(context, R.attr.dividerColor, Color.BLACK)
val colorSchemeParams = CustomTabColorSchemeParams.Builder()
.setToolbarColor(toolbarColor)
.setNavigationBarColor(navigationbarColor)

View File

@ -1,94 +0,0 @@
package com.keylesspalace.tusky.util;
import androidx.annotation.Nullable;
import androidx.arch.core.util.Function;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.List;
/**
* This list implementation can help to keep two lists in sync - like real models and view models.
* Every operation on the main list triggers update of the supplementary list (but not vice versa).
* This makes sure that the main list is always the source of truth.
* Main list is projected to the supplementary list by the passed mapper function.
* Paired list is newer actually exposed and clients are provided with {@code getPairedCopy()},
* {@code getPairedItem()} and {@code setPairedItem()}. This prevents modifications of the
* supplementary list size so lists are always have the same length.
* This implementation will not try to recover from exceptional cases so lists may be out of sync
* after the exception.
*
* It is most useful with immutable data because we cannot track changes inside stored objects.
* @param <T> type of elements in the main list
* @param <V> type of elements in supplementary list
*/
public final class PairedList<T, V> extends AbstractList<T> {
private final List<T> main = new ArrayList<>();
private final List<V> synced = new ArrayList<>();
private final Function<T, ? extends V> mapper;
/**
* Construct new paired list. Main and supplementary lists will be empty.
* @param mapper Function, which will be used to translate items from the main list to the
* supplementary one.
*/
public PairedList(Function<T, ? extends V> mapper) {
this.mapper = mapper;
}
public List<V> getPairedCopy() {
return new ArrayList<>(synced);
}
public V getPairedItem(int index) {
return synced.get(index);
}
@Nullable
public V getPairedItemOrNull(int index) {
if (index >= 0 && index < synced.size()) {
return synced.get(index);
} else {
return null;
}
}
public void setPairedItem(int index, V element) {
synced.set(index, element);
}
@Override
public T get(int index) {
return main.get(index);
}
@Override
public T set(int index, T element) {
synced.set(index, mapper.apply(element));
return main.set(index, element);
}
@Override
public boolean add(T t) {
synced.add(mapper.apply(t));
return main.add(t);
}
@Override
public void add(int index, T element) {
synced.add(index, mapper.apply(element));
main.add(index, element);
}
@Override
public T remove(int index) {
synced.remove(index);
return main.remove(index);
}
@Override
public int size() {
return main.size();
}
}

View File

@ -0,0 +1,74 @@
package com.keylesspalace.tusky.util
import androidx.arch.core.util.Function
/**
* This list implementation can help to keep two lists in sync - like real models and view models.
*
* Every operation on the main list triggers update of the supplementary list (but not vice versa).
*
* This makes sure that the main list is always the source of truth.
*
* Main list is projected to the supplementary list by the passed mapper function.
*
* Paired list is newer actually exposed and clients are provided with `getPairedCopy()`,
* `getPairedItem()` and `setPairedItem()`. This prevents modifications of the
* supplementary list size so lists are always have the same length.
*
* This implementation will not try to recover from exceptional cases so lists may be out of sync
* after the exception.
*
* It is most useful with immutable data because we cannot track changes inside stored objects.
*
* @param T type of elements in the main list
* @param V type of elements in supplementary list
* @param mapper Function, which will be used to translate items from the main list to the
* supplementary one.
* @constructor
*/
class PairedList<T, V> (private val mapper: Function<T, out V>) : AbstractMutableList<T>() {
private val main: MutableList<T> = ArrayList()
private val synced: MutableList<V> = ArrayList()
val pairedCopy: List<V>
get() = ArrayList(synced)
fun getPairedItem(index: Int): V {
return synced[index]
}
fun getPairedItemOrNull(index: Int): V? {
return synced.getOrNull(index)
}
fun setPairedItem(index: Int, element: V) {
synced[index] = element
}
override fun get(index: Int): T {
return main[index]
}
override fun set(index: Int, element: T): T {
synced[index] = mapper.apply(element)
return main.set(index, element)
}
override fun add(element: T): Boolean {
synced.add(mapper.apply(element))
return main.add(element)
}
override fun add(index: Int, element: T) {
synced.add(index, mapper.apply(element))
main.add(index, element)
}
override fun removeAt(index: Int): T {
synced.removeAt(index)
return main.removeAt(index)
}
override val size: Int
get() = main.size
}

View File

@ -16,6 +16,7 @@
package com.keylesspalace.tusky.util
import android.content.Context
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.text.InputFilter
import android.text.TextUtils
@ -24,6 +25,7 @@ import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.DrawableRes
import com.bumptech.glide.Glide
import com.google.android.material.color.MaterialColors
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.entity.Emoji
@ -85,7 +87,7 @@ class StatusViewHelper(private val itemView: View) {
return
}
val mediaPreviewUnloaded = ColorDrawable(ThemeUtils.getColor(context, R.attr.colorBackgroundAccent))
val mediaPreviewUnloaded = ColorDrawable(MaterialColors.getColor(context, R.attr.colorBackgroundAccent, Color.BLACK))
val n = min(attachments.size, Status.MAX_MEDIA_ATTACHMENTS)
@ -292,7 +294,7 @@ class StatusViewHelper(private val itemView: View) {
if (useAbsoluteTime) {
context.getString(R.string.poll_info_time_absolute, absoluteTimeFormatter.format(poll.expiresAt, false))
} else {
TimestampUtils.formatPollDuration(context, poll.expiresAt!!.time, timestamp)
formatPollDuration(context, poll.expiresAt!!.time, timestamp)
}
}

View File

@ -1,83 +0,0 @@
/* Copyright 2017 Andrew Dawson
*
* This file is a part of Tusky.
*
* 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.
*
* Tusky 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 Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.util;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.util.TypedValue;
import androidx.annotation.AttrRes;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatDelegate;
/**
* Provides runtime compatibility to obtain theme information and re-theme views, especially where
* the ability to do so is not supported in resource files.
*/
public class ThemeUtils {
public static final String APP_THEME_DEFAULT = ThemeUtils.THEME_NIGHT;
private static final String THEME_NIGHT = "night";
private static final String THEME_DAY = "day";
private static final String THEME_BLACK = "black";
private static final String THEME_AUTO = "auto";
private static final String THEME_SYSTEM = "auto_system";
@ColorInt
public static int getColor(@NonNull Context context, @AttrRes int attribute) {
TypedValue value = new TypedValue();
if (context.getTheme().resolveAttribute(attribute, value, true)) {
return value.data;
} else {
return Color.BLACK;
}
}
public static int getDimension(@NonNull Context context, @AttrRes int attribute) {
TypedArray array = context.obtainStyledAttributes(new int[] { attribute });
int dimen = array.getDimensionPixelSize(0, -1);
array.recycle();
return dimen;
}
public static void setDrawableTint(Context context, Drawable drawable, @AttrRes int attribute) {
drawable.setColorFilter(getColor(context, attribute), PorterDuff.Mode.SRC_IN);
}
public static void setAppNightMode(String flavor) {
switch (flavor) {
default:
case THEME_NIGHT:
case THEME_BLACK:
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
break;
case THEME_DAY:
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
break;
case THEME_AUTO:
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO_TIME);
break;
case THEME_SYSTEM:
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
break;
}
}
}

View File

@ -0,0 +1,67 @@
/* Copyright 2017 Andrew Dawson
*
* This file is a part of Tusky.
*
* 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.
*
* Tusky 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 Tusky; if not,
* see <http://www.gnu.org/licenses>. */
@file:JvmName("ThemeUtils")
package com.keylesspalace.tusky.util
import android.content.Context
import android.graphics.Color
import android.graphics.PorterDuff
import android.graphics.drawable.Drawable
import androidx.annotation.AttrRes
import androidx.appcompat.app.AppCompatDelegate
import com.google.android.material.color.MaterialColors
/**
* Provides runtime compatibility to obtain theme information and re-theme views, especially where
* the ability to do so is not supported in resource files.
*/
private const val THEME_NIGHT = "night"
private const val THEME_DAY = "day"
private const val THEME_BLACK = "black"
private const val THEME_AUTO = "auto"
private const val THEME_SYSTEM = "auto_system"
const val APP_THEME_DEFAULT = THEME_NIGHT
fun getDimension(context: Context, @AttrRes attribute: Int): Int {
val array = context.obtainStyledAttributes(intArrayOf(attribute))
val dimen = array.getDimensionPixelSize(0, -1)
array.recycle()
return dimen
}
fun setDrawableTint(context: Context, drawable: Drawable, @AttrRes attribute: Int) {
drawable.setColorFilter(
MaterialColors.getColor(context, attribute, Color.BLACK),
PorterDuff.Mode.SRC_IN
)
}
fun setAppNightMode(flavor: String?) {
when (flavor) {
THEME_NIGHT, THEME_BLACK -> AppCompatDelegate.setDefaultNightMode(
AppCompatDelegate.MODE_NIGHT_YES
)
THEME_DAY -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
THEME_AUTO -> AppCompatDelegate.setDefaultNightMode(
AppCompatDelegate.MODE_NIGHT_AUTO_TIME
)
THEME_SYSTEM -> AppCompatDelegate.setDefaultNightMode(
AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
)
else -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
}
}

View File

@ -1,108 +0,0 @@
/* Copyright 2017 Andrew Dawson
*
* This file is a part of Tusky.
*
* 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.
*
* Tusky 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 Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.util;
import android.content.Context;
import com.keylesspalace.tusky.R;
public class TimestampUtils {
private static final long SECOND_IN_MILLIS = 1000;
private static final long MINUTE_IN_MILLIS = SECOND_IN_MILLIS * 60;
private static final long HOUR_IN_MILLIS = MINUTE_IN_MILLIS * 60;
private static final long DAY_IN_MILLIS = HOUR_IN_MILLIS * 24;
private static final long YEAR_IN_MILLIS = DAY_IN_MILLIS * 365;
/**
* This is a rough duplicate of {@link android.text.format.DateUtils#getRelativeTimeSpanString},
* but even with the FORMAT_ABBREV_RELATIVE flag it wasn't abbreviating enough.
*/
public static String getRelativeTimeSpanString(Context context, long then, long now) {
long span = now - then;
boolean future = false;
if (Math.abs(span) < SECOND_IN_MILLIS) {
return context.getString(R.string.status_created_at_now);
}
else if (span < 0) {
future = true;
span = -span;
}
int format;
if (span < MINUTE_IN_MILLIS) {
span /= SECOND_IN_MILLIS;
if (future) {
format = R.string.abbreviated_in_seconds;
} else {
format = R.string.abbreviated_seconds_ago;
}
} else if (span < HOUR_IN_MILLIS) {
span /= MINUTE_IN_MILLIS;
if (future) {
format = R.string.abbreviated_in_minutes;
} else {
format = R.string.abbreviated_minutes_ago;
}
} else if (span < DAY_IN_MILLIS) {
span /= HOUR_IN_MILLIS;
if (future) {
format = R.string.abbreviated_in_hours;
} else {
format = R.string.abbreviated_hours_ago;
}
} else if (span < YEAR_IN_MILLIS) {
span /= DAY_IN_MILLIS;
if (future) {
format = R.string.abbreviated_in_days;
} else {
format = R.string.abbreviated_days_ago;
}
} else {
span /= YEAR_IN_MILLIS;
if (future) {
format = R.string.abbreviated_in_years;
} else {
format = R.string.abbreviated_years_ago;
}
}
return context.getString(format, span);
}
public static String formatPollDuration(Context context, long then, long now) {
long span = then - now;
if (span < 0) {
span = 0;
}
int format;
if (span < MINUTE_IN_MILLIS) {
span /= SECOND_IN_MILLIS;
format = R.plurals.poll_timespan_seconds;
} else if (span < HOUR_IN_MILLIS) {
span /= MINUTE_IN_MILLIS;
format = R.plurals.poll_timespan_minutes;
} else if (span < DAY_IN_MILLIS) {
span /= HOUR_IN_MILLIS;
format = R.plurals.poll_timespan_hours;
} else {
span /= DAY_IN_MILLIS;
format = R.plurals.poll_timespan_days;
}
return context.getResources().getQuantityString(format, (int) span, (int) span);
}
}

View File

@ -0,0 +1,102 @@
/* Copyright 2017 Andrew Dawson
*
* This file is a part of Tusky.
*
* 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.
*
* Tusky 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 Tusky; if not,
* see <http://www.gnu.org/licenses>. */
@file:JvmName("TimestampUtils")
package com.keylesspalace.tusky.util
import android.content.Context
import com.keylesspalace.tusky.R
import kotlin.math.abs
private const val SECOND_IN_MILLIS: Long = 1000
private const val MINUTE_IN_MILLIS = SECOND_IN_MILLIS * 60
private const val HOUR_IN_MILLIS = MINUTE_IN_MILLIS * 60
private const val DAY_IN_MILLIS = HOUR_IN_MILLIS * 24
private const val YEAR_IN_MILLIS = DAY_IN_MILLIS * 365
/**
* This is a rough duplicate of [android.text.format.DateUtils.getRelativeTimeSpanString],
* but even with the FORMAT_ABBREV_RELATIVE flag it wasn't abbreviating enough.
*/
fun getRelativeTimeSpanString(context: Context, then: Long, now: Long): String {
var span = now - then
var future = false
if (abs(span) < SECOND_IN_MILLIS) {
return context.getString(R.string.status_created_at_now)
} else if (span < 0) {
future = true
span = -span
}
val format: Int
if (span < MINUTE_IN_MILLIS) {
span /= SECOND_IN_MILLIS
format = if (future) {
R.string.abbreviated_in_seconds
} else {
R.string.abbreviated_seconds_ago
}
} else if (span < HOUR_IN_MILLIS) {
span /= MINUTE_IN_MILLIS
format = if (future) {
R.string.abbreviated_in_minutes
} else {
R.string.abbreviated_minutes_ago
}
} else if (span < DAY_IN_MILLIS) {
span /= HOUR_IN_MILLIS
format = if (future) {
R.string.abbreviated_in_hours
} else {
R.string.abbreviated_hours_ago
}
} else if (span < YEAR_IN_MILLIS) {
span /= DAY_IN_MILLIS
format = if (future) {
R.string.abbreviated_in_days
} else {
R.string.abbreviated_days_ago
}
} else {
span /= YEAR_IN_MILLIS
format = if (future) {
R.string.abbreviated_in_years
} else {
R.string.abbreviated_years_ago
}
}
return context.getString(format, span)
}
fun formatPollDuration(context: Context, then: Long, now: Long): String {
var span = then - now
if (span < 0) {
span = 0
}
val format: Int
if (span < MINUTE_IN_MILLIS) {
span /= SECOND_IN_MILLIS
format = R.plurals.poll_timespan_seconds
} else if (span < HOUR_IN_MILLIS) {
span /= MINUTE_IN_MILLIS
format = R.plurals.poll_timespan_minutes
} else if (span < DAY_IN_MILLIS) {
span /= HOUR_IN_MILLIS
format = R.plurals.poll_timespan_hours
} else {
span /= DAY_IN_MILLIS
format = R.plurals.poll_timespan_days
}
return context.resources.getQuantityString(format, span.toInt(), span.toInt())
}

View File

@ -16,12 +16,13 @@
package com.keylesspalace.tusky.view
import android.content.Context
import android.graphics.Color
import android.util.AttributeSet
import android.view.LayoutInflater
import com.google.android.material.card.MaterialCardView
import com.google.android.material.color.MaterialColors
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.databinding.CardLicenseBinding
import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.openLink
@ -35,7 +36,7 @@ class LicenseCard
init {
val binding = CardLicenseBinding.inflate(LayoutInflater.from(context), this)
setCardBackgroundColor(ThemeUtils.getColor(context, R.attr.colorSurface))
setCardBackgroundColor(MaterialColors.getColor(context, R.attr.colorSurface, Color.BLACK))
val a = context.theme.obtainStyledAttributes(attrs, R.styleable.LicenseCard, 0, 0)

View File

@ -0,0 +1,77 @@
package com.keylesspalace.tusky.util
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.Config
@Config(sdk = [28])
@RunWith(AndroidJUnit4::class)
class HttpHeaderLinkTest {
data class TestData(val name: String, val input: String, val want: List<HttpHeaderLink>)
@Test
fun shouldParseValidLinks() {
val testData = arrayOf(
// Examples from https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link
TestData(
"Single URL",
"<https://example.com>",
listOf(HttpHeaderLink("https://example.com"))
),
TestData(
"Single URL with parameters",
"<https://example.com>; rel=\"preconnect\"",
listOf(HttpHeaderLink("https://example.com"))
),
TestData(
"Single encoded URL with parameters",
"<https://example.com/%E8%8B%97%E6%9D%A1>; rel=\"preconnect\"",
listOf(HttpHeaderLink("https://example.com/%E8%8B%97%E6%9D%A1"))
),
TestData(
"Multiple URLs, separated by commas",
"<https://one.example.com>; rel=\"preconnect\", <https://two.example.com>; rel=\"preconnect\", <https://three.example.com>; rel=\"preconnect\"",
listOf(
HttpHeaderLink("https://one.example.com"),
HttpHeaderLink("https://two.example.com"),
HttpHeaderLink("https://three.example.com")
)
),
// Examples from https://httpwg.org/specs/rfc8288.html#rfc.section.3.5
TestData(
"Single URL, multiple parameters",
"<http://example.com/TheBook/chapter2>; rel=\"previous\"; title=\"previous chapter\"",
listOf(HttpHeaderLink("http://example.com/TheBook/chapter2"))
),
TestData(
"Root resource",
"</>; rel=\"http://example.net/foo\"",
listOf(HttpHeaderLink("/"))
),
TestData(
"Terms and anchor",
"</terms>; rel=\"copyright\"; anchor=\"#foo\"",
listOf(HttpHeaderLink("/terms"))
),
TestData(
"Multiple URLs with parameter encoding",
"</TheBook/chapter2>; rel=\"previous\"; title*=UTF-8'de'letztes%20Kapitel, </TheBook/chapter4>; rel=\"next\"; title*=UTF-8'de'n%c3%a4chstes%20Kapitel",
listOf(
HttpHeaderLink("/TheBook/chapter2"),
HttpHeaderLink("/TheBook/chapter4")
)
)
)
// Verify that the URLs are parsed correctly
for (test in testData) {
val links = HttpHeaderLink.parse(test.input)
assertEquals("${test.name}: Same size", links.size, test.want.size)
for (i in links.indices) {
assertEquals(test.name, test.want[i].uri, links[i].uri)
}
}
}
}

View File

@ -0,0 +1,91 @@
package com.keylesspalace.tusky.util
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Before
import org.junit.Test
/**
* Tests for PairedList, with a mapper that multiples everything by 2.
*/
class PairedListTest {
private lateinit var pairedList: PairedList<Int, Int>
@Before
fun beforeEachTest() {
pairedList = PairedList { it * 2 }
for (i in 0..10) {
pairedList.add(i)
}
}
@Test
fun pairedCopy() {
val copy = pairedList.pairedCopy
for (i in 0..10) {
assertEquals(i * 2, copy[i])
}
}
@Test
fun getPairedItem() {
for (i in 0..10) {
assertEquals(i * 2, pairedList.getPairedItem(i))
}
}
@Test
fun getPairedItemOrNull() {
for (i in 0..10) {
assertEquals(i * 2, pairedList.getPairedItem(i))
}
assertNull(pairedList.getPairedItemOrNull(11))
}
@Test
fun setPairedItem() {
pairedList.setPairedItem(2, 2)
assertEquals(2, pairedList.getPairedItem(2))
}
@Test
fun get() {
for (i in 0..10) {
assertEquals(i, pairedList[i])
}
}
@Test
fun set() {
assertEquals(0, pairedList[0])
pairedList[0] = 10
assertEquals(10, pairedList[0])
assertEquals(20, pairedList.getPairedItem(0))
}
@Test
fun add() {
pairedList.add(11)
assertEquals(11, pairedList[11])
assertEquals(22, pairedList.getPairedItem(11))
}
@Test
fun addAtIndex() {
pairedList.add(11, 11)
assertEquals(11, pairedList[11])
assertEquals(22, pairedList.getPairedItem(11))
}
@Test
fun removeAt() {
pairedList.removeAt(5)
assertEquals(6, pairedList[5])
assertEquals(12, pairedList.getPairedItem(5))
}
@Test
fun size() {
assertEquals(11, pairedList.size)
}
}

View File

@ -21,9 +21,9 @@ class TimestampUtilsTest {
@Test
fun shouldShowNowForSmallTimeSpans() {
assertEquals(STATUS_CREATED_AT_NOW, TimestampUtils.getRelativeTimeSpanString(ctx, 0, 300))
assertEquals(STATUS_CREATED_AT_NOW, TimestampUtils.getRelativeTimeSpanString(ctx, 300, 0))
assertEquals(STATUS_CREATED_AT_NOW, TimestampUtils.getRelativeTimeSpanString(ctx, 501, 0))
assertEquals(STATUS_CREATED_AT_NOW, TimestampUtils.getRelativeTimeSpanString(ctx, 0, 999))
assertEquals(STATUS_CREATED_AT_NOW, getRelativeTimeSpanString(ctx, 0, 300))
assertEquals(STATUS_CREATED_AT_NOW, getRelativeTimeSpanString(ctx, 300, 0))
assertEquals(STATUS_CREATED_AT_NOW, getRelativeTimeSpanString(ctx, 501, 0))
assertEquals(STATUS_CREATED_AT_NOW, getRelativeTimeSpanString(ctx, 0, 999))
}
}