Android 12 support, update AndroidX libraries (#2367)
* Android 12 support, update AndroidX libraries * fix ktlint * add Android 12 splash screen support * fix comments in MainActivity * remove deprecated Intent.ACTION_CLOSE_SYSTEM_DIALOGS * delete TimelineViewModelTest * fix notifications on Android 12 * improve splash screen * handle pending intent flags in a dedicated function
This commit is contained in:
parent
221cdb3611
commit
55513e8e2b
25 changed files with 260 additions and 488 deletions
|
@ -15,11 +15,11 @@ def getGitSha = {
|
|||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 30
|
||||
compileSdkVersion 31
|
||||
defaultConfig {
|
||||
applicationId APP_ID
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 30
|
||||
targetSdkVersion 31
|
||||
versionCode 87
|
||||
versionName "16.0"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
@ -89,8 +89,8 @@ android {
|
|||
}
|
||||
|
||||
ext.coroutinesVersion = "1.6.0"
|
||||
ext.lifecycleVersion = "2.3.1"
|
||||
ext.roomVersion = '2.3.0'
|
||||
ext.lifecycleVersion = "2.4.1"
|
||||
ext.roomVersion = '2.4.2'
|
||||
ext.retrofitVersion = '2.9.0'
|
||||
ext.okhttpVersion = '4.9.3'
|
||||
ext.glideVersion = '4.12.0'
|
||||
|
@ -104,31 +104,33 @@ dependencies {
|
|||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-rx3:$coroutinesVersion"
|
||||
|
||||
implementation "androidx.core:core-ktx:1.5.0"
|
||||
implementation "androidx.appcompat:appcompat:1.3.0"
|
||||
implementation "androidx.fragment:fragment-ktx:1.3.4"
|
||||
implementation "androidx.browser:browser:1.3.0"
|
||||
implementation "androidx.core:core-ktx:1.7.0"
|
||||
implementation "androidx.appcompat:appcompat:1.4.1"
|
||||
implementation "androidx.fragment:fragment-ktx:1.4.1"
|
||||
implementation "androidx.browser:browser:1.4.0"
|
||||
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
|
||||
implementation "androidx.recyclerview:recyclerview:1.2.1"
|
||||
implementation "androidx.exifinterface:exifinterface:1.3.3"
|
||||
implementation "androidx.cardview:cardview:1.0.0"
|
||||
implementation "androidx.preference:preference-ktx:1.1.1"
|
||||
implementation "androidx.sharetarget:sharetarget:1.1.0"
|
||||
implementation "androidx.preference:preference-ktx:1.2.0"
|
||||
implementation "androidx.sharetarget:sharetarget:1.2.0-rc01"
|
||||
implementation "androidx.emoji:emoji:1.1.0"
|
||||
implementation "androidx.emoji:emoji-appcompat:1.1.0"
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"
|
||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion"
|
||||
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"
|
||||
implementation "androidx.lifecycle:lifecycle-reactivestreams-ktx:$lifecycleVersion"
|
||||
implementation "androidx.constraintlayout:constraintlayout:2.1.2"
|
||||
implementation "androidx.paging:paging-runtime-ktx:3.0.0"
|
||||
implementation "androidx.constraintlayout:constraintlayout:2.1.3"
|
||||
implementation "androidx.paging:paging-runtime-ktx:3.1.0"
|
||||
implementation "androidx.viewpager2:viewpager2:1.0.0"
|
||||
implementation "androidx.work:work-runtime:2.5.0"
|
||||
implementation "androidx.work:work-runtime:2.7.1"
|
||||
implementation "androidx.room:room-ktx:$roomVersion"
|
||||
implementation "androidx.room:room-paging:$roomVersion"
|
||||
implementation "androidx.room:room-rxjava3:$roomVersion"
|
||||
kapt "androidx.room:room-compiler:$roomVersion"
|
||||
implementation 'androidx.core:core-splashscreen:1.0.0-beta01'
|
||||
|
||||
implementation "com.google.android.material:material:1.4.0"
|
||||
implementation "com.google.android.material:material:1.5.0"
|
||||
|
||||
implementation "com.google.code.gson:gson:2.8.9"
|
||||
|
||||
|
|
6
app/src/green/res/values/flavor-colors.xml
Normal file
6
app/src/green/res/values/flavor-colors.xml
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<color name="notification_color">#19A341</color>
|
||||
|
||||
</resources>
|
|
@ -20,20 +20,7 @@
|
|||
android:supportsRtl="true"
|
||||
android:theme="@style/TuskyTheme"
|
||||
android:usesCleartextTraffic="false">
|
||||
<activity
|
||||
android:name=".SplashActivity"
|
||||
android:theme="@style/SplashTheme">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.app.shortcuts"
|
||||
android:resource="@xml/share_shortcuts" />
|
||||
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".components.login.LoginActivity"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
|
@ -41,7 +28,15 @@
|
|||
<activity android:name=".components.login.LoginWebViewActivity" />
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden|screenLayout|smallestScreenSize">
|
||||
android:configChanges="orientation|screenSize|keyboardHidden|screenLayout|smallestScreenSize"
|
||||
android:theme="@style/SplashTheme"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
|
||||
|
@ -88,6 +83,9 @@
|
|||
<meta-data
|
||||
android:name="android.service.chooser.chooser_target_service"
|
||||
android:value="androidx.sharetarget.ChooserTargetServiceCompat" />
|
||||
<meta-data
|
||||
android:name="android.app.shortcuts"
|
||||
android:resource="@xml/share_shortcuts" />
|
||||
|
||||
</activity>
|
||||
<activity
|
||||
|
@ -97,7 +95,6 @@
|
|||
<activity
|
||||
android:name=".ViewThreadActivity"
|
||||
android:configChanges="orientation|screenSize" />
|
||||
<activity android:name=".ViewTagActivity" />
|
||||
<activity
|
||||
android:name=".ViewMediaActivity"
|
||||
android:theme="@style/TuskyBaseTheme" />
|
||||
|
@ -115,7 +112,8 @@
|
|||
android:theme="@style/Base.Theme.AppCompat" />
|
||||
<activity
|
||||
android:name=".components.search.SearchActivity"
|
||||
android:launchMode="singleTop">
|
||||
android:launchMode="singleTop"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEARCH" />
|
||||
</intent-filter>
|
||||
|
@ -125,7 +123,6 @@
|
|||
android:resource="@xml/searchable" />
|
||||
</activity>
|
||||
<activity android:name=".ListsActivity" />
|
||||
<activity android:name=".ModalTimelineActivity" />
|
||||
<activity android:name=".LicenseActivity" />
|
||||
<activity android:name=".FiltersActivity" />
|
||||
<activity
|
||||
|
@ -136,7 +133,8 @@
|
|||
<activity android:name=".components.announcements.AnnouncementsActivity" />
|
||||
<activity android:name=".components.drafts.DraftsActivity" />
|
||||
|
||||
<receiver android:name=".receiver.NotificationClearBroadcastReceiver" />
|
||||
<receiver android:name=".receiver.NotificationClearBroadcastReceiver"
|
||||
android:exported="false" />
|
||||
<receiver
|
||||
android:name=".receiver.SendStatusBroadcastReceiver"
|
||||
android:enabled="true"
|
||||
|
@ -147,13 +145,15 @@
|
|||
android:icon="@drawable/ic_tusky"
|
||||
android:label="Compose Toot"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
||||
android:exported="true"
|
||||
tools:targetApi="24">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<service android:name=".service.SendTootService" />
|
||||
<service android:name=".service.SendTootService"
|
||||
android:exported="false" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
|
@ -167,10 +167,16 @@
|
|||
|
||||
<!-- disable automatic WorkManager initialization -->
|
||||
<provider
|
||||
android:name="androidx.work.impl.WorkManagerInitializer"
|
||||
android:authorities="${applicationId}.workmanager-init"
|
||||
android:name="androidx.startup.InitializationProvider"
|
||||
android:authorities="${applicationId}.androidx-startup"
|
||||
android:exported="false"
|
||||
tools:node="remove" />
|
||||
tools:node="merge">
|
||||
<meta-data
|
||||
android:name="androidx.work.WorkManagerInitializer"
|
||||
android:value="androidx.startup"
|
||||
tools:node="remove" />
|
||||
</provider>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
|
|
@ -35,6 +35,7 @@ import androidx.appcompat.app.AlertDialog
|
|||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.pm.ShortcutManagerCompat
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.emoji.text.EmojiCompat
|
||||
import androidx.emoji.text.EmojiCompat.InitCallback
|
||||
import androidx.lifecycle.Lifecycle
|
||||
|
@ -159,8 +160,12 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
installSplashScreen()
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// delete old notification channels
|
||||
NotificationHelper.deleteLegacyNotificationChannels(this, accountManager)
|
||||
|
||||
val activeAccount = accountManager.activeAccount
|
||||
?: return // will be redirected to LoginActivity by BaseActivity
|
||||
|
||||
|
|
|
@ -1,49 +0,0 @@
|
|||
/* Copyright 2018 Conny Duck
|
||||
*
|
||||
* 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
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.keylesspalace.tusky.components.login.LoginActivity
|
||||
import com.keylesspalace.tusky.components.notifications.NotificationHelper
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.di.Injectable
|
||||
import javax.inject.Inject
|
||||
|
||||
class SplashActivity : AppCompatActivity(), Injectable {
|
||||
|
||||
@Inject
|
||||
lateinit var accountManager: AccountManager
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
/** delete old notification channels */
|
||||
NotificationHelper.deleteLegacyNotificationChannels(this, accountManager)
|
||||
|
||||
/** Determine whether the user is currently logged in, and if so go ahead and load the
|
||||
* timeline. Otherwise, start the activity_login screen. */
|
||||
|
||||
val intent = if (accountManager.activeAccount != null) {
|
||||
Intent(this, MainActivity::class.java)
|
||||
} else {
|
||||
LoginActivity.getIntent(this, false)
|
||||
}
|
||||
startActivity(intent)
|
||||
finish()
|
||||
}
|
||||
}
|
|
@ -16,7 +16,9 @@
|
|||
package com.keylesspalace.tusky.components.compose
|
||||
|
||||
import android.Manifest
|
||||
import android.app.NotificationManager
|
||||
import android.app.ProgressDialog
|
||||
import android.content.ClipData
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
|
@ -45,8 +47,8 @@ import androidx.appcompat.app.AlertDialog
|
|||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.view.inputmethod.InputConnectionCompat
|
||||
import androidx.core.view.inputmethod.InputContentInfoCompat
|
||||
import androidx.core.view.ContentInfoCompat
|
||||
import androidx.core.view.OnReceiveContentListener
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.preference.PreferenceManager
|
||||
|
@ -105,7 +107,7 @@ class ComposeActivity :
|
|||
ComposeAutoCompleteAdapter.AutocompletionProvider,
|
||||
OnEmojiSelectedListener,
|
||||
Injectable,
|
||||
InputConnectionCompat.OnCommitContentListener,
|
||||
OnReceiveContentListener,
|
||||
ComposeScheduleView.OnTimeSetListener {
|
||||
|
||||
@Inject
|
||||
|
@ -149,6 +151,18 @@ class ComposeActivity :
|
|||
public override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val notificationId = intent.getIntExtra(NOTIFICATION_ID_EXTRA, -1)
|
||||
if (notificationId != -1) {
|
||||
// ComposeActivity was opened from a notification, delete the notification
|
||||
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
||||
notificationManager.cancel(notificationId)
|
||||
}
|
||||
|
||||
val accountId = intent.getLongExtra(ACCOUNT_ID_EXTRA, -1)
|
||||
if (accountId != -1L) {
|
||||
accountManager.setActiveAccount(accountId)
|
||||
}
|
||||
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
val theme = preferences.getString("appTheme", ThemeUtils.APP_THEME_DEFAULT)
|
||||
if (theme == "black") {
|
||||
|
@ -282,7 +296,7 @@ class ComposeActivity :
|
|||
}
|
||||
|
||||
private fun setupComposeField(preferences: SharedPreferences, startingText: String?) {
|
||||
binding.composeEditField.setOnCommitContentListener(this)
|
||||
binding.composeEditField.setOnReceiveContentListener(this)
|
||||
|
||||
binding.composeEditField.setOnKeyListener { _, keyCode, event -> this.onKeyDown(keyCode, event) }
|
||||
|
||||
|
@ -742,26 +756,18 @@ class ComposeActivity :
|
|||
}
|
||||
}
|
||||
|
||||
/** This is for the fancy keyboards which can insert images and stuff. */
|
||||
override fun onCommitContent(inputContentInfo: InputContentInfoCompat, flags: Int, opts: Bundle?): Boolean {
|
||||
// Verify the returned content's type is of the correct MIME type
|
||||
val supported = inputContentInfo.description.hasMimeType("image/*")
|
||||
|
||||
if (supported) {
|
||||
val lacksPermission = (flags and InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0
|
||||
if (lacksPermission) {
|
||||
try {
|
||||
inputContentInfo.requestPermission()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "InputContentInfoCompat#requestPermission() failed." + e.message)
|
||||
return false
|
||||
/** This is for the fancy keyboards which can insert images and stuff, and drag&drop etc */
|
||||
override fun onReceiveContent(view: View, contentInfo: ContentInfoCompat): ContentInfoCompat? {
|
||||
if (contentInfo.clip.description.hasMimeType("image/*")) {
|
||||
val split = contentInfo.partition { item: ClipData.Item -> item.uri != null }
|
||||
split.first?.let { content ->
|
||||
for (i in 0 until content.clip.itemCount) {
|
||||
pickMedia(content.clip.getItemAt(i).uri)
|
||||
}
|
||||
}
|
||||
pickMedia(inputContentInfo.contentUri, inputContentInfo)
|
||||
return true
|
||||
return split.second
|
||||
}
|
||||
|
||||
return false
|
||||
return contentInfo
|
||||
}
|
||||
|
||||
private fun sendStatus() {
|
||||
|
@ -784,12 +790,11 @@ class ComposeActivity :
|
|||
}
|
||||
|
||||
viewModel.sendStatus(contentText, spoilerText).observe(
|
||||
this,
|
||||
{
|
||||
finishingUploadDialog?.dismiss()
|
||||
deleteDraftAndFinish()
|
||||
}
|
||||
)
|
||||
this
|
||||
) {
|
||||
finishingUploadDialog?.dismiss()
|
||||
deleteDraftAndFinish()
|
||||
}
|
||||
} else {
|
||||
binding.composeEditField.error = getString(R.string.error_compose_character_limit)
|
||||
enableButtons(true)
|
||||
|
@ -859,12 +864,9 @@ class ComposeActivity :
|
|||
viewModel.removeMediaFromQueue(item)
|
||||
}
|
||||
|
||||
private fun pickMedia(uri: Uri, contentInfoCompat: InputContentInfoCompat? = null) {
|
||||
private fun pickMedia(uri: Uri) {
|
||||
withLifecycleContext {
|
||||
viewModel.pickMedia(uri).observe { exceptionOrItem ->
|
||||
|
||||
contentInfoCompat?.releasePermission()
|
||||
|
||||
exceptionOrItem.asLeftOrNull()?.let {
|
||||
val errorId = when (it) {
|
||||
is VideoSizeException -> {
|
||||
|
@ -1043,12 +1045,32 @@ class ComposeActivity :
|
|||
private const val PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1
|
||||
|
||||
internal const val COMPOSE_OPTIONS_EXTRA = "COMPOSE_OPTIONS"
|
||||
private const val NOTIFICATION_ID_EXTRA = "NOTIFICATION_ID"
|
||||
private const val ACCOUNT_ID_EXTRA = "ACCOUNT_ID"
|
||||
private const val PHOTO_UPLOAD_URI_KEY = "PHOTO_UPLOAD_URI"
|
||||
|
||||
/**
|
||||
* @param options ComposeOptions to configure the ComposeActivity
|
||||
* @param notificationId the id of the notification that starts the Activity
|
||||
* @param accountId the id of the account to compose with, null for the current account
|
||||
* @return an Intent to start the ComposeActivity
|
||||
*/
|
||||
@JvmStatic
|
||||
fun startIntent(context: Context, options: ComposeOptions): Intent {
|
||||
@JvmOverloads
|
||||
fun startIntent(
|
||||
context: Context,
|
||||
options: ComposeOptions,
|
||||
notificationId: Int? = null,
|
||||
accountId: Long? = null
|
||||
): Intent {
|
||||
return Intent(context, ComposeActivity::class.java).apply {
|
||||
putExtra(COMPOSE_OPTIONS_EXTRA, options)
|
||||
if (notificationId != null) {
|
||||
putExtra(NOTIFICATION_ID_EXTRA, notificationId)
|
||||
}
|
||||
if (accountId != null) {
|
||||
putExtra(ACCOUNT_ID_EXTRA, accountId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -22,6 +22,8 @@ import android.util.AttributeSet
|
|||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.InputConnection
|
||||
import androidx.appcompat.widget.AppCompatMultiAutoCompleteTextView
|
||||
import androidx.core.view.OnReceiveContentListener
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.inputmethod.EditorInfoCompat
|
||||
import androidx.core.view.inputmethod.InputConnectionCompat
|
||||
import androidx.emoji.widget.EmojiEditTextHelper
|
||||
|
@ -32,41 +34,33 @@ class EditTextTyped @JvmOverloads constructor(
|
|||
) :
|
||||
AppCompatMultiAutoCompleteTextView(context, attributeSet) {
|
||||
|
||||
private var onCommitContentListener: InputConnectionCompat.OnCommitContentListener? = null
|
||||
private val emojiEditTextHelper: EmojiEditTextHelper = EmojiEditTextHelper(this)
|
||||
|
||||
init {
|
||||
// fix a bug with autocomplete and some keyboards
|
||||
val newInputType = inputType and (inputType xor InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE)
|
||||
inputType = newInputType
|
||||
super.setKeyListener(getEmojiEditTextHelper().getKeyListener(keyListener))
|
||||
super.setKeyListener(emojiEditTextHelper.getKeyListener(keyListener))
|
||||
}
|
||||
|
||||
override fun setKeyListener(input: KeyListener) {
|
||||
super.setKeyListener(getEmojiEditTextHelper().getKeyListener(input))
|
||||
override fun setKeyListener(input: KeyListener?) {
|
||||
if (input != null) {
|
||||
super.setKeyListener(emojiEditTextHelper.getKeyListener(input))
|
||||
} else {
|
||||
super.setKeyListener(input)
|
||||
}
|
||||
}
|
||||
|
||||
fun setOnCommitContentListener(listener: InputConnectionCompat.OnCommitContentListener) {
|
||||
onCommitContentListener = listener
|
||||
fun setOnReceiveContentListener(listener: OnReceiveContentListener) {
|
||||
ViewCompat.setOnReceiveContentListener(this, arrayOf("image/*"), listener)
|
||||
}
|
||||
|
||||
override fun onCreateInputConnection(editorInfo: EditorInfo): InputConnection {
|
||||
val connection = super.onCreateInputConnection(editorInfo)
|
||||
return if (onCommitContentListener != null) {
|
||||
EditorInfoCompat.setContentMimeTypes(editorInfo, arrayOf("image/*"))
|
||||
getEmojiEditTextHelper().onCreateInputConnection(
|
||||
InputConnectionCompat.createWrapper(
|
||||
connection, editorInfo,
|
||||
onCommitContentListener!!
|
||||
),
|
||||
editorInfo
|
||||
)!!
|
||||
} else {
|
||||
connection
|
||||
}
|
||||
}
|
||||
|
||||
private fun getEmojiEditTextHelper(): EmojiEditTextHelper {
|
||||
return emojiEditTextHelper
|
||||
EditorInfoCompat.setContentMimeTypes(editorInfo, arrayOf("image/*"))
|
||||
return emojiEditTextHelper.onCreateInputConnection(
|
||||
InputConnectionCompat.createWrapper(this, connection, editorInfo),
|
||||
editorInfo
|
||||
)!!
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,7 +24,6 @@ import android.content.Context;
|
|||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Color;
|
||||
import android.os.Build;
|
||||
import android.provider.Settings;
|
||||
import android.text.TextUtils;
|
||||
|
@ -46,9 +45,9 @@ import androidx.work.WorkRequest;
|
|||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
|
||||
import com.bumptech.glide.request.FutureTarget;
|
||||
import com.keylesspalace.tusky.BuildConfig;
|
||||
import com.keylesspalace.tusky.MainActivity;
|
||||
import com.keylesspalace.tusky.R;
|
||||
import com.keylesspalace.tusky.components.compose.ComposeActivity;
|
||||
import com.keylesspalace.tusky.db.AccountEntity;
|
||||
import com.keylesspalace.tusky.db.AccountManager;
|
||||
import com.keylesspalace.tusky.entity.Notification;
|
||||
|
@ -67,6 +66,7 @@ import java.util.ArrayList;
|
|||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
|
@ -88,8 +88,6 @@ public class NotificationHelper {
|
|||
|
||||
public static final String REPLY_ACTION = "REPLY_ACTION";
|
||||
|
||||
public static final String COMPOSE_ACTION = "COMPOSE_ACTION";
|
||||
|
||||
public static final String KEY_REPLY = "KEY_REPLY";
|
||||
|
||||
public static final String KEY_SENDER_ACCOUNT_ID = "KEY_SENDER_ACCOUNT_ID";
|
||||
|
@ -108,10 +106,6 @@ public class NotificationHelper {
|
|||
|
||||
public static final String KEY_MENTIONS = "KEY_MENTIONS";
|
||||
|
||||
public static final String KEY_CITED_TEXT = "KEY_CITED_TEXT";
|
||||
|
||||
public static final String KEY_CITED_AUTHOR_LOCAL = "KEY_CITED_AUTHOR_LOCAL";
|
||||
|
||||
/**
|
||||
* notification channels used on Android O+
|
||||
**/
|
||||
|
@ -206,21 +200,24 @@ public class NotificationHelper {
|
|||
.setLabel(context.getString(R.string.label_quick_reply))
|
||||
.build();
|
||||
|
||||
PendingIntent quickReplyPendingIntent = getStatusReplyIntent(REPLY_ACTION, context, body, account);
|
||||
PendingIntent quickReplyPendingIntent = getStatusReplyIntent(context, body, account);
|
||||
|
||||
NotificationCompat.Action quickReplyAction =
|
||||
new NotificationCompat.Action.Builder(R.drawable.ic_reply_24dp,
|
||||
context.getString(R.string.action_quick_reply), quickReplyPendingIntent)
|
||||
context.getString(R.string.action_quick_reply),
|
||||
quickReplyPendingIntent)
|
||||
.addRemoteInput(replyRemoteInput)
|
||||
.build();
|
||||
|
||||
builder.addAction(quickReplyAction);
|
||||
|
||||
PendingIntent composePendingIntent = getStatusReplyIntent(COMPOSE_ACTION, context, body, account);
|
||||
PendingIntent composeIntent = getStatusComposeIntent(context, body, account);
|
||||
|
||||
NotificationCompat.Action composeAction =
|
||||
new NotificationCompat.Action.Builder(R.drawable.ic_reply_24dp,
|
||||
context.getString(R.string.action_compose_shortcut), composePendingIntent)
|
||||
context.getString(R.string.action_compose_shortcut),
|
||||
composeIntent)
|
||||
.setShowsUserInterface(true)
|
||||
.build();
|
||||
|
||||
builder.addAction(composeAction);
|
||||
|
@ -237,7 +234,6 @@ public class NotificationHelper {
|
|||
}
|
||||
|
||||
// Summary
|
||||
// =======
|
||||
final NotificationCompat.Builder summaryBuilder = newNotification(context, body, account, true);
|
||||
|
||||
if (currentNotifications.length() != 1) {
|
||||
|
@ -275,7 +271,7 @@ public class NotificationHelper {
|
|||
summaryStackBuilder.addNextIntent(summaryResultIntent);
|
||||
|
||||
PendingIntent summaryResultPendingIntent = summaryStackBuilder.getPendingIntent((int) (notificationId + account.getId() * 10000),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
pendingIntentFlags(false));
|
||||
|
||||
// we have to switch account here
|
||||
Intent eventResultIntent = new Intent(context, MainActivity.class);
|
||||
|
@ -285,18 +281,18 @@ public class NotificationHelper {
|
|||
eventStackBuilder.addNextIntent(eventResultIntent);
|
||||
|
||||
PendingIntent eventResultPendingIntent = eventStackBuilder.getPendingIntent((int) account.getId(),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
pendingIntentFlags(false));
|
||||
|
||||
Intent deleteIntent = new Intent(context, NotificationClearBroadcastReceiver.class);
|
||||
deleteIntent.putExtra(ACCOUNT_ID, account.getId());
|
||||
PendingIntent deletePendingIntent = PendingIntent.getBroadcast(context, summary ? (int) account.getId() : notificationId, deleteIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
pendingIntentFlags(false));
|
||||
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, getChannelId(account, body))
|
||||
.setSmallIcon(R.drawable.ic_notify)
|
||||
.setContentIntent(summary ? summaryResultPendingIntent : eventResultPendingIntent)
|
||||
.setDeleteIntent(deletePendingIntent)
|
||||
.setColor(BuildConfig.FLAVOR == "green" ? Color.parseColor("#19A341") : ContextCompat.getColor(context, R.color.tusky_blue))
|
||||
.setColor(ContextCompat.getColor(context, R.color.notification_color))
|
||||
.setGroup(account.getAccountId())
|
||||
.setAutoCancel(true)
|
||||
.setShortcutId(Long.toString(account.getId()))
|
||||
|
@ -307,11 +303,9 @@ public class NotificationHelper {
|
|||
return builder;
|
||||
}
|
||||
|
||||
private static PendingIntent getStatusReplyIntent(String action, Context context, Notification body, AccountEntity account) {
|
||||
private static PendingIntent getStatusReplyIntent(Context context, Notification body, AccountEntity account) {
|
||||
Status status = body.getStatus();
|
||||
|
||||
String citedLocalAuthor = status.getAccount().getLocalUsername();
|
||||
String citedText = status.getContent().toString();
|
||||
String inReplyToId = status.getId();
|
||||
Status actionableStatus = status.getActionableStatus();
|
||||
Status.Visibility replyVisibility = actionableStatus.getVisibility();
|
||||
|
@ -326,9 +320,7 @@ public class NotificationHelper {
|
|||
mentionedUsernames = new ArrayList<>(new LinkedHashSet<>(mentionedUsernames));
|
||||
|
||||
Intent replyIntent = new Intent(context, SendStatusBroadcastReceiver.class)
|
||||
.setAction(action)
|
||||
.putExtra(KEY_CITED_AUTHOR_LOCAL, citedLocalAuthor)
|
||||
.putExtra(KEY_CITED_TEXT, citedText)
|
||||
.setAction(REPLY_ACTION)
|
||||
.putExtra(KEY_SENDER_ACCOUNT_ID, account.getId())
|
||||
.putExtra(KEY_SENDER_ACCOUNT_IDENTIFIER, account.getIdentifier())
|
||||
.putExtra(KEY_SENDER_ACCOUNT_FULL_NAME, account.getFullName())
|
||||
|
@ -341,7 +333,50 @@ public class NotificationHelper {
|
|||
return PendingIntent.getBroadcast(context.getApplicationContext(),
|
||||
notificationId,
|
||||
replyIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
pendingIntentFlags(true));
|
||||
}
|
||||
|
||||
private static PendingIntent getStatusComposeIntent(Context context, Notification body, AccountEntity account) {
|
||||
Status status = body.getStatus();
|
||||
|
||||
String citedLocalAuthor = status.getAccount().getLocalUsername();
|
||||
String citedText = status.getContent().toString();
|
||||
String inReplyToId = status.getId();
|
||||
Status actionableStatus = status.getActionableStatus();
|
||||
Status.Visibility replyVisibility = actionableStatus.getVisibility();
|
||||
String contentWarning = actionableStatus.getSpoilerText();
|
||||
List<Status.Mention> mentions = actionableStatus.getMentions();
|
||||
Set<String> mentionedUsernames = new LinkedHashSet<>();
|
||||
mentionedUsernames.add(actionableStatus.getAccount().getUsername());
|
||||
for (Status.Mention mention : mentions) {
|
||||
String mentionedUsername = mention.getUsername();
|
||||
if (!mentionedUsername.equals(account.getUsername())) {
|
||||
mentionedUsernames.add(mention.getUsername());
|
||||
}
|
||||
}
|
||||
|
||||
ComposeActivity.ComposeOptions composeOptions = new ComposeActivity.ComposeOptions();
|
||||
composeOptions.setInReplyToId(inReplyToId);
|
||||
composeOptions.setReplyVisibility(replyVisibility);
|
||||
composeOptions.setContentWarning(contentWarning);
|
||||
composeOptions.setReplyingStatusAuthor(citedLocalAuthor);
|
||||
composeOptions.setReplyingStatusContent(citedText);
|
||||
composeOptions.setMentionedUsernames(mentionedUsernames);
|
||||
composeOptions.setModifiedInitialState(true);
|
||||
|
||||
Intent composeIntent = ComposeActivity.startIntent(
|
||||
context,
|
||||
composeOptions,
|
||||
notificationId,
|
||||
account.getId()
|
||||
);
|
||||
|
||||
composeIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
|
||||
return PendingIntent.getActivity(context.getApplicationContext(),
|
||||
notificationId,
|
||||
composeIntent,
|
||||
pendingIntentFlags(false));
|
||||
}
|
||||
|
||||
public static void createNotificationChannelsForAccount(@NonNull AccountEntity account, @NonNull Context context) {
|
||||
|
@ -409,9 +444,7 @@ public class NotificationHelper {
|
|||
|
||||
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
|
||||
//noinspection ConstantConditions
|
||||
notificationManager.deleteNotificationChannelGroup(account.getIdentifier());
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -421,7 +454,6 @@ public class NotificationHelper {
|
|||
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
|
||||
// used until Tusky 1.4
|
||||
//noinspection ConstantConditions
|
||||
notificationManager.deleteNotificationChannel(CHANNEL_MENTION);
|
||||
notificationManager.deleteNotificationChannel(CHANNEL_FAVOURITE);
|
||||
notificationManager.deleteNotificationChannel(CHANNEL_BOOST);
|
||||
|
@ -440,7 +472,6 @@ public class NotificationHelper {
|
|||
// on Android >= O, notifications are enabled, if at least one channel is enabled
|
||||
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
|
||||
//noinspection ConstantConditions
|
||||
if (notificationManager.areNotificationsEnabled()) {
|
||||
for (NotificationChannel channel : notificationManager.getNotificationChannels()) {
|
||||
if (channel.getImportance() > NotificationManager.IMPORTANCE_NONE) {
|
||||
|
@ -491,7 +522,6 @@ public class NotificationHelper {
|
|||
accountManager.saveAccount(account);
|
||||
|
||||
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
//noinspection ConstantConditions
|
||||
notificationManager.cancel((int) account.getId());
|
||||
return true;
|
||||
})
|
||||
|
@ -511,7 +541,6 @@ public class NotificationHelper {
|
|||
// unknown notificationtype
|
||||
return false;
|
||||
}
|
||||
//noinspection ConstantConditions
|
||||
NotificationChannel channel = notificationManager.getNotificationChannel(channelId);
|
||||
return channel.getImportance() > NotificationManager.IMPORTANCE_NONE;
|
||||
}
|
||||
|
@ -674,4 +703,11 @@ public class NotificationHelper {
|
|||
return null;
|
||||
}
|
||||
|
||||
public static int pendingIntentFlags(boolean mutable) {
|
||||
if (mutable) {
|
||||
return PendingIntent.FLAG_UPDATE_CURRENT | (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S ? PendingIntent.FLAG_MUTABLE : 0);
|
||||
} else {
|
||||
return PendingIntent.FLAG_UPDATE_CURRENT | (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? PendingIntent.FLAG_IMMUTABLE : 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,8 +13,8 @@ import android.widget.Toast
|
|||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.keylesspalace.tusky.MainActivity
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.SplashActivity
|
||||
import com.keylesspalace.tusky.databinding.DialogEmojicompatBinding
|
||||
import com.keylesspalace.tusky.databinding.ItemEmojiPrefBinding
|
||||
import com.keylesspalace.tusky.util.EmojiCompatFont
|
||||
|
@ -215,7 +215,7 @@ class EmojiPreference(
|
|||
.setPositiveButton(R.string.restart) { _, _ ->
|
||||
// Restart the app
|
||||
// From https://stackoverflow.com/a/17166729/5070653
|
||||
val launchIntent = Intent(context, SplashActivity::class.java)
|
||||
val launchIntent = Intent(context, MainActivity::class.java)
|
||||
val mPendingIntent = PendingIntent.getActivity(
|
||||
context,
|
||||
0x1f973, // This is the codepoint of the party face emoji :D
|
||||
|
|
|
@ -280,23 +280,24 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
|||
}
|
||||
|
||||
private fun updateHttpProxySummary() {
|
||||
val sharedPreferences = preferenceManager.sharedPreferences
|
||||
val httpProxyEnabled = sharedPreferences.getBoolean(PrefKeys.HTTP_PROXY_ENABLED, false)
|
||||
val httpServer = sharedPreferences.getNonNullString(PrefKeys.HTTP_PROXY_SERVER, "")
|
||||
preferenceManager.sharedPreferences?.let { sharedPreferences ->
|
||||
val httpProxyEnabled = sharedPreferences.getBoolean(PrefKeys.HTTP_PROXY_ENABLED, false)
|
||||
val httpServer = sharedPreferences.getNonNullString(PrefKeys.HTTP_PROXY_SERVER, "")
|
||||
|
||||
try {
|
||||
val httpPort = sharedPreferences.getNonNullString(PrefKeys.HTTP_PROXY_PORT, "-1")
|
||||
.toInt()
|
||||
try {
|
||||
val httpPort = sharedPreferences.getNonNullString(PrefKeys.HTTP_PROXY_PORT, "-1")
|
||||
.toInt()
|
||||
|
||||
if (httpProxyEnabled && httpServer.isNotBlank() && httpPort > 0 && httpPort < 65535) {
|
||||
httpProxyPref?.summary = "$httpServer:$httpPort"
|
||||
return
|
||||
if (httpProxyEnabled && httpServer.isNotBlank() && httpPort > 0 && httpPort < 65535) {
|
||||
httpProxyPref?.summary = "$httpServer:$httpPort"
|
||||
return
|
||||
}
|
||||
} catch (e: NumberFormatException) {
|
||||
// user has entered wrong port, fall back to empty summary
|
||||
}
|
||||
} catch (e: NumberFormatException) {
|
||||
// user has entered wrong port, fall back to empty summary
|
||||
}
|
||||
|
||||
httpProxyPref?.summary = ""
|
||||
httpProxyPref?.summary = ""
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -90,9 +90,9 @@ class TimelineFragment :
|
|||
|
||||
private val viewModel: TimelineViewModel by lazy {
|
||||
if (kind == TimelineViewModel.Kind.HOME) {
|
||||
ViewModelProvider(this, viewModelFactory).get(CachedTimelineViewModel::class.java)
|
||||
ViewModelProvider(this, viewModelFactory)[CachedTimelineViewModel::class.java]
|
||||
} else {
|
||||
ViewModelProvider(this, viewModelFactory).get(NetworkTimelineViewModel::class.java)
|
||||
ViewModelProvider(this, viewModelFactory)[NetworkTimelineViewModel::class.java]
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -136,7 +136,7 @@ class TimelineFragment :
|
|||
|
||||
isSwipeToRefreshEnabled = arguments.getBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, true)
|
||||
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
val statusDisplayOptions = StatusDisplayOptions(
|
||||
animateAvatars = preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false),
|
||||
mediaPreviewEnabled = accountManager.activeAccount!!.mediaPreviewEnabled,
|
||||
|
@ -224,7 +224,7 @@ class TimelineFragment :
|
|||
}
|
||||
|
||||
if (actionButtonPresent()) {
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
hideFab = preferences.getBoolean("fabHide", false)
|
||||
scrollListener = object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(view: RecyclerView, dx: Int, dy: Int) {
|
||||
|
@ -401,7 +401,7 @@ class TimelineFragment :
|
|||
}
|
||||
|
||||
private fun onPreferenceChanged(key: String) {
|
||||
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
when (key) {
|
||||
PrefKeys.FAB_HIDE -> {
|
||||
hideFab = sharedPreferences.getBoolean(PrefKeys.FAB_HIDE, false)
|
||||
|
@ -468,7 +468,7 @@ class TimelineFragment :
|
|||
* Auto dispose observable on pause
|
||||
*/
|
||||
private fun startUpdateTimestamp() {
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
val useAbsoluteTime = preferences.getBoolean(PrefKeys.ABSOLUTE_TIME_VIEW, false)
|
||||
if (!useAbsoluteTime) {
|
||||
Observable.interval(1, TimeUnit.MINUTES)
|
||||
|
|
|
@ -23,7 +23,6 @@ import com.keylesspalace.tusky.FiltersActivity
|
|||
import com.keylesspalace.tusky.LicenseActivity
|
||||
import com.keylesspalace.tusky.ListsActivity
|
||||
import com.keylesspalace.tusky.MainActivity
|
||||
import com.keylesspalace.tusky.SplashActivity
|
||||
import com.keylesspalace.tusky.StatusListActivity
|
||||
import com.keylesspalace.tusky.TabPreferenceActivity
|
||||
import com.keylesspalace.tusky.ViewMediaActivity
|
||||
|
@ -88,9 +87,6 @@ abstract class ActivitiesModule {
|
|||
@ContributesAndroidInjector
|
||||
abstract fun contributesLoginWebViewActivity(): LoginWebViewActivity
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract fun contributesSplashActivity(): SplashActivity
|
||||
|
||||
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
|
||||
abstract fun contributesPreferencesActivity(): PreferencesActivity
|
||||
|
||||
|
|
|
@ -18,14 +18,14 @@ package com.keylesspalace.tusky.receiver
|
|||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.app.RemoteInput
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.keylesspalace.tusky.BuildConfig
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
||||
import com.keylesspalace.tusky.components.compose.ComposeActivity.ComposeOptions
|
||||
import com.keylesspalace.tusky.components.notifications.NotificationHelper
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
|
@ -45,22 +45,19 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() {
|
|||
override fun onReceive(context: Context, intent: Intent) {
|
||||
AndroidInjection.inject(this, context)
|
||||
|
||||
val notificationId = intent.getIntExtra(NotificationHelper.KEY_NOTIFICATION_ID, -1)
|
||||
val senderId = intent.getLongExtra(NotificationHelper.KEY_SENDER_ACCOUNT_ID, -1)
|
||||
val senderIdentifier = intent.getStringExtra(NotificationHelper.KEY_SENDER_ACCOUNT_IDENTIFIER)
|
||||
val senderFullName = intent.getStringExtra(NotificationHelper.KEY_SENDER_ACCOUNT_FULL_NAME)
|
||||
val citedStatusId = intent.getStringExtra(NotificationHelper.KEY_CITED_STATUS_ID)
|
||||
val visibility = intent.getSerializableExtra(NotificationHelper.KEY_VISIBILITY) as Status.Visibility
|
||||
val spoiler = intent.getStringExtra(NotificationHelper.KEY_SPOILER) ?: ""
|
||||
val mentions = intent.getStringArrayExtra(NotificationHelper.KEY_MENTIONS) ?: emptyArray()
|
||||
val citedText = intent.getStringExtra(NotificationHelper.KEY_CITED_TEXT)
|
||||
val localAuthorId = intent.getStringExtra(NotificationHelper.KEY_CITED_AUTHOR_LOCAL)
|
||||
|
||||
val account = accountManager.getAccountById(senderId)
|
||||
|
||||
val notificationManager = NotificationManagerCompat.from(context)
|
||||
|
||||
if (intent.action == NotificationHelper.REPLY_ACTION) {
|
||||
val notificationId = intent.getIntExtra(NotificationHelper.KEY_NOTIFICATION_ID, -1)
|
||||
val senderId = intent.getLongExtra(NotificationHelper.KEY_SENDER_ACCOUNT_ID, -1)
|
||||
val senderIdentifier = intent.getStringExtra(NotificationHelper.KEY_SENDER_ACCOUNT_IDENTIFIER)
|
||||
val senderFullName = intent.getStringExtra(NotificationHelper.KEY_SENDER_ACCOUNT_FULL_NAME)
|
||||
val citedStatusId = intent.getStringExtra(NotificationHelper.KEY_CITED_STATUS_ID)
|
||||
val visibility = intent.getSerializableExtra(NotificationHelper.KEY_VISIBILITY) as Status.Visibility
|
||||
val spoiler = intent.getStringExtra(NotificationHelper.KEY_SPOILER) ?: ""
|
||||
val mentions = intent.getStringArrayExtra(NotificationHelper.KEY_MENTIONS) ?: emptyArray()
|
||||
|
||||
val account = accountManager.getAccountById(senderId)
|
||||
|
||||
val notificationManager = NotificationManagerCompat.from(context)
|
||||
|
||||
val message = getReplyMessage(intent)
|
||||
|
||||
|
@ -109,9 +106,15 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() {
|
|||
|
||||
context.startService(sendIntent)
|
||||
|
||||
val color = if (BuildConfig.FLAVOR == "green") {
|
||||
Color.parseColor("#19A341")
|
||||
} else {
|
||||
ContextCompat.getColor(context, R.color.tusky_blue)
|
||||
}
|
||||
|
||||
val builder = NotificationCompat.Builder(context, NotificationHelper.CHANNEL_MENTION + senderIdentifier)
|
||||
.setSmallIcon(R.drawable.ic_notify)
|
||||
.setColor(ContextCompat.getColor(context, (R.color.tusky_blue)))
|
||||
.setColor(color)
|
||||
.setGroup(senderFullName)
|
||||
.setDefaults(0) // So it doesn't ring twice, notify only in Target callback
|
||||
|
||||
|
@ -125,29 +128,6 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() {
|
|||
|
||||
notificationManager.notify(notificationId, builder.build())
|
||||
}
|
||||
} else if (intent.action == NotificationHelper.COMPOSE_ACTION) {
|
||||
|
||||
context.sendBroadcast(Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS))
|
||||
|
||||
notificationManager.cancel(notificationId)
|
||||
|
||||
accountManager.setActiveAccount(senderId)
|
||||
|
||||
val composeIntent = ComposeActivity.startIntent(
|
||||
context,
|
||||
ComposeOptions(
|
||||
inReplyToId = citedStatusId,
|
||||
replyVisibility = visibility,
|
||||
contentWarning = spoiler,
|
||||
mentionedUsernames = mentions.toSet(),
|
||||
replyingStatusAuthor = localAuthorId,
|
||||
replyingStatusContent = citedText
|
||||
)
|
||||
)
|
||||
|
||||
composeIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
|
||||
context.startActivity(composeIntent)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -19,8 +19,8 @@ import com.keylesspalace.tusky.appstore.EventHub
|
|||
import com.keylesspalace.tusky.appstore.StatusComposedEvent
|
||||
import com.keylesspalace.tusky.appstore.StatusScheduledEvent
|
||||
import com.keylesspalace.tusky.components.drafts.DraftHelper
|
||||
import com.keylesspalace.tusky.components.notifications.NotificationHelper
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.db.AppDatabase
|
||||
import com.keylesspalace.tusky.di.Injectable
|
||||
import com.keylesspalace.tusky.entity.NewPoll
|
||||
import com.keylesspalace.tusky.entity.NewStatus
|
||||
|
@ -50,8 +50,6 @@ class SendTootService : Service(), Injectable {
|
|||
@Inject
|
||||
lateinit var eventHub: EventHub
|
||||
@Inject
|
||||
lateinit var database: AppDatabase
|
||||
@Inject
|
||||
lateinit var draftHelper: DraftHelper
|
||||
|
||||
private val supervisorJob = SupervisorJob()
|
||||
|
@ -95,7 +93,7 @@ class SendTootService : Service(), Injectable {
|
|||
.setContentText(notificationText)
|
||||
.setProgress(1, 0, true)
|
||||
.setOngoing(true)
|
||||
.setColor(ContextCompat.getColor(this, R.color.tusky_blue))
|
||||
.setColor(ContextCompat.getColor(this, R.color.notification_color))
|
||||
.addAction(0, getString(android.R.string.cancel), cancelSendingIntent(sendingNotificationId))
|
||||
|
||||
if (tootsToSend.size == 0 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
|
@ -183,7 +181,7 @@ class SendTootService : Service(), Injectable {
|
|||
.setSmallIcon(R.drawable.ic_notify)
|
||||
.setContentTitle(getString(R.string.send_toot_notification_error_title))
|
||||
.setContentText(getString(R.string.send_toot_notification_saved_content))
|
||||
.setColor(ContextCompat.getColor(this@SendTootService, R.color.tusky_blue))
|
||||
.setColor(ContextCompat.getColor(this@SendTootService, R.color.notification_color))
|
||||
|
||||
notificationManager.cancel(tootId)
|
||||
notificationManager.notify(errorNotificationId--, builder.build())
|
||||
|
@ -232,7 +230,7 @@ class SendTootService : Service(), Injectable {
|
|||
.setSmallIcon(R.drawable.ic_notify)
|
||||
.setContentTitle(getString(R.string.send_toot_notification_cancel_title))
|
||||
.setContentText(getString(R.string.send_toot_notification_saved_content))
|
||||
.setColor(ContextCompat.getColor(this@SendTootService, R.color.tusky_blue))
|
||||
.setColor(ContextCompat.getColor(this, R.color.notification_color))
|
||||
|
||||
notificationManager.notify(tootId, builder.build())
|
||||
|
||||
|
@ -267,12 +265,9 @@ class SendTootService : Service(), Injectable {
|
|||
}
|
||||
|
||||
private fun cancelSendingIntent(tootId: Int): PendingIntent {
|
||||
|
||||
val intent = Intent(this, SendTootService::class.java)
|
||||
|
||||
intent.putExtra(KEY_CANCEL, tootId)
|
||||
|
||||
return PendingIntent.getService(this, tootId, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
return PendingIntent.getService(this, tootId, intent, NotificationHelper.pendingIntentFlags(false))
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 9.1 KiB |
Binary file not shown.
Before Width: | Height: | Size: 5.1 KiB |
Binary file not shown.
Before Width: | Height: | Size: 12 KiB |
Binary file not shown.
Before Width: | Height: | Size: 22 KiB |
Binary file not shown.
Before Width: | Height: | Size: 29 KiB |
|
@ -1,11 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<color android:color="@color/tusky_grey_20"/>
|
||||
</item>
|
||||
<item>
|
||||
<bitmap
|
||||
android:src="@drawable/splash"
|
||||
android:gravity="center" />
|
||||
</item>
|
||||
</layer-list>
|
15
app/src/main/res/drawable/ic_splash.xml
Normal file
15
app/src/main/res/drawable/ic_splash.xml
Normal file
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_height="240dp"
|
||||
android:layout_width="240dp">
|
||||
|
||||
<item android:drawable="@drawable/ic_launcher_background" />
|
||||
|
||||
<item
|
||||
android:top="40dp"
|
||||
android:bottom="40dp"
|
||||
android:left="40dp"
|
||||
android:right="40dp"
|
||||
android:drawable="@drawable/ic_launcher_foreground" />
|
||||
|
||||
</layer-list>
|
|
@ -1,15 +1,5 @@
|
|||
<resources>
|
||||
|
||||
<style name="SplashTheme" parent="Theme.AppCompat.NoActionBar">
|
||||
<item name="android:windowBackground">@drawable/background_splash</item>
|
||||
<item name="colorPrimary">@color/tusky_grey_20</item>
|
||||
<item name="colorPrimaryDark">@color/tusky_grey_20</item>
|
||||
<item name="android:windowNoTitle">true</item>
|
||||
<item name="android:windowLightNavigationBar">false</item>
|
||||
<item name="android:navigationBarColor">@color/tusky_grey_20</item>
|
||||
<item name="android:navigationBarDividerColor">@color/tusky_grey_25</item>
|
||||
</style>
|
||||
|
||||
<style name="TuskyTheme" parent="TuskyBaseTheme">
|
||||
<item name="android:windowLightNavigationBar">@bool/lightNavigationBar</item>
|
||||
<item name="android:navigationBarColor">@color/colorBackground</item>
|
||||
|
|
|
@ -11,6 +11,8 @@
|
|||
<color name="white">#fff</color>
|
||||
<color name="black">#000</color>
|
||||
|
||||
<color name="notification_color">@color/tusky_blue</color>
|
||||
|
||||
<!-- the number roughly corresponds to the % lightness of the grey -->
|
||||
<color name="tusky_grey_05">#070b14</color>
|
||||
<color name="tusky_grey_10">#16191f</color>
|
||||
|
@ -24,7 +26,6 @@
|
|||
<color name="tusky_grey_90">#d9e1e8</color>
|
||||
<color name="tusky_grey_95">#ebeff4</color>
|
||||
|
||||
|
||||
<color name="transparent_tusky_blue">#8c2b90d9</color>
|
||||
<color name="transparent_black">#8f000000</color>
|
||||
<color name="header_background_filter_dark">#44000000</color>
|
||||
|
|
|
@ -30,11 +30,10 @@
|
|||
<item name="status_text_large">22sp</item>
|
||||
</style>
|
||||
|
||||
<style name="SplashTheme" parent="Theme.MaterialComponents.Light.NoActionBar">
|
||||
<item name="android:windowBackground">@drawable/background_splash</item>
|
||||
<item name="colorPrimary">@color/tusky_grey_10</item>
|
||||
<item name="colorPrimaryDark">@color/tusky_grey_10</item>
|
||||
<item name="android:windowNoTitle">true</item>
|
||||
<style name="SplashTheme" parent="Theme.SplashScreen">
|
||||
<item name="windowSplashScreenAnimatedIcon">@drawable/ic_splash</item>
|
||||
<item name="windowSplashScreenBackground">@color/tusky_grey_20</item>
|
||||
<item name="postSplashScreenTheme">@style/TuskyTheme</item>
|
||||
</style>
|
||||
|
||||
<style name="TuskyTheme" parent="TuskyBaseTheme" />
|
||||
|
|
|
@ -1,216 +0,0 @@
|
|||
package com.keylesspalace.tusky.components.timeline
|
||||
|
||||
import android.os.Looper
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
import androidx.paging.AsyncPagingDataDiffer
|
||||
import androidx.paging.ExperimentalPagingApi
|
||||
import androidx.recyclerview.widget.ListUpdateCallback
|
||||
import androidx.room.Room
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.google.gson.Gson
|
||||
import com.keylesspalace.tusky.appstore.EventHub
|
||||
import com.keylesspalace.tusky.components.timeline.TimelinePagingAdapter.Companion.TimelineDifferCallback
|
||||
import com.keylesspalace.tusky.components.timeline.viewmodel.CachedTimelineViewModel
|
||||
import com.keylesspalace.tusky.components.timeline.viewmodel.NetworkTimelineViewModel
|
||||
import com.keylesspalace.tusky.components.timeline.viewmodel.TimelineViewModel
|
||||
import com.keylesspalace.tusky.db.AccountEntity
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.db.AppDatabase
|
||||
import com.keylesspalace.tusky.db.Converters
|
||||
import com.keylesspalace.tusky.network.FilterModel
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.network.TimelineCases
|
||||
import com.nhaarman.mockitokotlin2.doReturn
|
||||
import com.nhaarman.mockitokotlin2.mock
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.take
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.test.TestCoroutineDispatcher
|
||||
import kotlinx.coroutines.test.TestCoroutineScope
|
||||
import kotlinx.coroutines.test.resetMain
|
||||
import kotlinx.coroutines.test.setMain
|
||||
import okhttp3.Headers
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.Shadows.shadowOf
|
||||
import org.robolectric.annotation.Config
|
||||
import retrofit2.Response
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
@Config(sdk = [29])
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class TimelineViewModelTest {
|
||||
|
||||
@get:Rule
|
||||
val instantRule = InstantTaskExecutorRule()
|
||||
|
||||
private val testDispatcher = TestCoroutineDispatcher()
|
||||
private val testScope = TestCoroutineScope(testDispatcher)
|
||||
|
||||
private val accountManager: AccountManager = mock {
|
||||
on { activeAccount } doReturn AccountEntity(
|
||||
id = 1,
|
||||
domain = "mastodon.example",
|
||||
accessToken = "token",
|
||||
isActive = true
|
||||
)
|
||||
}
|
||||
|
||||
private val eventHub = EventHub()
|
||||
|
||||
private lateinit var db: AppDatabase
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
Dispatchers.setMain(testDispatcher)
|
||||
|
||||
shadowOf(Looper.getMainLooper()).idle()
|
||||
|
||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
db = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java)
|
||||
.addTypeConverter(Converters(Gson()))
|
||||
.setTransactionExecutor(Executors.newSingleThreadExecutor())
|
||||
.allowMainThreadQueries()
|
||||
.build()
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
Dispatchers.resetMain()
|
||||
testDispatcher.cleanupTestCoroutines()
|
||||
db.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
@ExperimentalPagingApi
|
||||
fun shouldLoadNetworkTimeline() = runBlocking {
|
||||
|
||||
val api: MastodonApi = mock {
|
||||
on { publicTimeline(local = true, maxId = null, sinceId = null, limit = 30) } doReturn Single.just(
|
||||
Response.success(
|
||||
listOf(
|
||||
mockStatus("6"),
|
||||
mockStatus("5"),
|
||||
mockStatus("4")
|
||||
),
|
||||
Headers.headersOf(
|
||||
"Link", "<https://mastodon.examples/api/v1/favourites?limit=30&max_id=1>; rel=\"next\", <https://mastodon.example/api/v1/favourites?limit=30&min_id=5>; rel=\"prev\""
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
on { publicTimeline(local = true, maxId = "1", sinceId = null, limit = 30) } doReturn Single.just(
|
||||
Response.success(emptyList())
|
||||
)
|
||||
|
||||
on { getFilters() } doReturn Single.just(emptyList())
|
||||
}
|
||||
|
||||
val viewModel = NetworkTimelineViewModel(
|
||||
TimelineCases(api, eventHub),
|
||||
api,
|
||||
eventHub,
|
||||
accountManager,
|
||||
mock(),
|
||||
FilterModel()
|
||||
)
|
||||
|
||||
viewModel.init(TimelineViewModel.Kind.PUBLIC_LOCAL, null, emptyList())
|
||||
|
||||
val differ = AsyncPagingDataDiffer(
|
||||
diffCallback = TimelineDifferCallback,
|
||||
updateCallback = NoopListCallback(),
|
||||
workerDispatcher = testDispatcher
|
||||
)
|
||||
|
||||
viewModel.statuses.take(2).collectLatest {
|
||||
testScope.launch {
|
||||
differ.submitData(it)
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(
|
||||
listOf(
|
||||
mockStatusViewData("6"),
|
||||
mockStatusViewData("5"),
|
||||
mockStatusViewData("4")
|
||||
),
|
||||
differ.snapshot().items
|
||||
)
|
||||
}
|
||||
|
||||
// ToDo: Find out why Room & coroutines are not playing nice here
|
||||
// @Test
|
||||
@ExperimentalPagingApi
|
||||
fun shouldLoadCachedTimeline() = runBlocking {
|
||||
|
||||
val api: MastodonApi = mock {
|
||||
on { homeTimeline(limit = 30) } doReturn Single.just(
|
||||
Response.success(
|
||||
listOf(
|
||||
mockStatus("6"),
|
||||
mockStatus("5"),
|
||||
mockStatus("4")
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
on { homeTimeline(maxId = "1", sinceId = null, limit = 30) } doReturn Single.just(
|
||||
Response.success(emptyList())
|
||||
)
|
||||
|
||||
on { getFilters() } doReturn Single.just(emptyList())
|
||||
}
|
||||
|
||||
val viewModel = CachedTimelineViewModel(
|
||||
TimelineCases(api, eventHub),
|
||||
api,
|
||||
eventHub,
|
||||
accountManager,
|
||||
mock(),
|
||||
FilterModel(),
|
||||
db,
|
||||
Gson()
|
||||
)
|
||||
|
||||
viewModel.init(TimelineViewModel.Kind.HOME, null, emptyList())
|
||||
|
||||
val differ = AsyncPagingDataDiffer(
|
||||
diffCallback = TimelineDifferCallback,
|
||||
updateCallback = NoopListCallback(),
|
||||
workerDispatcher = testDispatcher
|
||||
)
|
||||
|
||||
viewModel.statuses.take(1000).collectLatest {
|
||||
testScope.launch {
|
||||
differ.submitData(it)
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(
|
||||
listOf(
|
||||
mockStatusViewData("6"),
|
||||
mockStatusViewData("5"),
|
||||
mockStatusViewData("4")
|
||||
),
|
||||
differ.snapshot().items
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class NoopListCallback : ListUpdateCallback {
|
||||
override fun onChanged(position: Int, count: Int, payload: Any?) {}
|
||||
override fun onMoved(fromPosition: Int, toPosition: Int) {}
|
||||
override fun onInserted(position: Int, count: Int) {}
|
||||
override fun onRemoved(position: Int, count: Int) {}
|
||||
}
|
Loading…
Reference in a new issue