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…
	
	Add table
		Add a link
		
	
		Reference in a new issue