Custom emojis in drawer (#737)
* upgrade MaterialDrawer * improve CustomEmojiHelper so now any parent view can be used for invalidation * cleanup MainActivity a bit * add emojiList to account database and show compatEmojis and custom emojis in drawer * improve perf of drawer profile update * fix account switching * reuse gson, break after profile item was found
This commit is contained in:
		
					parent
					
						
							
								b5a8915845
							
						
					
				
			
			
				commit
				
					
						9b422a97fe
					
				
			
		
					 9 changed files with 99 additions and 64 deletions
				
			
		|  | @ -65,7 +65,7 @@ ext.daggerVersion = '2.16' | |||
| 
 | ||||
| // if libraries are changed here, they should also be changed in LicenseActivity | ||||
| dependencies { | ||||
|     implementation('com.mikepenz:materialdrawer:6.0.7@aar') { | ||||
|     implementation('com.mikepenz:materialdrawer:6.0.9@aar') { | ||||
|         transitive = true | ||||
|     } | ||||
|     implementation "com.android.support:appcompat-v7:$supportLibraryVersion" | ||||
|  |  | |||
|  | @ -26,6 +26,7 @@ import android.support.annotation.Nullable; | |||
| import android.support.design.widget.FloatingActionButton; | ||||
| import android.support.design.widget.TabLayout; | ||||
| import android.support.graphics.drawable.VectorDrawableCompat; | ||||
| import android.support.text.emoji.EmojiCompat; | ||||
| import android.support.v4.app.Fragment; | ||||
| import android.support.v4.content.ContextCompat; | ||||
| import android.support.v4.view.ViewPager; | ||||
|  | @ -39,6 +40,7 @@ import com.keylesspalace.tusky.db.AccountEntity; | |||
| import com.keylesspalace.tusky.entity.Account; | ||||
| import com.keylesspalace.tusky.interfaces.ActionButtonActivity; | ||||
| import com.keylesspalace.tusky.pager.TimelinePagerAdapter; | ||||
| import com.keylesspalace.tusky.util.CustomEmojiHelper; | ||||
| import com.keylesspalace.tusky.util.NotificationHelper; | ||||
| import com.keylesspalace.tusky.util.ThemeUtils; | ||||
| import com.mikepenz.google_material_typeface_library.GoogleMaterial; | ||||
|  | @ -71,6 +73,7 @@ import retrofit2.Response; | |||
| 
 | ||||
| public final class MainActivity extends BottomSheetActivity implements ActionButtonActivity, | ||||
|         HasSupportFragmentInjector { | ||||
| 
 | ||||
|     private static final String TAG = "MainActivity"; // logging tag | ||||
|     private static final long DRAWER_ITEM_ADD_ACCOUNT = -13; | ||||
|     private static final long DRAWER_ITEM_EDIT_PROFILE = 0; | ||||
|  | @ -88,8 +91,6 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut | |||
|     @Inject | ||||
|     public DispatchingAndroidInjector<Fragment> fragmentInjector; | ||||
| 
 | ||||
|     private static int COMPOSE_RESULT = 1; | ||||
| 
 | ||||
|     private FloatingActionButton composeButton; | ||||
|     private AccountHeader headerResult; | ||||
|     private Drawer drawer; | ||||
|  | @ -125,7 +126,7 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut | |||
| 
 | ||||
|         floatingBtn.setOnClickListener(v -> { | ||||
|             Intent composeIntent = new Intent(getApplicationContext(), ComposeActivity.class); | ||||
|             startActivityForResult(composeIntent, COMPOSE_RESULT); | ||||
|             startActivity(composeIntent); | ||||
|         }); | ||||
| 
 | ||||
|         setupDrawer(); | ||||
|  | @ -305,29 +306,25 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut | |||
|                 R.drawable.ic_mute_24dp, getTheme()); | ||||
|         ThemeUtils.setDrawableTint(this, muteDrawable, R.attr.toolbar_icon_tint); | ||||
| 
 | ||||
|         List<IDrawerItem> listItem = new ArrayList<>(); | ||||
|         listItem.add(new PrimaryDrawerItem().withIdentifier(DRAWER_ITEM_EDIT_PROFILE).withName(getString(R.string.action_edit_profile)).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_person)); | ||||
|         listItem.add(new PrimaryDrawerItem().withIdentifier(DRAWER_ITEM_FAVOURITES).withName(getString(R.string.action_view_favourites)).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_star)); | ||||
|         listItem.add(new PrimaryDrawerItem().withIdentifier(DRAWER_ITEM_LISTS).withName(R.string.action_lists).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_list)); | ||||
|         listItem.add(new PrimaryDrawerItem().withIdentifier(DRAWER_ITEM_MUTED_USERS).withName(getString(R.string.action_view_mutes)).withSelectable(false).withIcon(muteDrawable)); | ||||
|         listItem.add(new PrimaryDrawerItem().withIdentifier(DRAWER_ITEM_BLOCKED_USERS).withName(getString(R.string.action_view_blocks)).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_block)); | ||||
|         listItem.add(new PrimaryDrawerItem().withIdentifier(DRAWER_ITEM_SEARCH).withName(getString(R.string.action_search)).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_search)); | ||||
|         listItem.add(new PrimaryDrawerItem().withIdentifier(DRAWER_ITEM_SAVED_TOOT).withName(getString(R.string.action_access_saved_toot)).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_save)); | ||||
|         listItem.add(new DividerDrawerItem()); | ||||
|         listItem.add(new SecondaryDrawerItem().withIdentifier(DRAWER_ITEM_PREFERENCES).withName(getString(R.string.action_view_preferences)).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_settings)); | ||||
|         listItem.add(new SecondaryDrawerItem().withIdentifier(DRAWER_ITEM_ABOUT).withName(getString(R.string.about_title_activity)).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_info)); | ||||
|         listItem.add(new SecondaryDrawerItem().withIdentifier(DRAWER_ITEM_LOG_OUT).withName(getString(R.string.action_logout)).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_exit_to_app)); | ||||
| 
 | ||||
|         IDrawerItem[] array = new IDrawerItem[listItem.size()]; | ||||
|         listItem.toArray(array); // fill the array | ||||
|         List<IDrawerItem> listItems = new ArrayList<>(11); | ||||
|         listItems.add(new PrimaryDrawerItem().withIdentifier(DRAWER_ITEM_EDIT_PROFILE).withName(R.string.action_edit_profile).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_person)); | ||||
|         listItems.add(new PrimaryDrawerItem().withIdentifier(DRAWER_ITEM_FAVOURITES).withName(R.string.action_view_favourites).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_star)); | ||||
|         listItems.add(new PrimaryDrawerItem().withIdentifier(DRAWER_ITEM_LISTS).withName(R.string.action_lists).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_list)); | ||||
|         listItems.add(new PrimaryDrawerItem().withIdentifier(DRAWER_ITEM_MUTED_USERS).withName(R.string.action_view_mutes).withSelectable(false).withIcon(muteDrawable)); | ||||
|         listItems.add(new PrimaryDrawerItem().withIdentifier(DRAWER_ITEM_BLOCKED_USERS).withName(R.string.action_view_blocks).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_block)); | ||||
|         listItems.add(new PrimaryDrawerItem().withIdentifier(DRAWER_ITEM_SEARCH).withName(R.string.action_search).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_search)); | ||||
|         listItems.add(new PrimaryDrawerItem().withIdentifier(DRAWER_ITEM_SAVED_TOOT).withName(R.string.action_access_saved_toot).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_save)); | ||||
|         listItems.add(new DividerDrawerItem()); | ||||
|         listItems.add(new SecondaryDrawerItem().withIdentifier(DRAWER_ITEM_PREFERENCES).withName(R.string.action_view_preferences).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_settings)); | ||||
|         listItems.add(new SecondaryDrawerItem().withIdentifier(DRAWER_ITEM_ABOUT).withName(R.string.about_title_activity).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_info)); | ||||
|         listItems.add(new SecondaryDrawerItem().withIdentifier(DRAWER_ITEM_LOG_OUT).withName(R.string.action_logout).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_exit_to_app)); | ||||
| 
 | ||||
|         drawer = new DrawerBuilder() | ||||
|                 .withActivity(this) | ||||
|                 //.withToolbar(toolbar) | ||||
|                 .withAccountHeader(headerResult) | ||||
|                 .withHasStableIds(true) | ||||
|                 .withSelectedItem(-1) | ||||
|                 .addDrawerItems(array) | ||||
|                 .withDrawerItems(listItems) | ||||
|                 .withOnDrawerItemClickListener((view, position, drawerItem) -> { | ||||
|                     if (drawerItem != null) { | ||||
|                         long drawerItemIdentifier = drawerItem.getIdentifier(); | ||||
|  | @ -490,7 +487,7 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut | |||
|                     .withSelectable(false) | ||||
|                     .withIcon(GoogleMaterial.Icon.gmd_person_add); | ||||
|             drawer.addItemAtPosition(followRequestsItem, 3); | ||||
|         } else { | ||||
|         } else if(!me.getLocked()){ | ||||
|             drawer.removeItem(DRAWER_ITEM_FOLLOW_REQUESTS); | ||||
|         } | ||||
| 
 | ||||
|  | @ -502,24 +499,29 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut | |||
| 
 | ||||
|         List<AccountEntity> allAccounts = accountManager.getAllAccountsOrderedByActive(); | ||||
| 
 | ||||
|         //remove profiles before adding them again to avoid duplicates | ||||
|         List<IProfile> profiles = new ArrayList<>(headerResult.getProfiles()); | ||||
|         for (IProfile profile : profiles) { | ||||
|             if (profile.getIdentifier() != DRAWER_ITEM_ADD_ACCOUNT) { | ||||
|                 headerResult.removeProfile(profile); | ||||
|         // reuse the already existing "add account" item | ||||
|         List<IProfile> profiles = new ArrayList<>(allAccounts.size()+1); | ||||
|         for (IProfile profile: headerResult.getProfiles()) { | ||||
|             if (profile.getIdentifier() == DRAWER_ITEM_ADD_ACCOUNT) { | ||||
|                 profiles.add(profile); | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         for (AccountEntity acc : allAccounts) { | ||||
|             headerResult.addProfiles( | ||||
|             CharSequence emojifiedName = CustomEmojiHelper.emojifyString(acc.getDisplayName(), acc.getEmojis(), headerResult.getView()); | ||||
|             emojifiedName = EmojiCompat.get().process(emojifiedName); | ||||
| 
 | ||||
|             profiles.add(0, | ||||
|                     new ProfileDrawerItem() | ||||
|                             .withName(acc.getDisplayName()) | ||||
|                             .withName(emojifiedName) | ||||
|                             .withIcon(acc.getProfilePictureUrl()) | ||||
|                             .withNameShown(true) | ||||
|                             .withIdentifier(acc.getId()) | ||||
|                             .withEmail(acc.getFullName())); | ||||
| 
 | ||||
|         } | ||||
|         headerResult.setProfiles(profiles); | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -70,7 +70,8 @@ public class TuskyApplication extends Application implements HasActivityInjector | |||
| 
 | ||||
|         appDatabase = Room.databaseBuilder(getApplicationContext(), AppDatabase.class, "tuskyDB") | ||||
|                 .allowMainThreadQueries() | ||||
|                 .addMigrations(AppDatabase.MIGRATION_2_3, AppDatabase.MIGRATION_3_4, AppDatabase.MIGRATION_4_5, AppDatabase.MIGRATION_5_6, AppDatabase.MIGRATION_6_7) | ||||
|                 .addMigrations(AppDatabase.MIGRATION_2_3, AppDatabase.MIGRATION_3_4, AppDatabase.MIGRATION_4_5, | ||||
|                         AppDatabase.MIGRATION_5_6, AppDatabase.MIGRATION_6_7, AppDatabase.MIGRATION_7_8) | ||||
|                 .build(); | ||||
|         accountManager = new AccountManager(appDatabase); | ||||
|         serviceLocator = new ServiceLocator() { | ||||
|  |  | |||
|  | @ -18,9 +18,13 @@ package com.keylesspalace.tusky.db | |||
| import android.arch.persistence.room.Entity | ||||
| import android.arch.persistence.room.Index | ||||
| import android.arch.persistence.room.PrimaryKey | ||||
| import android.arch.persistence.room.TypeConverters | ||||
| 
 | ||||
| import com.keylesspalace.tusky.entity.Emoji | ||||
| 
 | ||||
| @Entity(indices = [Index(value = ["domain", "accountId"], | ||||
|         unique = true)]) | ||||
| @TypeConverters(Converters::class) | ||||
| data class AccountEntity(@field:PrimaryKey(autoGenerate = true) var id: Long, | ||||
|                          val domain: String, | ||||
|                          var accessToken: String, | ||||
|  | @ -38,7 +42,8 @@ data class AccountEntity(@field:PrimaryKey(autoGenerate = true) var id: Long, | |||
|                          var notificationVibration: Boolean = true, | ||||
|                          var notificationLight: Boolean = true, | ||||
|                          var lastNotificationId: String = "0", | ||||
|                          var activeNotifications: String = "[]") { | ||||
|                          var activeNotifications: String = "[]", | ||||
|                          var emojis: List<Emoji> = emptyList()) { | ||||
| 
 | ||||
|     val identifier: String | ||||
|         get() = "$domain:$accountId" | ||||
|  |  | |||
|  | @ -111,6 +111,7 @@ class AccountManager(db: AppDatabase) { | |||
|             it.username = account.username | ||||
|             it.displayName = account.name | ||||
|             it.profilePictureUrl = account.avatar | ||||
|             it.emojis = account.emojis ?: emptyList() | ||||
| 
 | ||||
|             Log.d(TAG, "updateActiveAccount: saving account with id " + it.id) | ||||
|             it.id = accountDao.insertOrReplace(it) | ||||
|  |  | |||
|  | @ -25,7 +25,7 @@ import android.support.annotation.NonNull; | |||
|  * DB version & declare DAO | ||||
|  */ | ||||
| 
 | ||||
| @Database(entities = {TootEntity.class, AccountEntity.class, InstanceEntity.class}, version = 7, exportSchema = false) | ||||
| @Database(entities = {TootEntity.class, AccountEntity.class, InstanceEntity.class}, version = 8, exportSchema = false) | ||||
| public abstract class AppDatabase extends RoomDatabase { | ||||
| 
 | ||||
|     public abstract TootDao tootDao(); | ||||
|  | @ -87,8 +87,15 @@ public abstract class AppDatabase extends RoomDatabase { | |||
|         @Override | ||||
|         public void migrate(@NonNull SupportSQLiteDatabase database) { | ||||
|             database.execSQL("CREATE TABLE IF NOT EXISTS `InstanceEntity` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, PRIMARY KEY(`instance`))"); | ||||
|             database.execSQL("INSERT OR REPLACE INTO `InstanceEntity` SELECT `instance`,`emojiList`,NULL FROM `EmojiListEntity`;"); | ||||
|             database.execSQL("INSERT OR REPLACE INTO `InstanceEntity` SELECT `instance`,`emojiList`, NULL FROM `EmojiListEntity`;"); | ||||
|             database.execSQL("DROP TABLE `EmojiListEntity`;"); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     public static final Migration MIGRATION_7_8 = new Migration(7, 8) { | ||||
|         @Override | ||||
|         public void migrate(@NonNull SupportSQLiteDatabase database) { | ||||
|             database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `emojis` TEXT NOT NULL DEFAULT '[]'"); | ||||
|         } | ||||
|     }; | ||||
| } | ||||
							
								
								
									
										36
									
								
								app/src/main/java/com/keylesspalace/tusky/db/Converters.kt
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								app/src/main/java/com/keylesspalace/tusky/db/Converters.kt
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,36 @@ | |||
| /* 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.db | ||||
| 
 | ||||
| import android.arch.persistence.room.TypeConverter | ||||
| import com.google.gson.Gson | ||||
| import com.google.gson.reflect.TypeToken | ||||
| import com.keylesspalace.tusky.entity.Emoji | ||||
| 
 | ||||
| class Converters { | ||||
| 
 | ||||
|     private val gson = Gson() | ||||
| 
 | ||||
|     @TypeConverter | ||||
|     fun jsonToEmojiList(emojiListJson: String?): List<Emoji>? { | ||||
|         return gson.fromJson(emojiListJson, object : TypeToken<List<Emoji>>() {}.type) | ||||
|     } | ||||
| 
 | ||||
|     @TypeConverter | ||||
|     fun emojiListToJson(emojiList: List<Emoji>?): String { | ||||
|         return gson.toJson(emojiList) | ||||
|     } | ||||
| } | ||||
|  | @ -17,10 +17,7 @@ package com.keylesspalace.tusky.db | |||
| 
 | ||||
| import android.arch.persistence.room.Entity | ||||
| import android.arch.persistence.room.PrimaryKey | ||||
| import android.arch.persistence.room.TypeConverter | ||||
| import android.arch.persistence.room.TypeConverters | ||||
| import com.google.gson.Gson | ||||
| import com.google.gson.reflect.TypeToken | ||||
| import com.keylesspalace.tusky.entity.Emoji | ||||
| 
 | ||||
| @Entity | ||||
|  | @ -29,17 +26,3 @@ data class InstanceEntity( | |||
|         @field:PrimaryKey var instance: String, | ||||
|         val emojiList: List<Emoji>?, | ||||
|         val maximumTootCharacters: Int?) | ||||
| 
 | ||||
| 
 | ||||
| class Converters { | ||||
| 
 | ||||
|     @TypeConverter | ||||
|     fun jsonToList(emojiListJson: String?): List<Emoji>? { | ||||
|         return Gson().fromJson(emojiListJson, object : TypeToken<List<Emoji>>() {}.type) | ||||
|     } | ||||
| 
 | ||||
|     @TypeConverter | ||||
|     fun listToJson(emojiList: List<Emoji>?): String { | ||||
|         return Gson().toJson(emojiList) | ||||
|     } | ||||
| } | ||||
|  | @ -26,7 +26,7 @@ import android.text.SpannableStringBuilder; | |||
| import android.text.Spanned; | ||||
| import android.text.SpannedString; | ||||
| import android.text.style.ReplacementSpan; | ||||
| import android.widget.TextView; | ||||
| import android.view.View; | ||||
| 
 | ||||
| import com.keylesspalace.tusky.entity.Emoji; | ||||
| import com.squareup.picasso.Picasso; | ||||
|  | @ -43,10 +43,10 @@ public class CustomEmojiHelper { | |||
|      * replaces emoji shortcodes in a text with EmojiSpans | ||||
|      * @param text the text containing custom emojis | ||||
|      * @param emojis a list of the custom emojis (nullable for backward compatibility with old mastodon instances) | ||||
|      * @param textView a reference to the textView the emojis will be shown in | ||||
|      * @param view a reference to the a view the emojis will be shown in (should be the TextView, but parents of the TextView are also acceptable) | ||||
|      * @return the text with the shortcodes replaced by EmojiSpans | ||||
|      */ | ||||
|     public static Spanned emojifyText(@NonNull Spanned text, @Nullable List<Emoji> emojis, @NonNull final TextView textView) { | ||||
|     public static Spanned emojifyText(@NonNull Spanned text, @Nullable List<Emoji> emojis, @NonNull final View view) { | ||||
| 
 | ||||
|         if (emojis != null && !emojis.isEmpty()) { | ||||
| 
 | ||||
|  | @ -57,9 +57,9 @@ public class CustomEmojiHelper { | |||
|                 while (matcher.find()) { | ||||
|                     // We keep a span as a Picasso target, because Picasso keeps weak reference to | ||||
|                     // the target so an anonymous class would likely be garbage collected. | ||||
|                     EmojiSpan span = new EmojiSpan(textView); | ||||
|                     EmojiSpan span = new EmojiSpan(view); | ||||
|                     builder.setSpan(span, matcher.start(), matcher.end(), 0); | ||||
|                     Picasso.with(textView.getContext()) | ||||
|                     Picasso.with(view.getContext()) | ||||
|                             .load(emoji.getUrl()) | ||||
|                             .into(span); | ||||
|                 } | ||||
|  | @ -71,18 +71,18 @@ public class CustomEmojiHelper { | |||
|         return text; | ||||
|     } | ||||
| 
 | ||||
|     public static Spanned emojifyString(@NonNull String string, @Nullable List<Emoji> emojis, @NonNull final TextView textView) { | ||||
|         return emojifyText(new SpannedString(string), emojis, textView); | ||||
|     public static Spanned emojifyString(@NonNull String string, @Nullable List<Emoji> emojis, @NonNull final View ciew) { | ||||
|         return emojifyText(new SpannedString(string), emojis, ciew); | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     public static class EmojiSpan extends ReplacementSpan implements Target { | ||||
| 
 | ||||
|         private @Nullable Drawable imageDrawable; | ||||
|         private WeakReference<TextView> textViewWeakReference; | ||||
|         private WeakReference<View> viewWeakReference; | ||||
| 
 | ||||
|         EmojiSpan(TextView textView) { | ||||
|             this.textViewWeakReference = new WeakReference<>(textView); | ||||
|         EmojiSpan(View view) { | ||||
|             this.viewWeakReference = new WeakReference<>(view); | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|  | @ -120,10 +120,10 @@ public class CustomEmojiHelper { | |||
| 
 | ||||
|         @Override | ||||
|         public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) { | ||||
|             TextView textView = textViewWeakReference.get(); | ||||
|             if(textView != null) { | ||||
|                 imageDrawable = new BitmapDrawable(textView.getContext().getResources(), bitmap); | ||||
|                 textView.invalidate(); | ||||
|             View view = viewWeakReference.get(); | ||||
|             if(view != null) { | ||||
|                 imageDrawable = new BitmapDrawable(view.getContext().getResources(), bitmap); | ||||
|                 view.invalidate(); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue