Update to 2.8.1

Thanks to https://github.com/DrKLO/Telegram/issues/738 for smart
notifications
This commit is contained in:
DrKLO 2015-05-03 14:48:36 +03:00
parent f6655078a1
commit 2b8304ebf4
557 changed files with 42344 additions and 13434 deletions

View File

@ -13,11 +13,10 @@ repositories {
}
dependencies {
compile 'com.android.support:support-v4:22.0.+'
compile 'com.android.support:support-v4:22.1.+'
compile 'com.google.android.gms:play-services:3.2.+'
compile 'net.hockeyapp.android:HockeySDK:3.5.+'
compile 'com.googlecode.mp4parser:isoparser:1.0.+'
compile 'com.android.support:recyclerview-v7:+'
}
android {
@ -82,7 +81,7 @@ android {
defaultConfig {
minSdkVersion 8
targetSdkVersion 22
versionCode 492
versionName "2.7.0"
versionCode 521
versionName "2.8.1"
}
}

View File

@ -104,7 +104,7 @@ include $(BUILD_STATIC_LIBRARY)
include $(CLEAR_VARS)
LOCAL_PRELINK_MODULE := false
LOCAL_STATIC_LIBRARIES := webp sqlite
LOCAL_MODULE := tmessages.7
LOCAL_MODULE := tmessages.8
LOCAL_CFLAGS := -w -std=gnu99 -O2 -DNULL=0 -DSOCKLEN_T=socklen_t -DLOCALE_NOT_USED -D_LARGEFILE_SOURCE=1 -D_FILE_OFFSET_BITS=64
LOCAL_CFLAGS += -Drestrict='' -D__EMX__ -DOPUS_BUILD -DFIXED_POINT -DUSE_ALLOCA -DHAVE_LRINT -DHAVE_LRINTF -fno-math-errno
LOCAL_CFLAGS += -DANDROID_NDK -DDISABLE_IMPORTGL -fno-strict-aliasing -fprefetch-loop-arrays -DAVOID_TABLES -DANDROID_TILE_BASED_DECODE -DANDROID_ARMV6_IDCT -ffast-math

View File

@ -288,7 +288,7 @@ METHODDEF(void) my_error_exit(j_common_ptr cinfo) {
longjmp(myerr->setjmp_buffer, 1);
}
JNIEXPORT void Java_org_telegram_messenger_Utilities_blurBitmap(JNIEnv *env, jclass class, jobject bitmap, int radius) {
JNIEXPORT void Java_org_telegram_messenger_Utilities_blurBitmap(JNIEnv *env, jclass class, jobject bitmap, int radius, int unpin) {
if (!bitmap) {
return;
}
@ -312,7 +312,9 @@ JNIEXPORT void Java_org_telegram_messenger_Utilities_blurBitmap(JNIEnv *env, jcl
} else {
fastBlurMore(info.width, info.height, info.stride, pixels, radius);
}
AndroidBitmap_unlockPixels(env, bitmap);
if (unpin) {
AndroidBitmap_unlockPixels(env, bitmap);
}
}
JNIEXPORT void Java_org_telegram_messenger_Utilities_calcCDT(JNIEnv *env, jclass class, jobject hsvBuffer, int width, int height, jobject buffer) {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 237 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 297 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

View File

@ -45,10 +45,10 @@ import org.telegram.messenger.R;
import org.telegram.messenger.TLRPC;
import org.telegram.messenger.ApplicationLoader;
import org.telegram.messenger.UserConfig;
import org.telegram.ui.AnimationCompat.AnimatorListenerAdapterProxy;
import org.telegram.ui.AnimationCompat.AnimatorSetProxy;
import org.telegram.ui.AnimationCompat.ObjectAnimatorProxy;
import org.telegram.ui.AnimationCompat.ViewProxy;
import org.telegram.android.AnimationCompat.AnimatorListenerAdapterProxy;
import org.telegram.android.AnimationCompat.AnimatorSetProxy;
import org.telegram.android.AnimationCompat.ObjectAnimatorProxy;
import org.telegram.android.AnimationCompat.ViewProxy;
import org.telegram.ui.Components.ForegroundDetector;
import org.telegram.ui.Components.NumberPicker;
import org.telegram.ui.Components.TypefaceSpan;
@ -72,6 +72,7 @@ public class AndroidUtilities {
public static Integer photoSize = null;
public static DisplayMetrics displayMetrics = new DisplayMetrics();
public static int leftBaseline;
public static boolean usingHardwareInput;
private static Boolean isTablet = null;
static {
@ -227,21 +228,38 @@ public class AndroidUtilities {
}
public static int dp(float value) {
if (value == 0) {
return 0;
}
return (int)Math.ceil(density * value);
}
public static int compare(int lhs, int rhs) {
if (lhs == rhs) {
return 0;
} else if (lhs > rhs) {
return 1;
}
return -1;
}
public static float dpf2(float value) {
if (value == 0) {
return 0;
}
return density * value;
}
public static void checkDisplaySize() {
try {
WindowManager manager = (WindowManager)ApplicationLoader.applicationContext.getSystemService(Context.WINDOW_SERVICE);
Configuration configuration = ApplicationLoader.applicationContext.getResources().getConfiguration();
usingHardwareInput = configuration.keyboard != Configuration.KEYBOARD_NOKEYS && configuration.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_NO;
WindowManager manager = (WindowManager) ApplicationLoader.applicationContext.getSystemService(Context.WINDOW_SERVICE);
if (manager != null) {
Display display = manager.getDefaultDisplay();
if (display != null) {
display.getMetrics(displayMetrics);
if(android.os.Build.VERSION.SDK_INT < 13) {
if (android.os.Build.VERSION.SDK_INT < 13) {
displaySize.set(display.getWidth(), display.getHeight());
} else {
display.getSize(displaySize);
@ -252,6 +270,38 @@ public class AndroidUtilities {
} catch (Exception e) {
FileLog.e("tmessages", e);
}
/*
keyboardHidden
public static final int KEYBOARDHIDDEN_NO = 1
Constant for keyboardHidden, value corresponding to the keysexposed resource qualifier.
public static final int KEYBOARDHIDDEN_UNDEFINED = 0
Constant for keyboardHidden: a value indicating that no value has been set.
public static final int KEYBOARDHIDDEN_YES = 2
Constant for keyboardHidden, value corresponding to the keyshidden resource qualifier.
hardKeyboardHidden
public static final int HARDKEYBOARDHIDDEN_NO = 1
Constant for hardKeyboardHidden, value corresponding to the physical keyboard being exposed.
public static final int HARDKEYBOARDHIDDEN_UNDEFINED = 0
Constant for hardKeyboardHidden: a value indicating that no value has been set.
public static final int HARDKEYBOARDHIDDEN_YES = 2
Constant for hardKeyboardHidden, value corresponding to the physical keyboard being hidden.
keyboard
public static final int KEYBOARD_12KEY = 3
Constant for keyboard, value corresponding to the 12key resource qualifier.
public static final int KEYBOARD_NOKEYS = 1
Constant for keyboard, value corresponding to the nokeys resource qualifier.
public static final int KEYBOARD_QWERTY = 2
Constant for keyboard, value corresponding to the qwerty resource qualifier.
*/
}
public static float getPixelsInCM(float cm, boolean isX) {
@ -562,6 +612,9 @@ public class AndroidUtilities {
if (start != -1) {
stringBuilder.replace(start, start + 3, "");
end = stringBuilder.indexOf("</b>");
if (end == -1) {
end = stringBuilder.indexOf("<b>");
}
stringBuilder.replace(end, end + 4, "");
bolds.add(start);
bolds.add(end);

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.telegram.ui.Animation;
package org.telegram.android.Animation;
import android.view.animation.Interpolator;

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.telegram.ui.Animation;
package org.telegram.android.Animation;
public abstract class AnimatorListenerAdapter10 implements Animator10.AnimatorListener, Animator10.AnimatorPauseListener {

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.telegram.ui.Animation;
package org.telegram.android.Animation;
import android.view.animation.Interpolator;

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.telegram.ui.Animation;
package org.telegram.android.Animation;
public class FloatEvaluator implements TypeEvaluator<Number> {
public Float evaluate(float fraction, Number startValue, Number endValue) {

View File

@ -14,11 +14,11 @@
* limitations under the License.
*/
package org.telegram.ui.Animation;
package org.telegram.android.Animation;
import android.view.animation.Interpolator;
import org.telegram.ui.Animation.Keyframe.FloatKeyframe;
import org.telegram.android.Animation.Keyframe.FloatKeyframe;
import java.util.ArrayList;

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.telegram.ui.Animation;
package org.telegram.android.Animation;
public abstract class FloatProperty10<T> extends Property<T, Float> {

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.telegram.ui.Animation;
package org.telegram.android.Animation;
public class IntEvaluator implements TypeEvaluator<Integer> {
public Integer evaluate(float fraction, Integer startValue, Integer endValue) {

View File

@ -14,11 +14,11 @@
* limitations under the License.
*/
package org.telegram.ui.Animation;
package org.telegram.android.Animation;
import android.view.animation.Interpolator;
import org.telegram.ui.Animation.Keyframe.IntKeyframe;
import org.telegram.android.Animation.Keyframe.IntKeyframe;
import java.util.ArrayList;

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.telegram.ui.Animation;
package org.telegram.android.Animation;
public abstract class IntProperty<T> extends Property<T, Integer> {

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.telegram.ui.Animation;
package org.telegram.android.Animation;
import android.view.animation.Interpolator;

View File

@ -14,16 +14,16 @@
* limitations under the License.
*/
package org.telegram.ui.Animation;
package org.telegram.android.Animation;
import java.util.ArrayList;
import java.util.Arrays;
import android.util.Log;
import android.view.animation.Interpolator;
import org.telegram.ui.Animation.Keyframe.IntKeyframe;
import org.telegram.ui.Animation.Keyframe.FloatKeyframe;
import org.telegram.ui.Animation.Keyframe.ObjectKeyframe;
import org.telegram.android.Animation.Keyframe.IntKeyframe;
import org.telegram.android.Animation.Keyframe.FloatKeyframe;
import org.telegram.android.Animation.Keyframe.ObjectKeyframe;
class KeyframeSet {

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.telegram.ui.Animation;
package org.telegram.android.Animation;
public class NoSuchPropertyException extends RuntimeException {

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.telegram.ui.Animation;
package org.telegram.android.Animation;
import android.view.View;

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.telegram.ui.Animation;
package org.telegram.android.Animation;
public abstract class Property<T, V> {

View File

@ -14,9 +14,8 @@
* limitations under the License.
*/
package org.telegram.ui.Animation;
package org.telegram.android.Animation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.concurrent.locks.ReentrantReadWriteLock;

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.telegram.ui.Animation;
package org.telegram.android.Animation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.telegram.ui.Animation;
package org.telegram.android.Animation;
public interface TypeEvaluator<T> {
T evaluate(float fraction, T startValue, T endValue);

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.telegram.ui.Animation;
package org.telegram.android.Animation;
import android.os.Looper;
import android.util.AndroidRuntimeException;

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package org.telegram.ui.Animation;
package org.telegram.android.Animation;
import android.graphics.Camera;
import android.graphics.Matrix;

View File

@ -6,14 +6,14 @@
* Copyright Nikolai Kudashov, 2013-2014.
*/
package org.telegram.ui.AnimationCompat;
package org.telegram.android.AnimationCompat;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import org.telegram.ui.Animation.Animator10;
import org.telegram.ui.Animation.AnimatorListenerAdapter10;
import org.telegram.ui.Animation.View10;
import org.telegram.android.Animation.Animator10;
import org.telegram.android.Animation.AnimatorListenerAdapter10;
import org.telegram.android.Animation.View10;
public class AnimatorListenerAdapterProxy {
protected Object animatorListenerAdapter;

View File

@ -6,17 +6,17 @@
* Copyright Nikolai Kudashov, 2013-2014.
*/
package org.telegram.ui.AnimationCompat;
package org.telegram.android.AnimationCompat;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.view.animation.Interpolator;
import org.telegram.ui.Animation.Animator10;
import org.telegram.ui.Animation.AnimatorListenerAdapter10;
import org.telegram.ui.Animation.AnimatorSet10;
import org.telegram.ui.Animation.View10;
import org.telegram.android.Animation.Animator10;
import org.telegram.android.Animation.AnimatorListenerAdapter10;
import org.telegram.android.Animation.AnimatorSet10;
import org.telegram.android.Animation.View10;
import java.lang.reflect.Array;
import java.util.ArrayList;

View File

@ -6,15 +6,15 @@
* Copyright Nikolai Kudashov, 2013-2014.
*/
package org.telegram.ui.AnimationCompat;
package org.telegram.android.AnimationCompat;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.view.animation.Interpolator;
import org.telegram.ui.Animation.AnimatorListenerAdapter10;
import org.telegram.ui.Animation.ObjectAnimator10;
import org.telegram.ui.Animation.View10;
import org.telegram.android.Animation.AnimatorListenerAdapter10;
import org.telegram.android.Animation.ObjectAnimator10;
import org.telegram.android.Animation.View10;
public class ObjectAnimatorProxy {

View File

@ -6,11 +6,11 @@
* Copyright Nikolai Kudashov, 2013-2014.
*/
package org.telegram.ui.AnimationCompat;
package org.telegram.android.AnimationCompat;
import android.view.View;
import org.telegram.ui.Animation.View10;
import org.telegram.android.Animation.View10;
public class ViewProxy {

View File

@ -14,6 +14,7 @@ import android.app.Activity;
import android.content.ContentProviderOperation;
import android.content.ContentProviderResult;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.net.Uri;
@ -37,7 +38,6 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Locale;
import java.util.concurrent.ConcurrentHashMap;
public class ContactsController {
@ -168,7 +168,7 @@ public class ContactsController {
if (!updatingInviteText && (inviteText == null || time + 86400 < (int)(System.currentTimeMillis() / 1000))) {
updatingInviteText = true;
TLRPC.TL_help_getInviteText req = new TLRPC.TL_help_getInviteText();
req.lang_code = LocaleController.getLocaleString(Locale.getDefault());
req.lang_code = LocaleController.getLocaleString(LocaleController.getInstance().getSystemDefaultLocale());
if (req.lang_code == null || req.lang_code.length() == 0) {
req.lang_code = "en";
}
@ -202,7 +202,19 @@ public class ContactsController {
public void checkAppAccount() {
AccountManager am = AccountManager.get(ApplicationLoader.applicationContext);
Account[] accounts = am.getAccountsByType("org.telegram.account");
Account[] accounts;
try {
accounts = am.getAccountsByType("org.telegram.account");
if (accounts != null && accounts.length > 0) {
for (Account c : accounts) {
am.removeAccount(c, null, null);
}
}
} catch (Exception e) {
FileLog.e("tmessages", e);
}
accounts = am.getAccountsByType("org.telegram.messenger");
boolean recreateAccount = false;
if (UserConfig.isClientActivated()) {
if (accounts.length == 1) {
@ -227,7 +239,7 @@ public class ContactsController {
}
if (UserConfig.isClientActivated()) {
try {
currentAccount = new Account(UserConfig.getCurrentUser().phone, "org.telegram.account");
currentAccount = new Account(UserConfig.getCurrentUser().phone, "org.telegram.messenger");
am.addAccountExplicitly(currentAccount, "", null);
} catch (Exception e) {
FileLog.e("tmessages", e);
@ -239,7 +251,7 @@ public class ContactsController {
public void deleteAllAppAccounts() {
try {
AccountManager am = AccountManager.get(ApplicationLoader.applicationContext);
Account[] accounts = am.getAccountsByType("org.telegram.account");
Account[] accounts = am.getAccountsByType("org.telegram.messenger");
for (Account c : accounts) {
am.removeAccount(c, null, null);
}
@ -1246,7 +1258,7 @@ public class ContactsController {
private void performWriteContactsToPhoneBook() {
final ArrayList<TLRPC.TL_contact> contactsArray = new ArrayList<>();
contactsArray.addAll(contacts);
Utilities.photoBookQueue.postRunnable(new Runnable() {
Utilities.phoneBookQueue.postRunnable(new Runnable() {
@Override
public void run() {
performWriteContactsToPhoneBookInternal(contactsArray);
@ -1303,7 +1315,7 @@ public class ContactsController {
}
for (final Integer uid : contactsTD) {
Utilities.photoBookQueue.postRunnable(new Runnable() {
Utilities.phoneBookQueue.postRunnable(new Runnable() {
@Override
public void run() {
deleteContactFromPhoneBook(uid);
@ -1463,7 +1475,7 @@ public class ContactsController {
builder = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI);
builder.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0);
builder.withValue(ContactsContract.Data.MIMETYPE, "vnd.android.cursor.item/vnd.org.telegram.messenger.android.profile");
builder.withValue(ContactsContract.Data.DATA1, "+" + user.phone);
builder.withValue(ContactsContract.Data.DATA1, user.id);
builder.withValue(ContactsContract.Data.DATA2, "Telegram Profile");
builder.withValue(ContactsContract.Data.DATA3, "+" + user.phone);
builder.withValue(ContactsContract.Data.DATA4, user.id);
@ -1496,6 +1508,22 @@ public class ContactsController {
}
}
protected void markAsContacted(final String contactId) {
if (contactId == null) {
return;
}
Utilities.phoneBookQueue.postRunnable(new Runnable() {
@Override
public void run() {
Uri uri = Uri.parse(contactId);
ContentValues values = new ContentValues();
values.put(ContactsContract.Contacts.LAST_TIME_CONTACTED, System.currentTimeMillis());
ContentResolver cr = ApplicationLoader.applicationContext.getContentResolver();
cr.update(uri, values, null, null);
}
});
}
public void addContact(TLRPC.User user) {
if (user == null || user.phone == null) {
return;
@ -1533,7 +1561,7 @@ public class ContactsController {
// }
for (final TLRPC.User u : res.users) {
Utilities.photoBookQueue.postRunnable(new Runnable() {
Utilities.phoneBookQueue.postRunnable(new Runnable() {
@Override
public void run() {
addContactToPhoneBook(u, true);
@ -1546,7 +1574,7 @@ public class ContactsController {
MessagesStorage.getInstance().putContacts(arrayList, false);
if (u.phone != null && u.phone.length() > 0) {
String name = formatName(u.first_name, u.last_name);
CharSequence name = formatName(u.first_name, u.last_name);
MessagesStorage.getInstance().applyPhoneBookUpdates(u.phone, "");
Contact contact = contactsBookSPhones.get(u.phone);
if (contact != null) {
@ -1599,7 +1627,7 @@ public class ContactsController {
return;
}
MessagesStorage.getInstance().deleteContacts(uids);
Utilities.photoBookQueue.postRunnable(new Runnable() {
Utilities.phoneBookQueue.postRunnable(new Runnable() {
@Override
public void run() {
for (TLRPC.User user : users) {
@ -1610,7 +1638,7 @@ public class ContactsController {
for (TLRPC.User user : users) {
if (user.phone != null && user.phone.length() > 0) {
String name = ContactsController.formatName(user.first_name, user.last_name);
CharSequence name = ContactsController.formatName(user.first_name, user.last_name);
MessagesStorage.getInstance().applyPhoneBookUpdates(user.phone, "");
Contact contact = contactsBookSPhones.get(user.phone);
if (contact != null) {
@ -1772,22 +1800,37 @@ public class ContactsController {
}
public static String formatName(String firstName, String lastName) {
String result = "";
/*if ((firstName == null || firstName.length() == 0) && (lastName == null || lastName.length() == 0)) {
return LocaleController.getString("HiddenName", R.string.HiddenName);
}*/
if (firstName != null) {
firstName = firstName.trim();
}
if (lastName != null) {
lastName = lastName.trim();
}
StringBuilder result = new StringBuilder((firstName != null ? firstName.length() : 0) + (lastName != null ? lastName.length() : 0) + 1);
if (LocaleController.nameDisplayOrder == 1) {
result = firstName;
if (result == null || result.length() == 0) {
result = lastName;
} else if (result.length() != 0 && lastName != null && lastName.length() != 0) {
result += " " + lastName;
if (firstName != null && firstName.length() > 0) {
result.append(firstName);
if (lastName != null && lastName.length() > 0) {
result.append(" ");
result.append(lastName);
}
} else if (lastName != null && lastName.length() > 0) {
result.append(lastName);
}
} else {
result = lastName;
if (result == null || result.length() == 0) {
result = firstName;
} else if (result.length() != 0 && firstName != null && firstName.length() != 0) {
result += " " + firstName;
if (lastName != null && lastName.length() > 0) {
result.append(lastName);
if (firstName != null && firstName.length() > 0) {
result.append(" ");
result.append(firstName);
}
} else if (firstName != null && firstName.length() > 0) {
result.append(firstName);
}
}
return result.trim();
return result.toString();
}
}

View File

@ -33,7 +33,8 @@ import org.telegram.messenger.ApplicationLoader;
public class Emoji {
private static HashMap<Long, DrawableInfo> rects = new HashMap<>();
private static int drawImgSize, bigImgSize;
private static int drawImgSize;
private static int bigImgSize;
private static boolean inited = false;
private static Paint placeholderPaint;
private static Bitmap emojiBmp[] = new Bitmap[5];
@ -193,19 +194,19 @@ public class Emoji {
static {
int emojiFullSize;
if (AndroidUtilities.density <= 1.0f) {
emojiFullSize = 30;
emojiFullSize = 32;
} else if (AndroidUtilities.density <= 1.5f) {
emojiFullSize = 45;
emojiFullSize = 48;
} else if (AndroidUtilities.density <= 2.0f) {
emojiFullSize = 60;
emojiFullSize = 64;
} else {
emojiFullSize = 90;
emojiFullSize = 96;
}
drawImgSize = AndroidUtilities.dp(20);
if (AndroidUtilities.isTablet()) {
bigImgSize = AndroidUtilities.dp(40);
} else {
bigImgSize = AndroidUtilities.dp(30);
bigImgSize = AndroidUtilities.dp(32);
}
for (int j = 1; j < data.length; j++) {
@ -234,8 +235,26 @@ public class Emoji {
scale = 3.0f;
}
String imageName = String.format(Locale.US, "emoji%.01fx_%d.jpg", scale, page);
File imageFile = ApplicationLoader.applicationContext.getFileStreamPath(imageName);
String imageName;
File imageFile;
try {
imageName = String.format(Locale.US, "emoji%.01fx_%d.jpg", scale, page);
imageFile = ApplicationLoader.applicationContext.getFileStreamPath(imageName);
if (imageFile.exists()) {
imageFile.delete();
}
imageName = String.format(Locale.US, "emoji%.01fx_a_%d.jpg", scale, page);
imageFile = ApplicationLoader.applicationContext.getFileStreamPath(imageName);
if (imageFile.exists()) {
imageFile.delete();
}
} catch (Exception e) {
FileLog.e("tmessages", e);
}
imageName = String.format(Locale.US, "v4_emoji%.01fx_%d.jpg", scale, page);
imageFile = ApplicationLoader.applicationContext.getFileStreamPath(imageName);
if (!imageFile.exists()) {
InputStream is = ApplicationLoader.applicationContext.getAssets().open("emoji/" + imageName);
Utilities.copyFile(is, imageFile);
@ -253,7 +272,7 @@ public class Emoji {
final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Utilities.loadBitmap(imageFile.getAbsolutePath(), bitmap, imageResize, width, height, stride);
imageName = String.format(Locale.US, "emoji%.01fx_a_%d.jpg", scale, page);
imageName = String.format(Locale.US, "v4_emoji%.01fx_a_%d.jpg", scale, page);
imageFile = ApplicationLoader.applicationContext.getFileStreamPath(imageName);
if (!imageFile.exists()) {
InputStream is = ApplicationLoader.applicationContext.getAssets().open("emoji/" + imageName);

View File

@ -70,6 +70,8 @@ public class ImageLoader {
private DispatchQueue recycleQueue = new DispatchQueue("recycleQueue");
private ConcurrentHashMap<String, Float> fileProgresses = new ConcurrentHashMap<>();
private HashMap<String, ThumbGenerateTask> thumbGenerateTasks = new HashMap<>();
private static byte[] bytes;
private static byte[] bytesThumb;
private int currentHttpTasksCount = 0;
private LinkedList<HttpFileTask> httpFileLoadTasks = new LinkedList<>();
@ -507,6 +509,7 @@ public class ImageLoader {
}
Long mediaId = null;
boolean mediaIsVideo = false;
Bitmap image = null;
File cacheFileFinal = cacheImage.finalFilePath;
boolean canDeleteFile = true;
@ -537,18 +540,35 @@ public class ImageLoader {
}
}
if (image == null) {
if (isWebp) {
RandomAccessFile file = new RandomAccessFile(cacheFileFinal, "r");
ByteBuffer buffer = file.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, cacheFileFinal.length());
image = Utilities.loadWebpImage(buffer, buffer.limit(), null);
file.close();
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inSampleSize = 1;
if (!isWebp && Build.VERSION.SDK_INT > 10 && Build.VERSION.SDK_INT < 21) {
opts.inPurgeable = true;
}
if (isWebp) {
RandomAccessFile file = new RandomAccessFile(cacheFileFinal, "r");
ByteBuffer buffer = file.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, cacheFileFinal.length());
image = Utilities.loadWebpImage(buffer, buffer.limit(), null);
file.close();
} else {
if (opts.inPurgeable) {
RandomAccessFile f = new RandomAccessFile(cacheFileFinal, "r");
int len = (int) f.length();
byte[] data = bytesThumb != null && bytesThumb.length >= len ? bytesThumb : null;
if (data == null) {
bytesThumb = data = new byte[len];
}
f.readFully(data, 0, len);
image = BitmapFactory.decodeByteArray(data, 0, len, opts);
} else {
FileInputStream is = new FileInputStream(cacheFileFinal);
image = BitmapFactory.decodeStream(is, null, null);
image = BitmapFactory.decodeStream(is, null, opts);
is.close();
}
}
if (image == null) {
if (canDeleteFile && (cacheFileFinal.length() == 0 || cacheImage.filter == null)) {
cacheFileFinal.delete();
@ -556,15 +576,18 @@ public class ImageLoader {
} else {
if (image != null) {
if (blurType == 1) {
Utilities.blurBitmap(image, 3);
Utilities.blurBitmap(image, 3, opts.inPurgeable ? 0 : 1);
} else if (blurType == 2) {
Utilities.blurBitmap(image, 1);
Utilities.blurBitmap(image, 1, opts.inPurgeable ? 0 : 1);
} else if (blurType == 3) {
Utilities.blurBitmap(image, 7);
Utilities.blurBitmap(image, 7);
Utilities.blurBitmap(image, 7);
Utilities.blurBitmap(image, 7, opts.inPurgeable ? 0 : 1);
Utilities.blurBitmap(image, 7, opts.inPurgeable ? 0 : 1);
Utilities.blurBitmap(image, 7, opts.inPurgeable ? 0 : 1);
}
}
if (blurType == 0 && opts.inPurgeable) {
Utilities.pinBitmap(image);
}
if (runtimeHack != null) {
runtimeHack.trackFree(image.getRowBytes() * image.getHeight());
}
@ -579,6 +602,14 @@ public class ImageLoader {
int idx = cacheImage.httpUrl.indexOf(":", 8);
if (idx >= 0) {
mediaId = Long.parseLong(cacheImage.httpUrl.substring(8, idx));
mediaIsVideo = false;
}
canDeleteFile = false;
} else if (cacheImage.httpUrl.startsWith("vthumb://")) {
int idx = cacheImage.httpUrl.indexOf(":", 9);
if (idx >= 0) {
mediaId = Long.parseLong(cacheImage.httpUrl.substring(9, idx));
mediaIsVideo = true;
}
canDeleteFile = false;
} else if (!cacheImage.httpUrl.startsWith("http")) {
@ -604,35 +635,44 @@ public class ImageLoader {
}
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inSampleSize = 1;
float w_filter = 0;
float h_filter = 0;
boolean blur = false;
if (cacheImage.filter != null) {
String args[] = cacheImage.filter.split("_");
w_filter = Float.parseFloat(args[0]) * AndroidUtilities.density;
h_filter = Float.parseFloat(args[1]) * AndroidUtilities.density;
if (args.length > 2) {
if (args.length >= 2) {
w_filter = Float.parseFloat(args[0]) * AndroidUtilities.density;
h_filter = Float.parseFloat(args[1]) * AndroidUtilities.density;
}
if (cacheImage.filter.contains("b")) {
blur = true;
}
opts.inJustDecodeBounds = true;
if (w_filter != 0 && h_filter != 0) {
opts.inJustDecodeBounds = true;
if (mediaId != null) {
MediaStore.Images.Thumbnails.getThumbnail(ApplicationLoader.applicationContext.getContentResolver(), mediaId, MediaStore.Images.Thumbnails.MINI_KIND, opts);
} else {
FileInputStream is = new FileInputStream(cacheFileFinal);
image = BitmapFactory.decodeStream(is, null, opts);
is.close();
}
if (mediaId != null) {
if (mediaIsVideo) {
MediaStore.Video.Thumbnails.getThumbnail(ApplicationLoader.applicationContext.getContentResolver(), mediaId, MediaStore.Video.Thumbnails.MINI_KIND, opts);
} else {
MediaStore.Images.Thumbnails.getThumbnail(ApplicationLoader.applicationContext.getContentResolver(), mediaId, MediaStore.Images.Thumbnails.MINI_KIND, opts);
}
} else {
FileInputStream is = new FileInputStream(cacheFileFinal);
image = BitmapFactory.decodeStream(is, null, opts);
is.close();
}
float photoW = opts.outWidth;
float photoH = opts.outHeight;
float scaleFactor = Math.max(photoW / w_filter, photoH / h_filter);
if (scaleFactor < 1) {
scaleFactor = 1;
float photoW = opts.outWidth;
float photoH = opts.outHeight;
float scaleFactor = Math.max(photoW / w_filter, photoH / h_filter);
if (scaleFactor < 1) {
scaleFactor = 1;
}
opts.inJustDecodeBounds = false;
opts.inSampleSize = (int) scaleFactor;
}
opts.inJustDecodeBounds = false;
opts.inSampleSize = (int)scaleFactor;
}
synchronized (sync) {
if (isCancelled) {
@ -645,13 +685,17 @@ public class ImageLoader {
} else {
opts.inPreferredConfig = Bitmap.Config.RGB_565;
}
//if (Build.VERSION.SDK_INT < 21) {
// opts.inPurgeable = true;
//}
if (!isWebp && Build.VERSION.SDK_INT > 10 && Build.VERSION.SDK_INT < 21) {
opts.inPurgeable = true;
}
opts.inDither = false;
if (mediaId != null) {
image = MediaStore.Images.Thumbnails.getThumbnail(ApplicationLoader.applicationContext.getContentResolver(), mediaId, MediaStore.Images.Thumbnails.MINI_KIND, opts);
if (mediaIsVideo) {
image = MediaStore.Video.Thumbnails.getThumbnail(ApplicationLoader.applicationContext.getContentResolver(), mediaId, MediaStore.Video.Thumbnails.MINI_KIND, opts);
} else {
image = MediaStore.Images.Thumbnails.getThumbnail(ApplicationLoader.applicationContext.getContentResolver(), mediaId, MediaStore.Images.Thumbnails.MINI_KIND, opts);
}
}
if (image == null) {
if (isWebp) {
@ -660,9 +704,20 @@ public class ImageLoader {
image = Utilities.loadWebpImage(buffer, buffer.limit(), null);
file.close();
} else {
FileInputStream is = new FileInputStream(cacheFileFinal);
image = BitmapFactory.decodeStream(is, null, opts);
is.close();
if (opts.inPurgeable) {
RandomAccessFile f = new RandomAccessFile(cacheFileFinal, "r");
int len = (int) f.length();
byte[] data = bytes != null && bytes.length >= len ? bytes : null;
if (data == null) {
bytes = data = new byte[len];
}
f.readFully(data, 0, len);
image = BitmapFactory.decodeByteArray(data, 0, len, opts);
} else {
FileInputStream is = new FileInputStream(cacheFileFinal);
image = BitmapFactory.decodeStream(is, null, opts);
is.close();
}
}
}
if (image == null) {
@ -670,12 +725,13 @@ public class ImageLoader {
cacheFileFinal.delete();
}
} else {
boolean blured = false;
if (cacheImage.filter != null) {
float bitmapW = image.getWidth();
float bitmapH = image.getHeight();
if (bitmapW != w_filter && bitmapW > w_filter) {
if (!opts.inPurgeable && w_filter != 0 && bitmapW != w_filter && bitmapW > w_filter + 20) {
float scaleFactor = bitmapW / w_filter;
Bitmap scaledBitmap = Bitmap.createScaledBitmap(image, (int)w_filter, (int)(bitmapH / scaleFactor), true);
Bitmap scaledBitmap = Bitmap.createScaledBitmap(image, (int) w_filter, (int) (bitmapH / scaleFactor), true);
if (image != scaledBitmap) {
image.recycle();
callGC();
@ -683,9 +739,13 @@ public class ImageLoader {
}
}
if (image != null && blur && bitmapH < 100 && bitmapW < 100) {
Utilities.blurBitmap(image, 3);
Utilities.blurBitmap(image, 3, opts.inPurgeable ? 0 : 1);
blured = true;
}
}
if (!blured && opts.inPurgeable) {
Utilities.pinBitmap(image);
}
if (runtimeHack != null) {
runtimeHack.trackFree(image.getRowBytes() * image.getHeight());
}
@ -753,7 +813,7 @@ public class ImageLoader {
}
try {
Object res = trackAllocation.invoke(runtime, size);
return (res instanceof Boolean) ? (Boolean)res : true;
return (res instanceof Boolean) ? (Boolean) res : true;
} catch (Exception e) {
return false;
}
@ -765,7 +825,7 @@ public class ImageLoader {
}
try {
Object res = trackFree.invoke(runtime, size);
return (res instanceof Boolean) ? (Boolean)res : true;
return (res instanceof Boolean) ? (Boolean) res : true;
} catch (Exception e) {
return false;
}
@ -778,8 +838,8 @@ public class ImageLoader {
Method getRt = cl.getMethod("getRuntime", new Class[0]);
Object[] objects = new Object[0];
runtime = getRt.invoke(null, objects);
trackAllocation = cl.getMethod("trackExternalAllocation", new Class[] {long.class});
trackFree = cl.getMethod("trackExternalFree", new Class[] {long.class});
trackAllocation = cl.getMethod("trackExternalAllocation", new Class[]{long.class});
trackFree = cl.getMethod("trackExternalFree", new Class[]{long.class});
} catch (Exception e) {
FileLog.e("tmessages", e);
runtime = null;
@ -872,7 +932,7 @@ public class ImageLoader {
@Override
public void run() {
for (ImageReceiver imgView : finalImageReceiverArray) {
imgView.setImageBitmapByKey(image, key, thumb);
imgView.setImageBitmapByKey(image, key, thumb, false);
}
}
});
@ -891,6 +951,7 @@ public class ImageLoader {
}
private static volatile ImageLoader Instance = null;
public static ImageLoader getInstance() {
ImageLoader localInstance = Instance;
if (localInstance == null) {
@ -915,12 +976,13 @@ public class ImageLoader {
@Override
protected int sizeOf(String key, BitmapDrawable bitmap) {
Bitmap b = bitmap.getBitmap();
if(Build.VERSION.SDK_INT < 12) {
if (Build.VERSION.SDK_INT < 12) {
return b.getRowBytes() * b.getHeight();
} else {
return b.getByteCount();
}
}
@Override
protected void entryRemoved(boolean evicted, String key, final BitmapDrawable oldBitmap, BitmapDrawable newBitmap) {
if (ignoreRemoval != null && key != null && ignoreRemoval.equals(key)) {
@ -1193,7 +1255,7 @@ public class ImageLoader {
recycleQueue.postRunnable(new Runnable() {
@Override
public void run() {
System.gc();
//System.gc();
}
});
}
@ -1296,17 +1358,21 @@ public class ImageLoader {
return memCache.get(key);
}
public void replaceImageInCache(final String oldKey, final String newKey) {
public void replaceImageInCache(final String oldKey, final String newKey, final TLRPC.FileLocation newLocation) {
AndroidUtilities.runOnUIThread(new Runnable() {
@Override
public void run() {
ArrayList<String> arr = memCache.getFilterKeys(oldKey);
if (arr != null) {
for (String filter : arr) {
performReplace(oldKey + "@" + filter, newKey + "@" + filter);
String oldK = oldKey + "@" + filter;
String newK = newKey + "@" + filter;
performReplace(oldK, newK);
NotificationCenter.getInstance().postNotificationName(NotificationCenter.didReplacedPhotoInMemCache, oldK, newK, newLocation);
}
} else {
performReplace(oldKey, newKey);
NotificationCenter.getInstance().postNotificationName(NotificationCenter.didReplacedPhotoInMemCache, oldKey, newKey, newLocation);
}
}
});
@ -1384,6 +1450,11 @@ public class ImageLoader {
if (idx >= 0) {
cacheFile = new File(httpLocation.substring(idx + 1));
}
} else if (httpLocation.startsWith("vthumb://")) {
int idx = httpLocation.indexOf(":", 9);
if (idx >= 0) {
cacheFile = new File(httpLocation.substring(idx + 1));
}
} else {
cacheFile = new File(httpLocation);
}
@ -1487,7 +1558,7 @@ public class ImageLoader {
if (bitmapDrawable != null) {
cancelLoadingForImageReceiver(imageReceiver, 0);
if (!imageReceiver.isForcePreview()) {
imageReceiver.setImageBitmapByKey(bitmapDrawable, key, false);
imageReceiver.setImageBitmapByKey(bitmapDrawable, key, false, true);
return;
}
}
@ -1497,7 +1568,7 @@ public class ImageLoader {
if (thumbKey != null) {
BitmapDrawable bitmapDrawable = memCache.get(thumbKey);
if (bitmapDrawable != null) {
imageReceiver.setImageBitmapByKey(bitmapDrawable, thumbKey, true);
imageReceiver.setImageBitmapByKey(bitmapDrawable, thumbKey, true, true);
cancelLoadingForImageReceiver(imageReceiver, 1);
thumbSet = true;
}
@ -1784,7 +1855,7 @@ public class ImageLoader {
scaleFactor = 1;
}
bmOptions.inJustDecodeBounds = false;
bmOptions.inSampleSize = (int)scaleFactor;
bmOptions.inSampleSize = (int) scaleFactor;
String exifPath = null;
if (path != null) {
@ -1900,7 +1971,7 @@ public class ImageLoader {
size.size = size.bytes.length;
stream2.close();
} else {
size.size = (int)stream.getChannel().size();
size.size = (int) stream.getChannel().size();
}
stream.close();
if (scaledBitmap != bitmap) {
@ -1926,11 +1997,17 @@ public class ImageLoader {
boolean scaleAnyway = false;
float scaleFactor = Math.max(photoW / maxWidth, photoH / maxHeight);
if (minWidth != 0 && minHeight != 0 && (photoW < minWidth || photoH < minHeight)) {
scaleFactor = Math.max(photoW / minWidth, photoH / minHeight);
if (photoW < minWidth && photoH > minHeight) {
scaleFactor = photoW / minWidth;
} else if (photoW > minWidth && photoH < minHeight) {
scaleFactor = photoH / minHeight;
} else {
scaleFactor = Math.max(photoW / minWidth, photoH / minHeight);
}
scaleAnyway = true;
}
int w = (int)(photoW / scaleFactor);
int h = (int)(photoH / scaleFactor);
int w = (int) (photoW / scaleFactor);
int h = (int) (photoH / scaleFactor);
if (h == 0 || w == 0) {
return null;
}

View File

@ -11,6 +11,7 @@ package org.telegram.android;
import android.graphics.Bitmap;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PorterDuff;
@ -33,12 +34,25 @@ public class ImageReceiver implements NotificationCenter.NotificationCenterDeleg
void didSetImage(ImageReceiver imageReceiver, boolean set, boolean thumb);
}
private class SetImageBackup {
public TLObject fileLocation;
public String httpUrl;
public String filter;
public Drawable thumb;
public TLRPC.FileLocation thumbLocation;
public String thumbFilter;
public int size;
public boolean cacheOnly;
}
private View parentView;
private Integer tag;
private Integer thumbTag;
private MessageObject parentMessageObject;
private boolean canceledLoading;
private SetImageBackup setImageBackup;
private TLObject currentImageLocation;
private String currentKey;
private String currentThumbKey;
@ -66,11 +80,16 @@ public class ImageReceiver implements NotificationCenter.NotificationCenterDeleg
private RectF roundRect;
private RectF bitmapRect;
private Matrix shaderMatrix;
private int alpha = 255;
private float overrideAlpha = 1.0f;
private boolean isPressed;
private int orientation;
private boolean centerRotation;
private ImageReceiverDelegate delegate;
private float currentAlpha;
private long lastUpdateAlphaTime;
private byte crossfadeAlpha = 1;
private boolean crossfadeWithThumb;
private ColorFilter colorFilter;
public ImageReceiver() {
@ -106,6 +125,13 @@ public class ImageReceiver implements NotificationCenter.NotificationCenterDeleg
}
public void setImage(TLObject fileLocation, String httpUrl, String filter, Drawable thumb, TLRPC.FileLocation thumbLocation, String thumbFilter, int size, boolean cacheOnly) {
if (setImageBackup != null) {
setImageBackup.fileLocation = null;
setImageBackup.httpUrl = null;
setImageBackup.thumbLocation = null;
setImageBackup.thumb = null;
}
if ((fileLocation == null && httpUrl == null && thumbLocation == null)
|| (fileLocation != null && !(fileLocation instanceof TLRPC.TL_fileLocation)
&& !(fileLocation instanceof TLRPC.TL_fileEncryptedLocation)
@ -120,13 +146,14 @@ public class ImageReceiver implements NotificationCenter.NotificationCenterDeleg
currentFilter = null;
currentCacheOnly = false;
staticThumb = thumb;
currentAlpha = 1;
currentThumbLocation = null;
currentSize = 0;
currentImage = null;
bitmapShader = null;
ImageLoader.getInstance().cancelLoadingForImageReceiver(this, 0);
if (parentView != null) {
parentView.invalidate();
parentView.invalidate(imageX, imageY, imageX + imageW, imageY + imageH);
}
if (delegate != null) {
delegate.didSetImage(this, currentImage != null || currentThumb != null || staticThumb != null, currentImage == null);
@ -134,6 +161,8 @@ public class ImageReceiver implements NotificationCenter.NotificationCenterDeleg
return;
}
if (!(thumbLocation instanceof TLRPC.TL_fileLocation)) {
thumbLocation = null;
}
@ -187,6 +216,7 @@ public class ImageReceiver implements NotificationCenter.NotificationCenterDeleg
currentThumbLocation = thumbLocation;
staticThumb = thumb;
bitmapShader = null;
currentAlpha = 1.0f;
if (delegate != null) {
delegate.didSetImage(this, currentImage != null || currentThumb != null || staticThumb != null, currentImage == null);
@ -194,10 +224,14 @@ public class ImageReceiver implements NotificationCenter.NotificationCenterDeleg
ImageLoader.getInstance().loadImageForImageReceiver(this);
if (parentView != null) {
parentView.invalidate();
parentView.invalidate(imageX, imageY, imageX + imageW, imageY + imageH);
}
}
public void setColorFilter(ColorFilter filter) {
colorFilter = filter;
}
public void setDelegate(ImageReceiverDelegate delegate) {
this.delegate = delegate;
}
@ -239,11 +273,18 @@ public class ImageReceiver implements NotificationCenter.NotificationCenterDeleg
currentSize = 0;
currentCacheOnly = false;
bitmapShader = null;
if (setImageBackup != null) {
setImageBackup.fileLocation = null;
setImageBackup.httpUrl = null;
setImageBackup.thumbLocation = null;
setImageBackup.thumb = null;
}
currentAlpha = 1;
if (delegate != null) {
delegate.didSetImage(this, currentImage != null || currentThumb != null || staticThumb != null, currentImage == null);
}
if (parentView != null) {
parentView.invalidate();
parentView.invalidate(imageX, imageY, imageX + imageW, imageY + imageH);
}
}
@ -256,6 +297,212 @@ public class ImageReceiver implements NotificationCenter.NotificationCenterDeleg
}
}
public void onDetachedFromWindow() {
if (currentImageLocation != null || currentHttpUrl != null || currentThumbLocation != null || staticThumb != null) {
if (setImageBackup == null) {
setImageBackup = new SetImageBackup();
}
setImageBackup.fileLocation = currentImageLocation;
setImageBackup.httpUrl = currentHttpUrl;
setImageBackup.filter = currentFilter;
setImageBackup.thumb = staticThumb;
setImageBackup.thumbLocation = currentThumbLocation;
setImageBackup.thumbFilter = currentThumbFilter;
setImageBackup.size = currentSize;
setImageBackup.cacheOnly = currentCacheOnly;
}
NotificationCenter.getInstance().removeObserver(this, NotificationCenter.didReplacedPhotoInMemCache);
clearImage();
}
public boolean onAttachedToWindow() {
NotificationCenter.getInstance().addObserver(this, NotificationCenter.didReplacedPhotoInMemCache);
if (setImageBackup != null && (setImageBackup.fileLocation != null || setImageBackup.httpUrl != null || setImageBackup.thumbLocation != null || setImageBackup.thumb != null)) {
setImage(setImageBackup.fileLocation, setImageBackup.httpUrl, setImageBackup.filter, setImageBackup.thumb, setImageBackup.thumbLocation, setImageBackup.thumbFilter, setImageBackup.size, setImageBackup.cacheOnly);
return true;
}
return false;
}
private void drawDrawable(Canvas canvas, Drawable drawable, int alpha) {
if (drawable instanceof BitmapDrawable) {
BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
Paint paint = bitmapDrawable.getPaint();
boolean hasFilter = paint != null && paint.getColorFilter() != null;
if (hasFilter && !isPressed) {
bitmapDrawable.setColorFilter(null);
hasFilter = false;
} else if (!hasFilter && isPressed) {
bitmapDrawable.setColorFilter(new PorterDuffColorFilter(0xffdddddd, PorterDuff.Mode.MULTIPLY));
hasFilter = true;
}
if (colorFilter != null) {
bitmapDrawable.setColorFilter(colorFilter);
}
if (bitmapShader != null) {
drawRegion.set(imageX, imageY, imageX + imageW, imageY + imageH);
if (isVisible) {
roundRect.set(drawRegion);
shaderMatrix.reset();
shaderMatrix.setRectToRect(bitmapRect, roundRect, Matrix.ScaleToFit.FILL);
bitmapShader.setLocalMatrix(shaderMatrix);
roundPaint.setAlpha(alpha);
canvas.drawRoundRect(roundRect, roundRadius, roundRadius, roundPaint);
}
} else {
int bitmapW;
int bitmapH;
int originalW = bitmapDrawable.getIntrinsicWidth();
int originalH = bitmapDrawable.getIntrinsicHeight();
if (orientation == 90 || orientation == 270) {
bitmapW = bitmapDrawable.getIntrinsicHeight();
bitmapH = bitmapDrawable.getIntrinsicWidth();
} else {
bitmapW = bitmapDrawable.getIntrinsicWidth();
bitmapH = bitmapDrawable.getIntrinsicHeight();
}
float scaleW = bitmapW / (float) imageW;
float scaleH = bitmapH / (float) imageH;
if (isAspectFit) {
float scale = Math.max(scaleW, scaleH);
canvas.save();
bitmapW /= scale;
bitmapH /= scale;
drawRegion.set(imageX + (imageW - bitmapW) / 2, imageY + (imageH - bitmapH) / 2, imageX + (imageW + bitmapW) / 2, imageY + (imageH + bitmapH) / 2);
bitmapDrawable.setBounds(drawRegion);
try {
bitmapDrawable.setAlpha(alpha);
bitmapDrawable.draw(canvas);
} catch (Exception e) {
if (bitmapDrawable == currentImage && currentKey != null) {
ImageLoader.getInstance().removeImage(currentKey);
currentKey = null;
} else if (bitmapDrawable == currentThumb && currentThumbKey != null) {
ImageLoader.getInstance().removeImage(currentThumbKey);
currentThumbKey = null;
}
setImage(currentImageLocation, currentHttpUrl, currentFilter, currentThumb, currentThumbLocation, currentThumbFilter, currentSize, currentCacheOnly);
FileLog.e("tmessages", e);
}
canvas.restore();
} else {
if (Math.abs(scaleW - scaleH) > 0.00001f) {
canvas.save();
canvas.clipRect(imageX, imageY, imageX + imageW, imageY + imageH);
if (orientation != 0) {
if (centerRotation) {
canvas.rotate(orientation, imageW / 2, imageH / 2);
} else {
canvas.rotate(orientation, 0, 0);
}
}
if (bitmapW / scaleH > imageW) {
bitmapW /= scaleH;
originalW /= scaleH;
drawRegion.set(imageX - (bitmapW - imageW) / 2, imageY, imageX + (bitmapW + imageW) / 2, imageY + imageH);
} else {
bitmapH /= scaleW;
originalH /= scaleW;
drawRegion.set(imageX, imageY - (bitmapH - imageH) / 2, imageX + imageW, imageY + (bitmapH + imageH) / 2);
}
if (orientation == 90 || orientation == 270) {
int width = (drawRegion.right - drawRegion.left) / 2;
int height = (drawRegion.bottom - drawRegion.top) / 2;
int centerX = (drawRegion.right + drawRegion.left) / 2;
int centerY = (drawRegion.top + drawRegion.bottom) / 2;
bitmapDrawable.setBounds(centerX - height, centerY - width, centerX + height, centerY + width);
} else {
bitmapDrawable.setBounds(drawRegion);
}
if (isVisible) {
try {
bitmapDrawable.setAlpha(alpha);
bitmapDrawable.draw(canvas);
} catch (Exception e) {
if (bitmapDrawable == currentImage && currentKey != null) {
ImageLoader.getInstance().removeImage(currentKey);
currentKey = null;
} else if (bitmapDrawable == currentThumb && currentThumbKey != null) {
ImageLoader.getInstance().removeImage(currentThumbKey);
currentThumbKey = null;
}
setImage(currentImageLocation, currentHttpUrl, currentFilter, currentThumb, currentThumbLocation, currentThumbFilter, currentSize, currentCacheOnly);
FileLog.e("tmessages", e);
}
}
canvas.restore();
} else {
canvas.save();
if (orientation != 0) {
if (centerRotation) {
canvas.rotate(orientation, imageW / 2, imageH / 2);
} else {
canvas.rotate(orientation, 0, 0);
}
}
drawRegion.set(imageX, imageY, imageX + imageW, imageY + imageH);
if (orientation == 90 || orientation == 270) {
int width = (drawRegion.right - drawRegion.left) / 2;
int height = (drawRegion.bottom - drawRegion.top) / 2;
int centerX = (drawRegion.right + drawRegion.left) / 2;
int centerY = (drawRegion.top + drawRegion.bottom) / 2;
bitmapDrawable.setBounds(centerX - height, centerY - width, centerX + height, centerY + width);
} else {
bitmapDrawable.setBounds(drawRegion);
}
if (isVisible) {
try {
bitmapDrawable.setAlpha(alpha);
bitmapDrawable.draw(canvas);
} catch (Exception e) {
if (bitmapDrawable == currentImage && currentKey != null) {
ImageLoader.getInstance().removeImage(currentKey);
currentKey = null;
} else if (bitmapDrawable == currentThumb && currentThumbKey != null) {
ImageLoader.getInstance().removeImage(currentThumbKey);
currentThumbKey = null;
}
setImage(currentImageLocation, currentHttpUrl, currentFilter, currentThumb, currentThumbLocation, currentThumbFilter, currentSize, currentCacheOnly);
FileLog.e("tmessages", e);
}
}
canvas.restore();
}
}
}
} else {
drawRegion.set(imageX, imageY, imageX + imageW, imageY + imageH);
drawable.setBounds(drawRegion);
if (isVisible) {
try {
drawable.setAlpha(alpha);
drawable.draw(canvas);
} catch (Exception e) {
FileLog.e("tmessages", e);
}
}
}
}
private void checkAlphaAnimation() {
if (currentAlpha != 1) {
long currentTime = System.currentTimeMillis();
currentAlpha += (currentTime - lastUpdateAlphaTime) / 150.0f;
if (currentAlpha > 1) {
currentAlpha = 1;
}
lastUpdateAlphaTime = System.currentTimeMillis();
if (parentView != null) {
parentView.invalidate(imageX, imageY, imageX + imageW, imageY + imageH);
}
}
}
public boolean draw(Canvas canvas) {
try {
BitmapDrawable bitmapDrawable = null;
@ -267,161 +514,34 @@ public class ImageReceiver implements NotificationCenter.NotificationCenterDeleg
bitmapDrawable = currentThumb;
}
if (bitmapDrawable != null) {
Paint paint = bitmapDrawable.getPaint();
boolean hasFilter = paint != null && paint.getColorFilter() != null;
if (hasFilter && !isPressed) {
bitmapDrawable.setColorFilter(null);
hasFilter = false;
} else if (!hasFilter && isPressed) {
bitmapDrawable.setColorFilter(new PorterDuffColorFilter(0xffdddddd, PorterDuff.Mode.MULTIPLY));
hasFilter = true;
}
if (bitmapShader != null) {
drawRegion.set(imageX, imageY, imageX + imageW, imageY + imageH);
if (isVisible) {
roundRect.set(drawRegion);
shaderMatrix.reset();
shaderMatrix.setRectToRect(bitmapRect, roundRect, Matrix.ScaleToFit.FILL);
bitmapShader.setLocalMatrix(shaderMatrix);
canvas.drawRoundRect(roundRect, roundRadius, roundRadius, roundPaint);
if (crossfadeAlpha != 0) {
if (crossfadeWithThumb && currentAlpha != 1.0f) {
Drawable thumbDrawable = null;
if (bitmapDrawable == currentImage) {
if (staticThumb != null) {
thumbDrawable = staticThumb;
} else if (currentThumb != null) {
thumbDrawable = currentThumb;
}
} else if (bitmapDrawable == currentThumb) {
if (staticThumb != null) {
thumbDrawable = staticThumb;
}
}
if (thumbDrawable != null) {
drawDrawable(canvas, thumbDrawable, (int) (overrideAlpha * 255));
}
}
drawDrawable(canvas, bitmapDrawable, (int) (overrideAlpha * currentAlpha * 255));
} else {
int bitmapW;
int bitmapH;
int originalW = bitmapDrawable.getIntrinsicWidth();
int originalH = bitmapDrawable.getIntrinsicHeight();
if (orientation == 90 || orientation == 270) {
bitmapW = bitmapDrawable.getIntrinsicHeight();
bitmapH = bitmapDrawable.getIntrinsicWidth();
} else {
bitmapW = bitmapDrawable.getIntrinsicWidth();
bitmapH = bitmapDrawable.getIntrinsicHeight();
}
float scaleW = bitmapW / (float) imageW;
float scaleH = bitmapH / (float) imageH;
if (isAspectFit) {
float scale = Math.max(scaleW, scaleH);
canvas.save();
bitmapW /= scale;
bitmapH /= scale;
drawRegion.set(imageX + (imageW - bitmapW) / 2, imageY + (imageH - bitmapH) / 2, imageX + (imageW + bitmapW) / 2, imageY + (imageH + bitmapH) / 2);
bitmapDrawable.setBounds(drawRegion);
try {
bitmapDrawable.setAlpha(alpha);
bitmapDrawable.draw(canvas);
} catch (Exception e) {
if (bitmapDrawable == currentImage && currentKey != null) {
ImageLoader.getInstance().removeImage(currentKey);
currentKey = null;
} else if (bitmapDrawable == currentThumb && currentThumbKey != null) {
ImageLoader.getInstance().removeImage(currentThumbKey);
currentThumbKey = null;
}
setImage(currentImageLocation, currentHttpUrl, currentFilter, currentThumb, currentThumbLocation, currentThumbFilter, currentSize, currentCacheOnly);
FileLog.e("tmessages", e);
}
canvas.restore();
} else {
if (Math.abs(scaleW - scaleH) > 0.00001f) {
canvas.save();
canvas.clipRect(imageX, imageY, imageX + imageW, imageY + imageH);
if (orientation != 0) {
if (centerRotation) {
canvas.rotate(orientation, imageW / 2, imageH / 2);
} else {
canvas.rotate(orientation, 0, 0);
}
}
if (bitmapW / scaleH > imageW) {
bitmapW /= scaleH;
originalW /= scaleH;
drawRegion.set(imageX - (bitmapW - imageW) / 2, imageY, imageX + (bitmapW + imageW) / 2, imageY + imageH);
} else {
bitmapH /= scaleW;
originalH /= scaleW;
drawRegion.set(imageX, imageY - (bitmapH - imageH) / 2, imageX + imageW, imageY + (bitmapH + imageH) / 2);
}
if (orientation == 90 || orientation == 270) {
int width = (drawRegion.right - drawRegion.left) / 2;
int height = (drawRegion.bottom - drawRegion.top) / 2;
int centerX = (drawRegion.right + drawRegion.left) / 2;
int centerY = (drawRegion.top + drawRegion.bottom) / 2;
bitmapDrawable.setBounds(centerX - height, centerY - width, centerX + height, centerY + width);
} else {
bitmapDrawable.setBounds(drawRegion);
}
if (isVisible) {
try {
bitmapDrawable.setAlpha(alpha);
bitmapDrawable.draw(canvas);
} catch (Exception e) {
if (bitmapDrawable == currentImage && currentKey != null) {
ImageLoader.getInstance().removeImage(currentKey);
currentKey = null;
} else if (bitmapDrawable == currentThumb && currentThumbKey != null) {
ImageLoader.getInstance().removeImage(currentThumbKey);
currentThumbKey = null;
}
setImage(currentImageLocation, currentHttpUrl, currentFilter, currentThumb, currentThumbLocation, currentThumbFilter, currentSize, currentCacheOnly);
FileLog.e("tmessages", e);
}
}
canvas.restore();
} else {
canvas.save();
if (orientation != 0) {
if (centerRotation) {
canvas.rotate(orientation, imageW / 2, imageH / 2);
} else {
canvas.rotate(orientation, 0, 0);
}
}
drawRegion.set(imageX, imageY, imageX + imageW, imageY + imageH);
if (orientation == 90 || orientation == 270) {
int width = (drawRegion.right - drawRegion.left) / 2;
int height = (drawRegion.bottom - drawRegion.top) / 2;
int centerX = (drawRegion.right + drawRegion.left) / 2;
int centerY = (drawRegion.top + drawRegion.bottom) / 2;
bitmapDrawable.setBounds(centerX - height, centerY - width, centerX + height, centerY + width);
} else {
bitmapDrawable.setBounds(drawRegion);
}
if (isVisible) {
try {
bitmapDrawable.setAlpha(alpha);
bitmapDrawable.draw(canvas);
} catch (Exception e) {
if (bitmapDrawable == currentImage && currentKey != null) {
ImageLoader.getInstance().removeImage(currentKey);
currentKey = null;
} else if (bitmapDrawable == currentThumb && currentThumbKey != null) {
ImageLoader.getInstance().removeImage(currentThumbKey);
currentThumbKey = null;
}
setImage(currentImageLocation, currentHttpUrl, currentFilter, currentThumb, currentThumbLocation, currentThumbFilter, currentSize, currentCacheOnly);
FileLog.e("tmessages", e);
}
}
canvas.restore();
}
}
drawDrawable(canvas, bitmapDrawable, (int) (overrideAlpha * 255));
}
checkAlphaAnimation();
return true;
} else if (staticThumb != null) {
drawRegion.set(imageX, imageY, imageX + imageW, imageY + imageH);
staticThumb.setBounds(drawRegion);
if (isVisible) {
try {
staticThumb.setAlpha(alpha);
staticThumb.draw(canvas);
} catch (Exception e) {
FileLog.e("tmessages", e);
}
}
drawDrawable(canvas, staticThumb, 255);
checkAlphaAnimation();
return true;
}
} catch (Exception e) {
@ -457,7 +577,7 @@ public class ImageReceiver implements NotificationCenter.NotificationCenterDeleg
}
isVisible = value;
if (invalidate && parentView != null) {
parentView.invalidate();
parentView.invalidate(imageX, imageY, imageX + imageW, imageY + imageH);
}
}
@ -466,7 +586,11 @@ public class ImageReceiver implements NotificationCenter.NotificationCenterDeleg
}
public void setAlpha(float value) {
alpha = (int)(value * 255.0f);
overrideAlpha = value;
}
public void setCrossfadeAlpha(byte value) {
crossfadeAlpha = value;
}
public boolean hasImage() {
@ -630,7 +754,7 @@ public class ImageReceiver implements NotificationCenter.NotificationCenterDeleg
}
}
protected void setImageBitmapByKey(BitmapDrawable bitmap, String key, boolean thumb) {
protected void setImageBitmapByKey(BitmapDrawable bitmap, String key, boolean thumb, boolean memCache) {
if (bitmap == null || key == null) {
return;
}
@ -646,17 +770,38 @@ public class ImageReceiver implements NotificationCenter.NotificationCenterDeleg
roundPaint.setShader(bitmapShader);
bitmapRect.set(0, 0, object.getWidth(), object.getHeight());
}
if (!memCache && !forcePreview) {
if (currentThumb == null && staticThumb == null || currentAlpha == 1.0f) {
currentAlpha = 0.0f;
lastUpdateAlphaTime = System.currentTimeMillis();
crossfadeWithThumb = currentThumb != null || staticThumb != null;
}
} else {
currentAlpha = 1.0f;
}
if (parentView != null) {
parentView.invalidate();
parentView.invalidate(imageX, imageY, imageX + imageW, imageY + imageH);
}
} else if (currentThumb == null && (currentImage == null || forcePreview)) {
if (currentThumbKey == null || !key.equals(currentThumbKey)) {
return;
}
ImageLoader.getInstance().incrementUseCount(currentThumbKey);
currentThumb = bitmap;
if (!memCache && crossfadeAlpha != 2) {
currentAlpha = 0.0f;
lastUpdateAlphaTime = System.currentTimeMillis();
crossfadeWithThumb = staticThumb != null && currentKey == null;
} else {
currentAlpha = 1.0f;
}
if (!(staticThumb instanceof BitmapDrawable) && parentView != null) {
parentView.invalidate();
parentView.invalidate(imageX, imageY, imageX + imageW, imageY + imageH);
}
}
@ -721,7 +866,27 @@ public class ImageReceiver implements NotificationCenter.NotificationCenterDeleg
staticThumb = null;
}
if (parentView != null) {
parentView.invalidate();
parentView.invalidate(imageX, imageY, imageX + imageW, imageY + imageH);
}
}
} else if (id == NotificationCenter.didReplacedPhotoInMemCache) {
String oldKey = (String) args[0];
if (currentKey != null && currentKey.equals(oldKey)) {
currentKey = (String) args[1];
currentImageLocation = (TLRPC.FileLocation) args[2];
}
if (currentThumbKey != null && currentThumbKey.equals(oldKey)) {
currentThumbKey = (String) args[1];
currentThumbLocation = (TLRPC.FileLocation) args[2];
}
if (setImageBackup != null) {
if (currentKey != null && currentKey.equals(oldKey)) {
currentKey = (String) args[1];
currentImageLocation = (TLRPC.FileLocation) args[2];
}
if (currentThumbKey != null && currentThumbKey.equals(oldKey)) {
currentThumbKey = (String) args[1];
currentThumbLocation = (TLRPC.FileLocation) args[2];
}
}
}

View File

@ -309,6 +309,10 @@ public class LocaleController {
}
}
public Locale getSystemDefaultLocale() {
return systemDefaultLocale;
}
public static String getLocaleString(Locale locale) {
if (locale == null) {
return "en";
@ -607,11 +611,12 @@ public class LocaleController {
}
public static String formatString(String key, int res, Object... args) {
String value = getInstance().localeValues.get(key);
if (value == null) {
value = ApplicationLoader.applicationContext.getString(res);
}
try {
String value = getInstance().localeValues.get(key);
if (value == null) {
value = ApplicationLoader.applicationContext.getString(res);
}
if (getInstance().currentLocale != null) {
return String.format(getInstance().currentLocale, value, args);
} else {

View File

@ -117,17 +117,27 @@ public class MediaController implements NotificationCenter.NotificationCenterDel
MediaStore.Images.Media.ORIENTATION
};
private static final String[] projectionVideo = {
MediaStore.Video.Media._ID,
MediaStore.Video.Media.BUCKET_ID,
MediaStore.Video.Media.BUCKET_DISPLAY_NAME,
MediaStore.Video.Media.DATA,
MediaStore.Video.Media.DATE_TAKEN
};
public static class AlbumEntry {
public int bucketId;
public String bucketName;
public PhotoEntry coverPhoto;
public ArrayList<PhotoEntry> photos = new ArrayList<>();
public HashMap<Integer, PhotoEntry> photosByIds = new HashMap<>();
public boolean isVideo;
public AlbumEntry(int bucketId, String bucketName, PhotoEntry coverPhoto) {
public AlbumEntry(int bucketId, String bucketName, PhotoEntry coverPhoto, boolean isVideo) {
this.bucketId = bucketId;
this.bucketName = bucketName;
this.coverPhoto = coverPhoto;
this.isVideo = isVideo;
}
public void addPhoto(PhotoEntry photoEntry) {
@ -144,13 +154,16 @@ public class MediaController implements NotificationCenter.NotificationCenterDel
public int orientation;
public String thumbPath;
public String imagePath;
public boolean isVideo;
public CharSequence caption;
public PhotoEntry(int bucketId, int imageId, long dateTaken, String path, int orientation) {
public PhotoEntry(int bucketId, int imageId, long dateTaken, String path, int orientation, boolean isVideo) {
this.bucketId = bucketId;
this.imageId = imageId;
this.dateTaken = dateTaken;
this.path = path;
this.orientation = orientation;
this.isVideo = isVideo;
}
}
@ -167,6 +180,7 @@ public class MediaController implements NotificationCenter.NotificationCenterDel
public int date;
public String thumbPath;
public String imagePath;
public CharSequence caption;
}
public final static String MIME_TYPE = "video/avc";
@ -178,6 +192,8 @@ public class MediaController implements NotificationCenter.NotificationCenterDel
private final static int PROCESSOR_TYPE_TI = 5;
private final Object videoConvertSync = new Object();
private HashMap<Long, Long> typingTimes = new HashMap<>();
private SensorManager sensorManager;
private Sensor proximitySensor;
private boolean ignoreProximity;
@ -550,6 +566,7 @@ public class MediaController implements NotificationCenter.NotificationCenterDel
videoDownloadQueue.clear();
downloadQueueKeys.clear();
videoConvertQueue.clear();
typingTimes.clear();
cancelVideoConvert(null);
}
@ -975,6 +992,29 @@ public class MediaController implements NotificationCenter.NotificationCenterDel
}
listenerInProgress = false;
processLaterArrays();
try {
ArrayList<SendMessagesHelper.DelayedMessage> delayedMessages = SendMessagesHelper.getInstance().getDelayedMessages(fileName);
if (delayedMessages != null) {
for (SendMessagesHelper.DelayedMessage delayedMessage : delayedMessages) {
if (delayedMessage.encryptedChat == null) {
long dialog_id = delayedMessage.obj.getDialogId();
Long lastTime = typingTimes.get(dialog_id);
if (lastTime == null || lastTime + 4000 < System.currentTimeMillis()) {
if (delayedMessage.videoLocation != null) {
MessagesController.getInstance().sendTyping(dialog_id, 5, 0);
} else if (delayedMessage.documentLocation != null) {
MessagesController.getInstance().sendTyping(dialog_id, 3, 0);
} else if (delayedMessage.location != null) {
MessagesController.getInstance().sendTyping(dialog_id, 4, 0);
}
typingTimes.put(dialog_id, System.currentTimeMillis());
}
}
}
}
} catch (Exception e) {
FileLog.e("tmessages", e);
}
} else if (id == NotificationCenter.messagesDeleted) {
if (playingMessageObject != null) {
ArrayList<Integer> markAsDeletedMessages = (ArrayList<Integer>)args[0];
@ -1970,10 +2010,12 @@ public class MediaController implements NotificationCenter.NotificationCenterDel
@Override
public void run() {
final ArrayList<AlbumEntry> albumsSorted = new ArrayList<>();
final ArrayList<AlbumEntry> videoAlbumsSorted = new ArrayList<>();
HashMap<Integer, AlbumEntry> albums = new HashMap<>();
AlbumEntry allPhotosAlbum = null;
String cameraFolder = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getAbsolutePath() + "/" + "Camera/";
Integer cameraAlbumId = null;
Integer cameraAlbumVideoId = null;
Cursor cursor = null;
try {
@ -1998,10 +2040,10 @@ public class MediaController implements NotificationCenter.NotificationCenterDel
continue;
}
PhotoEntry photoEntry = new PhotoEntry(bucketId, imageId, dateTaken, path, orientation);
PhotoEntry photoEntry = new PhotoEntry(bucketId, imageId, dateTaken, path, orientation, false);
if (allPhotosAlbum == null) {
allPhotosAlbum = new AlbumEntry(0, LocaleController.getString("AllPhotos", R.string.AllPhotos), photoEntry);
allPhotosAlbum = new AlbumEntry(0, LocaleController.getString("AllPhotos", R.string.AllPhotos), photoEntry, false);
albumsSorted.add(0, allPhotosAlbum);
}
if (allPhotosAlbum != null) {
@ -2010,7 +2052,7 @@ public class MediaController implements NotificationCenter.NotificationCenterDel
AlbumEntry albumEntry = albums.get(bucketId);
if (albumEntry == null) {
albumEntry = new AlbumEntry(bucketId, bucketName, photoEntry);
albumEntry = new AlbumEntry(bucketId, bucketName, photoEntry, false);
albums.put(bucketId, albumEntry);
if (cameraAlbumId == null && cameraFolder != null && path != null && path.startsWith(cameraFolder)) {
albumsSorted.add(0, albumEntry);
@ -2034,11 +2076,72 @@ public class MediaController implements NotificationCenter.NotificationCenterDel
}
}
}
try {
albums.clear();
allPhotosAlbum = null;
cursor = MediaStore.Images.Media.query(ApplicationLoader.applicationContext.getContentResolver(), MediaStore.Video.Media.EXTERNAL_CONTENT_URI, projectionVideo, "", null, MediaStore.Video.Media.DATE_TAKEN + " DESC");
if (cursor != null) {
int imageIdColumn = cursor.getColumnIndex(MediaStore.Video.Media._ID);
int bucketIdColumn = cursor.getColumnIndex(MediaStore.Video.Media.BUCKET_ID);
int bucketNameColumn = cursor.getColumnIndex(MediaStore.Video.Media.BUCKET_DISPLAY_NAME);
int dataColumn = cursor.getColumnIndex(MediaStore.Video.Media.DATA);
int dateColumn = cursor.getColumnIndex(MediaStore.Video.Media.DATE_TAKEN);
while (cursor.moveToNext()) {
int imageId = cursor.getInt(imageIdColumn);
int bucketId = cursor.getInt(bucketIdColumn);
String bucketName = cursor.getString(bucketNameColumn);
String path = cursor.getString(dataColumn);
long dateTaken = cursor.getLong(dateColumn);
if (path == null || path.length() == 0) {
continue;
}
PhotoEntry photoEntry = new PhotoEntry(bucketId, imageId, dateTaken, path, 0, true);
if (allPhotosAlbum == null) {
allPhotosAlbum = new AlbumEntry(0, LocaleController.getString("AllVideo", R.string.AllVideo), photoEntry, true);
videoAlbumsSorted.add(0, allPhotosAlbum);
}
if (allPhotosAlbum != null) {
allPhotosAlbum.addPhoto(photoEntry);
}
AlbumEntry albumEntry = albums.get(bucketId);
if (albumEntry == null) {
albumEntry = new AlbumEntry(bucketId, bucketName, photoEntry, true);
albums.put(bucketId, albumEntry);
if (cameraAlbumVideoId == null && cameraFolder != null && path != null && path.startsWith(cameraFolder)) {
videoAlbumsSorted.add(0, albumEntry);
cameraAlbumVideoId = bucketId;
} else {
videoAlbumsSorted.add(albumEntry);
}
}
albumEntry.addPhoto(photoEntry);
}
}
} catch (Exception e) {
FileLog.e("tmessages", e);
} finally {
if (cursor != null) {
try {
cursor.close();
} catch (Exception e) {
FileLog.e("tmessages", e);
}
}
}
final Integer cameraAlbumIdFinal = cameraAlbumId;
final Integer cameraAlbumVideoIdFinal = cameraAlbumVideoId;
AndroidUtilities.runOnUIThread(new Runnable() {
@Override
public void run() {
NotificationCenter.getInstance().postNotificationName(NotificationCenter.albumsDidLoaded, guid, albumsSorted, cameraAlbumIdFinal);
NotificationCenter.getInstance().postNotificationName(NotificationCenter.albumsDidLoaded, guid, albumsSorted, cameraAlbumIdFinal, videoAlbumsSorted, cameraAlbumVideoIdFinal);
}
});
}

View File

@ -42,6 +42,7 @@ public class MessageObject {
public TLRPC.Message messageOwner;
public CharSequence messageText;
public CharSequence linkDescription;
public CharSequence caption;
public MessageObject replyMessageObject;
public int type;
public int contentType;
@ -52,7 +53,7 @@ public class MessageObject {
public int audioProgressSec;
public ArrayList<TLRPC.PhotoSize> photoThumbs;
private static TextPaint textPaint;
public static TextPaint textPaint;
public int lastLineWidth;
public int textWidth;
public int textHeight;
@ -144,17 +145,35 @@ public class MessageObject {
whoUser = MessagesController.getInstance().getUser(message.action.user_id);
}
if (whoUser != null && fromUser != null) {
if (isOut()) {
messageText = replaceWithLink(LocaleController.getString("ActionYouAddUser", R.string.ActionYouAddUser), "un2", whoUser);
} else if (message.action.user_id == UserConfig.getClientUserId()) {
messageText = replaceWithLink(LocaleController.getString("ActionAddUserYou", R.string.ActionAddUserYou), "un1", fromUser);
if (whoUser.id == fromUser.id) {
if (isOut()) {
messageText = LocaleController.getString("ActionAddUserSelf", R.string.ActionAddUserSelf).replace("un1", LocaleController.getString("FromYou", R.string.FromYou));
} else {
messageText = replaceWithLink(LocaleController.getString("ActionAddUserSelf", R.string.ActionAddUserSelf), "un1", fromUser);
}
} else {
messageText = replaceWithLink(LocaleController.getString("ActionAddUser", R.string.ActionAddUser), "un2", whoUser);
messageText = replaceWithLink(messageText, "un1", fromUser);
if (isOut()) {
messageText = replaceWithLink(LocaleController.getString("ActionYouAddUser", R.string.ActionYouAddUser), "un2", whoUser);
} else if (message.action.user_id == UserConfig.getClientUserId()) {
messageText = replaceWithLink(LocaleController.getString("ActionAddUserYou", R.string.ActionAddUserYou), "un1", fromUser);
} else {
messageText = replaceWithLink(LocaleController.getString("ActionAddUser", R.string.ActionAddUser), "un2", whoUser);
messageText = replaceWithLink(messageText, "un1", fromUser);
}
}
} else {
messageText = LocaleController.getString("ActionAddUser", R.string.ActionAddUser).replace("un2", "").replace("un1", "");
}
} else if (message.action instanceof TLRPC.TL_messageActionChatJoinedByLink) {
if (fromUser != null) {
if (isOut()) {
messageText = LocaleController.getString("ActionInviteYou", R.string.ActionInviteYou);
} else {
messageText = replaceWithLink(LocaleController.getString("ActionInviteUser", R.string.ActionInviteUser), "un1", fromUser);
}
} else {
messageText = LocaleController.getString("ActionInviteUser", R.string.ActionInviteUser).replace("un1", "");
}
} else if (message.action instanceof TLRPC.TL_messageActionChatEditPhoto) {
if (isOut()) {
messageText = LocaleController.getString("ActionYouChangedPhoto", R.string.ActionYouChangedPhoto);
@ -279,7 +298,7 @@ public class MessageObject {
messageText = LocaleController.getString("AttachPhoto", R.string.AttachPhoto);
} else if (message.media instanceof TLRPC.TL_messageMediaVideo) {
messageText = LocaleController.getString("AttachVideo", R.string.AttachVideo);
} else if (message.media instanceof TLRPC.TL_messageMediaGeo) {
} else if (message.media instanceof TLRPC.TL_messageMediaGeo || message.media instanceof TLRPC.TL_messageMediaVenue) {
messageText = LocaleController.getString("AttachLocation", R.string.AttachLocation);
} else if (message.media instanceof TLRPC.TL_messageMediaContact) {
messageText = LocaleController.getString("AttachContact", R.string.AttachContact);
@ -314,7 +333,7 @@ public class MessageObject {
contentType = type = 0;
} else if (message.media instanceof TLRPC.TL_messageMediaPhoto) {
contentType = type = 1;
} else if (message.media instanceof TLRPC.TL_messageMediaGeo) {
} else if (message.media instanceof TLRPC.TL_messageMediaGeo || message.media instanceof TLRPC.TL_messageMediaVenue) {
contentType = 1;
type = 4;
} else if (message.media instanceof TLRPC.TL_messageMediaVideo) {
@ -374,6 +393,7 @@ public class MessageObject {
monthKey = String.format("%d_%02d", dateYear, dateMonth);
}
generateCaption();
if (generateLayout) {
generateLayout();
}
@ -569,6 +589,41 @@ public class MessageObject {
}
}
public void generateCaption() {
if (caption != null) {
return;
}
if (messageOwner.media != null && messageOwner.media.caption != null && messageOwner.media.caption.length() > 0) {
caption = Emoji.replaceEmoji(messageOwner.media.caption, textPaint.getFontMetricsInt(), AndroidUtilities.dp(20));
if (containsUrls(caption)) {
try {
Linkify.addLinks((Spannable) caption, Linkify.WEB_URLS);
} catch (Exception e) {
FileLog.e("tmessages", e);
}
addUsernamesAndHashtags(caption);
}
}
}
private void addUsernamesAndHashtags(CharSequence charSequence) {
try {
Pattern pattern = Pattern.compile("(^|\\s)@[a-zA-Z\\d_]{5,32}|(^|\\s)#[\\w\\.]+");
Matcher matcher = pattern.matcher(charSequence);
while (matcher.find()) {
int start = matcher.start();
int end = matcher.end();
if (charSequence.charAt(start) != '@' && charSequence.charAt(start) != '#') {
start++;
}
URLSpanNoUnderline url = new URLSpanNoUnderline(charSequence.subSequence(start, end).toString());
((Spannable) charSequence).setSpan(url, start, end, 0);
}
} catch (Exception e) {
FileLog.e("tmessages", e);
}
}
private void generateLayout() {
if (type != 0 || messageOwner.to_id == null || messageText == null || messageText.length() == 0) {
return;
@ -579,26 +634,19 @@ public class MessageObject {
if (messageText instanceof Spannable && containsUrls(messageText)) {
if (messageText.length() < 100) {
Linkify.addLinks((Spannable) messageText, Linkify.WEB_URLS | Linkify.PHONE_NUMBERS);
} else {
Linkify.addLinks((Spannable) messageText, Linkify.WEB_URLS);
}
try {
Pattern pattern = Pattern.compile("(^|\\s)@[a-zA-Z\\d_]{5,32}|(^|\\s)#[\\w\\.]+");
Matcher matcher = pattern.matcher(messageText);
while (matcher.find()) {
int start = matcher.start();
int end = matcher.end();
if (messageText.charAt(start) != '@' && messageText.charAt(start) != '#') {
start++;
}
URLSpanNoUnderline url = new URLSpanNoUnderline(messageText.subSequence(start, end).toString());
((Spannable) messageText).setSpan(url, start, end, 0);
try {
Linkify.addLinks((Spannable) messageText, Linkify.WEB_URLS | Linkify.PHONE_NUMBERS);
} catch (Exception e) {
FileLog.e("tmessages", e);
}
} else {
try {
Linkify.addLinks((Spannable) messageText, Linkify.WEB_URLS);
} catch (Exception e) {
FileLog.e("tmessages", e);
}
} catch (Exception e) {
FileLog.e("tmessages", e);
}
addUsernamesAndHashtags(messageText);
}
int maxWidth;
@ -763,10 +811,33 @@ public class MessageObject {
return (messageOwner.flags & TLRPC.MESSAGE_FLAG_UNREAD) != 0;
}
public boolean isContentUnread() {
return (messageOwner.flags & TLRPC.MESSAGE_FLAG_CONTENT_UNREAD) != 0;
}
public void setIsRead() {
messageOwner.flags &= ~TLRPC.MESSAGE_FLAG_UNREAD;
}
public int getUnradFlags() {
return getUnreadFlags(messageOwner);
}
public static int getUnreadFlags(TLRPC.Message message) {
int flags = 0;
if ((message.flags & TLRPC.MESSAGE_FLAG_UNREAD) == 0) {
flags |= 1;
}
if ((message.flags & TLRPC.MESSAGE_FLAG_CONTENT_UNREAD) == 0) {
flags |= 2;
}
return flags;
}
public void setContentIsRead() {
messageOwner.flags &= ~TLRPC.MESSAGE_FLAG_CONTENT_UNREAD;
}
public int getId() {
return messageOwner.id;
}
@ -782,18 +853,27 @@ public class MessageObject {
messageOwner.media instanceof TLRPC.TL_messageMediaVideo);
}
public static void setIsUnread(TLRPC.Message message, boolean unread) {
if (unread) {
public static void setUnreadFlags(TLRPC.Message message, int flag) {
if ((flag & 1) == 0) {
message.flags |= TLRPC.MESSAGE_FLAG_UNREAD;
} else {
message.flags &= ~TLRPC.MESSAGE_FLAG_UNREAD;
}
if ((flag & 2) == 0) {
message.flags |= TLRPC.MESSAGE_FLAG_CONTENT_UNREAD;
} else {
message.flags &= ~TLRPC.MESSAGE_FLAG_CONTENT_UNREAD;
}
}
public static boolean isUnread(TLRPC.Message message) {
return (message.flags & TLRPC.MESSAGE_FLAG_UNREAD) != 0;
}
public static boolean isContentUnread(TLRPC.Message message) {
return (message.flags & TLRPC.MESSAGE_FLAG_CONTENT_UNREAD) != 0;
}
public static boolean isOut(TLRPC.Message message) {
return (message.flags & TLRPC.MESSAGE_FLAG_OUT) != 0;
}

View File

@ -22,7 +22,6 @@ import org.telegram.messenger.FileLog;
import org.telegram.messenger.R;
import org.telegram.messenger.RPCRequest;
import org.telegram.messenger.SerializedData;
import org.telegram.messenger.TLClassStore;
import org.telegram.messenger.TLObject;
import org.telegram.messenger.TLRPC;
import org.telegram.messenger.UserConfig;
@ -34,7 +33,6 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Locale;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Semaphore;
@ -45,13 +43,16 @@ public class MessagesController implements NotificationCenter.NotificationCenter
private ConcurrentHashMap<Integer, TLRPC.User> users = new ConcurrentHashMap<>(100, 1.0f, 2);
private ConcurrentHashMap<String, TLRPC.User> usersByUsernames = new ConcurrentHashMap<>(100, 1.0f, 2);
private HashMap<Integer, TLRPC.ExportedChatInvite> exportedChats = new HashMap<>();
public ArrayList<TLRPC.TL_dialog> dialogs = new ArrayList<>();
public ArrayList<TLRPC.TL_dialog> dialogsServerOnly = new ArrayList<>();
public ConcurrentHashMap<Long, TLRPC.TL_dialog> dialogs_dict = new ConcurrentHashMap<>(100, 1.0f, 2);
public HashMap<Integer, MessageObject> dialogMessage = new HashMap<>();
public ConcurrentHashMap<Long, ArrayList<PrintingUser>> printingUsers = new ConcurrentHashMap<>(20, 1.0f, 2);
public HashMap<Long, CharSequence> printingStrings = new HashMap<>();
public HashMap<Long, Boolean> sendingTypings = new HashMap<>();
public HashMap<Long, Integer> printingStringsTypes = new HashMap<>();
public HashMap<Integer, HashMap<Long, Boolean>> sendingTypings = new HashMap<>();
public ConcurrentHashMap<Integer, Integer> onlinePrivacy = new ConcurrentHashMap<>(20, 1.0f, 2);
private int lastPrintingStringCount = 0;
@ -124,6 +125,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter
public static class PrintingUser {
public long lastTime;
public int userId;
public TLRPC.SendMessageAction action;
}
private static volatile MessagesController Instance = null;
@ -163,9 +165,9 @@ public class MessagesController implements NotificationCenter.NotificationCenter
byte[] bytes = Base64.decode(disabledFeaturesString, Base64.DEFAULT);
if (bytes != null) {
SerializedData data = new SerializedData(bytes);
int count = data.readInt32();
int count = data.readInt32(false);
for (int a = 0; a < count; a++) {
TLRPC.TL_disabledFeature feature = (TLRPC.TL_disabledFeature) TLClassStore.Instance().TLdeserialize(data, data.readInt32());
TLRPC.TL_disabledFeature feature = TLRPC.TL_disabledFeature.TLdeserialize(data, data.readInt32(false), false);
if (feature != null && feature.feature != null && feature.description != null) {
disabledFeatures.add(feature);
}
@ -372,6 +374,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter
SecretChatHelper.getInstance().cleanUp();
dialogs_dict.clear();
exportedChats.clear();
dialogs.clear();
dialogsServerOnly.clear();
users.clear();
@ -380,6 +383,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter
dialogMessage.clear();
printingUsers.clear();
printingStrings.clear();
printingStringsTypes.clear();
onlinePrivacy.clear();
totalDialogsCount = 0;
lastPrintingStringCount = 0;
@ -463,6 +467,14 @@ public class MessagesController implements NotificationCenter.NotificationCenter
return chat;
}
public TLRPC.ExportedChatInvite getExportedInvite(int chat_id) {
return exportedChats.get(chat_id);
}
public void putExportedInvite(int chat_id, TLRPC.TL_chatInviteExported invite) {
exportedChats.put(chat_id, invite);
}
public boolean putUser(TLRPC.User user, boolean fromCache) {
if (user == null) {
return false;
@ -564,7 +576,11 @@ public class MessagesController implements NotificationCenter.NotificationCenter
}
public void loadFullChat(final int chat_id, final int classGuid) {
if (loadingFullChats.contains(chat_id) || loadedFullChats.contains(chat_id)) {
loadFullChat(chat_id, classGuid, false);
}
public void loadFullChat(final int chat_id, final int classGuid, boolean force) {
if (loadingFullChats.contains(chat_id) || !force && loadedFullChats.contains(chat_id)) {
return;
}
loadingFullChats.add(chat_id);
@ -580,12 +596,13 @@ public class MessagesController implements NotificationCenter.NotificationCenter
AndroidUtilities.runOnUIThread(new Runnable() {
@Override
public void run() {
loadingFullChats.remove((Integer)chat_id);
exportedChats.put(chat_id, res.full_chat.exported_invite);
loadingFullChats.remove((Integer) chat_id);
loadedFullChats.add(chat_id);
putUsers(res.users, false);
putChats(res.chats, false);
NotificationCenter.getInstance().postNotificationName(NotificationCenter.chatInfoDidLoaded, chat_id, res.full_chat.participants);
NotificationCenter.getInstance().postNotificationName(NotificationCenter.chatInfoDidLoaded, chat_id, res.full_chat.participants, classGuid);
}
});
} else {
@ -617,7 +634,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter
AndroidUtilities.runOnUIThread(new Runnable() {
@Override
public void run() {
loadingFullUsers.remove((Integer)user.id);
loadingFullUsers.remove((Integer) user.id);
loadedFullUsers.add(user.id);
String names = user.first_name + user.last_name + user.username;
TLRPC.TL_userFull userFull = (TLRPC.TL_userFull)response;
@ -1275,15 +1292,72 @@ public class MessagesController implements NotificationCenter.NotificationCenter
}
}
public void updatePrintingStrings() {
private String getUserNameForTyping(TLRPC.User user) {
if (user == null) {
return "";
}
if (user.first_name != null && user.first_name.length() > 0) {
return user.first_name;
} else if (user.last_name != null && user.last_name.length() > 0) {
return user.last_name;
}
return "";
}
private void updatePrintingStrings() {
final HashMap<Long, CharSequence> newPrintingStrings = new HashMap<>();
final HashMap<Long, Integer> newPrintingStringsTypes = new HashMap<>();
ArrayList<Long> keys = new ArrayList<>(printingUsers.keySet());
for (Long key : keys) {
if (key > 0 || key.intValue() == 0) {
newPrintingStrings.put(key, LocaleController.getString("Typing", R.string.Typing));
for (HashMap.Entry<Long, ArrayList<PrintingUser>> entry : printingUsers.entrySet()) {
long key = entry.getKey();
ArrayList<PrintingUser> arr = entry.getValue();
int lower_id = (int) key;
if (lower_id > 0 || lower_id == 0 || arr.size() == 1) {
PrintingUser pu = arr.get(0);
TLRPC.User user = getUser(pu.userId);
if (user == null) {
return;
}
if (pu.action instanceof TLRPC.TL_sendMessageUploadAudioAction || pu.action instanceof TLRPC.TL_sendMessageRecordAudioAction) {
if (lower_id < 0) {
newPrintingStrings.put(key, LocaleController.formatString("IsRecordingAudio", R.string.IsRecordingAudio, getUserNameForTyping(user)));
} else {
newPrintingStrings.put(key, LocaleController.getString("RecordingAudio", R.string.RecordingAudio));
}
newPrintingStringsTypes.put(key, 1);
} else if (pu.action instanceof TLRPC.TL_sendMessageUploadVideoAction || pu.action instanceof TLRPC.TL_sendMessageRecordVideoAction) {
if (lower_id < 0) {
newPrintingStrings.put(key, LocaleController.formatString("IsSendingVideo", R.string.IsSendingVideo, getUserNameForTyping(user)));
} else {
newPrintingStrings.put(key, LocaleController.getString("SendingVideoStatus", R.string.SendingVideoStatus));
}
newPrintingStringsTypes.put(key, 2);
} else if (pu.action instanceof TLRPC.TL_sendMessageUploadDocumentAction) {
if (lower_id < 0) {
newPrintingStrings.put(key, LocaleController.formatString("IsSendingFile", R.string.IsSendingFile, getUserNameForTyping(user)));
} else {
newPrintingStrings.put(key, LocaleController.getString("SendingFile", R.string.SendingFile));
}
newPrintingStringsTypes.put(key, 2);
} else if (pu.action instanceof TLRPC.TL_sendMessageUploadPhotoAction) {
if (lower_id < 0) {
newPrintingStrings.put(key, LocaleController.formatString("IsSendingPhoto", R.string.IsSendingPhoto, getUserNameForTyping(user)));
} else {
newPrintingStrings.put(key, LocaleController.getString("SendingPhoto", R.string.SendingPhoto));
}
newPrintingStringsTypes.put(key, 2);
} else {
if (lower_id < 0) {
newPrintingStrings.put(key, String.format("%s %s", getUserNameForTyping(user), LocaleController.getString("IsTyping", R.string.IsTyping)));
} else {
newPrintingStrings.put(key, LocaleController.getString("Typing", R.string.Typing));
}
newPrintingStringsTypes.put(key, 0);
}
} else {
ArrayList<PrintingUser> arr = printingUsers.get(key);
int count = 0;
String label = "";
for (PrintingUser pu : arr) {
@ -1292,11 +1366,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter
if (label.length() != 0) {
label += ", ";
}
if (user.first_name != null && user.first_name.length() > 0) {
label += user.first_name;
} else if (user.last_name != null && user.last_name.length() > 0) {
label += user.last_name;
}
label += getUserNameForTyping(user);
count++;
}
if (count == 2) {
@ -1304,15 +1374,12 @@ public class MessagesController implements NotificationCenter.NotificationCenter
}
}
if (label.length() != 0) {
if (count > 1) {
if (arr.size() > 2) {
newPrintingStrings.put(key, String.format("%s %s", label, LocaleController.formatPluralString("AndMoreTyping", arr.size() - 2)));
} else {
newPrintingStrings.put(key, String.format("%s %s", label, LocaleController.getString("AreTyping", R.string.AreTyping)));
}
if (arr.size() > 2) {
newPrintingStrings.put(key, String.format("%s %s", label, LocaleController.formatPluralString("AndMoreTyping", arr.size() - 2)));
} else {
newPrintingStrings.put(key, String.format("%s %s", label, LocaleController.getString("IsTyping", R.string.IsTyping)));
newPrintingStrings.put(key, String.format("%s %s", label, LocaleController.getString("AreTyping", R.string.AreTyping)));
}
newPrintingStringsTypes.put(key, 0);
}
}
}
@ -1323,21 +1390,30 @@ public class MessagesController implements NotificationCenter.NotificationCenter
@Override
public void run() {
printingStrings = newPrintingStrings;
printingStringsTypes = newPrintingStringsTypes;
}
});
}
public void cancelTyping(long dialog_id) {
sendingTypings.remove(dialog_id);
public void cancelTyping(int action, long dialog_id) {
HashMap<Long, Boolean> typings = sendingTypings.get(action);
if (typings != null) {
typings.remove(dialog_id);
}
}
public void sendTyping(final long dialog_id, int classGuid) {
public void sendTyping(final long dialog_id, final int action, int classGuid) {
if (dialog_id == 0) {
return;
}
if (sendingTypings.get(dialog_id) != null) {
HashMap<Long, Boolean> typings = sendingTypings.get(action);
if (typings != null && typings.get(dialog_id) != null) {
return;
}
if (typings == null) {
typings = new HashMap<>();
sendingTypings.put(action, typings);
}
int lower_part = (int)dialog_id;
int high_id = (int)(dialog_id >> 32);
if (lower_part != 0) {
@ -1364,21 +1440,41 @@ public class MessagesController implements NotificationCenter.NotificationCenter
return;
}
}
req.action = new TLRPC.TL_sendMessageTypingAction();
sendingTypings.put(dialog_id, true);
if (action == 0) {
req.action = new TLRPC.TL_sendMessageTypingAction();
} else if (action == 1) {
req.action = new TLRPC.TL_sendMessageRecordAudioAction();
} else if (action == 2) {
req.action = new TLRPC.TL_sendMessageCancelAction();
} else if (action == 3) {
req.action = new TLRPC.TL_sendMessageUploadDocumentAction();
} else if (action == 4) {
req.action = new TLRPC.TL_sendMessageUploadPhotoAction();
} else if (action == 5) {
req.action = new TLRPC.TL_sendMessageUploadVideoAction();
}
typings.put(dialog_id, true);
long reqId = ConnectionsManager.getInstance().performRpc(req, new RPCRequest.RPCRequestDelegate() {
@Override
public void run(TLObject response, TLRPC.TL_error error) {
AndroidUtilities.runOnUIThread(new Runnable() {
@Override
public void run() {
sendingTypings.remove(dialog_id);
HashMap<Long, Boolean> typings = sendingTypings.get(action);
if (typings != null) {
typings.remove(dialog_id);
}
}
});
}
}, true, RPCRequest.RPCRequestClassGeneric | RPCRequest.RPCRequestClassFailOnServerErrors);
ConnectionsManager.getInstance().bindRequestToGuid(reqId, classGuid);
if (classGuid != 0) {
ConnectionsManager.getInstance().bindRequestToGuid(reqId, classGuid);
}
} else {
if (action != 0) {
return;
}
TLRPC.EncryptedChat chat = getEncryptedChat(high_id);
if (chat.auth_key != null && chat.auth_key.length > 1 && chat instanceof TLRPC.TL_encryptedChat) {
TLRPC.TL_messages_setEncryptedTyping req = new TLRPC.TL_messages_setEncryptedTyping();
@ -1386,14 +1482,24 @@ public class MessagesController implements NotificationCenter.NotificationCenter
req.peer.chat_id = chat.id;
req.peer.access_hash = chat.access_hash;
req.typing = true;
sendingTypings.put(dialog_id, true);
typings.put(dialog_id, true);
long reqId = ConnectionsManager.getInstance().performRpc(req, new RPCRequest.RPCRequestDelegate() {
@Override
public void run(TLObject response, TLRPC.TL_error error) {
sendingTypings.remove(dialog_id);
AndroidUtilities.runOnUIThread(new Runnable() {
@Override
public void run() {
HashMap<Long, Boolean> typings = sendingTypings.get(action);
if (typings != null) {
typings.remove(dialog_id);
}
}
});
}
}, true, RPCRequest.RPCRequestClassGeneric | RPCRequest.RPCRequestClassFailOnServerErrors);
ConnectionsManager.getInstance().bindRequestToGuid(reqId, classGuid);
if (classGuid != 0) {
ConnectionsManager.getInstance().bindRequestToGuid(reqId, classGuid);
}
}
}
}
@ -1567,7 +1673,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter
currentDialog.unread_count = entry.getValue();
}
}
NotificationCenter.getInstance().postNotificationName(NotificationCenter.dialogsNeedReload);
NotificationCenter.getInstance().postNotificationName(NotificationCenter.updateInterfaces, UPDATE_MASK_READ_DIALOG_MESSAGE);
NotificationsController.getInstance().processDialogsUpdateRead(dialogsToUpdate);
}
});
@ -1803,12 +1909,29 @@ public class MessagesController implements NotificationCenter.NotificationCenter
dialogsEndReached = (dialogsRes.dialogs.size() == 0 || dialogsRes.dialogs.size() != count) && !isCache;
NotificationCenter.getInstance().postNotificationName(NotificationCenter.dialogsNeedReload);
generateUpdateMessage();
}
});
}
});
}
public void markMessageContentAsRead(int mid) {
TLRPC.TL_messages_readMessageContents req = new TLRPC.TL_messages_readMessageContents();
req.id.add(mid);
MessagesStorage.getInstance().markMessagesContentAsRead(req.id);
NotificationCenter.getInstance().postNotificationName(NotificationCenter.messagesReadContent, req.id);
ConnectionsManager.getInstance().performRpc(req, new RPCRequest.RPCRequestDelegate() {
@Override
public void run(TLObject response, TLRPC.TL_error error) {
if (error == null) {
TLRPC.TL_messages_affectedMessages res = (TLRPC.TL_messages_affectedMessages) response;
processNewDifferenceParams(-1, res.pts, -1, res.pts_count);
}
}
});
}
public void markMessageAsRead(final long dialog_id, final long random_id, int ttl) {
if (random_id == 0 || dialog_id == 0 || ttl <= 0) {
return;
@ -2030,7 +2153,6 @@ public class MessagesController implements NotificationCenter.NotificationCenter
public void run() {
putUsers(updates.users, false);
putChats(updates.chats, false);
TLRPC.Chat chat = null;
if (updates.chats != null && !updates.chats.isEmpty()) {
NotificationCenter.getInstance().postNotificationName(NotificationCenter.chatDidCreated, updates.chats.get(0).id);
} else {
@ -2213,6 +2335,35 @@ public class MessagesController implements NotificationCenter.NotificationCenter
});
}
public void generateUpdateMessage() {
Utilities.stageQueue.postRunnable(new Runnable() {
@Override
public void run() {
try {
String build = LocaleController.getString("updateBuild", R.string.updateBuild);
if (build != null) {
int version = Utilities.parseInt(build);
if (version <= UserConfig.lastUpdateVersion) {
return;
}
UserConfig.lastUpdateVersion = version;
UserConfig.saveConfig(false);
}
TLRPC.TL_updateServiceNotification update = new TLRPC.TL_updateServiceNotification();
update.message = LocaleController.getString("updateText", R.string.updateText);
update.media = new TLRPC.TL_messageMediaEmpty();
update.type = "update";
update.popup = false;
ArrayList<TLRPC.Update> updates = new ArrayList<>();
updates.add(update);
processUpdateArray(updates, null, null);
} catch (Exception e) {
FileLog.e("tmessages", e);
}
}
});
}
public void registerForPush(final String regid) {
if (regid == null || regid.length() == 0 || registeringForPush || UserConfig.getClientUserId() == 0) {
return;
@ -2226,7 +2377,10 @@ public class MessagesController implements NotificationCenter.NotificationCenter
req.token = regid;
req.app_sandbox = false;
try {
req.lang_code = LocaleController.getLocaleString(Locale.getDefault());
req.lang_code = LocaleController.getLocaleString(LocaleController.getInstance().getSystemDefaultLocale());
if (req.lang_code == null || req.lang_code.length() == 0) {
req.lang_code = "en";
}
req.device_model = Build.MANUFACTURER + Build.MODEL;
if (req.device_model == null) {
req.device_model = "Android unknown";
@ -2359,7 +2513,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter
} else if (type == 2) {
if (updates.qts <= MessagesStorage.lastQtsValue) {
return 2;
} else if (MessagesStorage.lastQtsValue + 1 == updates.qts) {
} else if (MessagesStorage.lastQtsValue + updates.updates.size() == updates.qts) {
return 0;
} else {
return 1;
@ -2375,14 +2529,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter
Collections.sort(updatesQueue, new Comparator<TLRPC.Updates>() {
@Override
public int compare(TLRPC.Updates updates, TLRPC.Updates updates2) {
int seq1 = getUpdateSeq(updates);
int seq2 = getUpdateSeq(updates2);
if (seq1 == seq2) {
return 0;
} else if (seq1 > seq2) {
return 1;
}
return -1;
return AndroidUtilities.compare(getUpdateSeq(updates), getUpdateSeq(updates2));
}
});
} else if (type == 1) {
@ -2390,12 +2537,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter
Collections.sort(updatesQueue, new Comparator<TLRPC.Updates>() {
@Override
public int compare(TLRPC.Updates updates, TLRPC.Updates updates2) {
if (updates.pts == updates2.pts) {
return 0;
} else if (updates.pts > updates2.pts) {
return 1;
}
return -1;
return AndroidUtilities.compare(updates.pts, updates2.pts);
}
});
} else if (type == 2) {
@ -2403,12 +2545,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter
Collections.sort(updatesQueue, new Comparator<TLRPC.Updates>() {
@Override
public int compare(TLRPC.Updates updates, TLRPC.Updates updates2) {
if (updates.qts == updates2.qts) {
return 0;
} else if (updates.qts > updates2.qts) {
return 1;
}
return -1;
return AndroidUtilities.compare(updates.qts, updates2.qts);
}
});
}
@ -2710,6 +2847,17 @@ public class MessagesController implements NotificationCenter.NotificationCenter
});
}
private int getUpdateType(TLRPC.Update update) {
if (update instanceof TLRPC.TL_updateNewMessage || update instanceof TLRPC.TL_updateReadMessagesContents || update instanceof TLRPC.TL_updateReadHistoryInbox ||
update instanceof TLRPC.TL_updateReadHistoryOutbox || update instanceof TLRPC.TL_updateDeleteMessages) {
return 0;
} else if (update instanceof TLRPC.TL_updateNewEncryptedMessage) {
return 1;
} else {
return 2;
}
}
public void processUpdates(final TLRPC.Updates updates, boolean fromQueue) {
boolean needGetDiff = false;
boolean needReceivedQueue = false;
@ -2719,11 +2867,12 @@ public class MessagesController implements NotificationCenter.NotificationCenter
arr.add(updates.update);
processUpdateArray(arr, null, null);
} else if (updates instanceof TLRPC.TL_updateShortChatMessage || updates instanceof TLRPC.TL_updateShortMessage) {
TLRPC.User user = getUser(updates.user_id);
final int user_id = updates instanceof TLRPC.TL_updateShortChatMessage ? updates.from_id : updates.user_id;
TLRPC.User user = getUser(user_id);
TLRPC.User user2 = null;
if (user == null) {
user = MessagesStorage.getInstance().getUserSync(updates.user_id);
user = MessagesStorage.getInstance().getUserSync(user_id);
putUser(user, true);
}
@ -2763,13 +2912,13 @@ public class MessagesController implements NotificationCenter.NotificationCenter
if ((updates.flags & TLRPC.MESSAGE_FLAG_OUT) != 0) {
message.from_id = UserConfig.getClientUserId();
} else {
message.from_id = updates.user_id;
message.from_id = user_id;
}
message.to_id = new TLRPC.TL_peerUser();
message.to_id.user_id = updates.user_id;
message.dialog_id = updates.user_id;
message.to_id.user_id = user_id;
message.dialog_id = user_id;
} else {
message.from_id = updates.user_id;
message.from_id = user_id;
message.to_id = new TLRPC.TL_peerChat();
message.to_id.chat_id = updates.chat_id;
message.dialog_id = -updates.chat_id;
@ -2798,7 +2947,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter
if (printUpdate) {
NotificationCenter.getInstance().postNotificationName(NotificationCenter.updateInterfaces, UPDATE_MASK_USER_PRINT);
}
updateInterfaceWithMessages(updates.user_id, objArr);
updateInterfaceWithMessages(user_id, objArr);
NotificationCenter.getInstance().postNotificationName(NotificationCenter.dialogsNeedReload);
}
});
@ -2840,7 +2989,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter
if (updatesStartWaitTimePts == 0) {
updatesStartWaitTimePts = System.currentTimeMillis();
}
FileLog.e("tmessages", "add short message to queue");
FileLog.e("tmessages", "add to queue");
updatesQueuePts.add(updates);
} else {
needGetDiff = true;
@ -2849,56 +2998,92 @@ public class MessagesController implements NotificationCenter.NotificationCenter
}
} else if (updates instanceof TLRPC.TL_updatesCombined || updates instanceof TLRPC.TL_updates) {
MessagesStorage.getInstance().putUsersAndChats(updates.users, updates.chats, true, true);
int lastQtsValue = MessagesStorage.lastQtsValue;
Collections.sort(updates.updates, new Comparator<TLRPC.Update>() {
@Override
public int compare(TLRPC.Update lhs, TLRPC.Update rhs) {
int ltype = getUpdateType(lhs);
int rtype = getUpdateType(rhs);
if (ltype != rtype) {
return AndroidUtilities.compare(ltype, rtype);
} else if (ltype == 0) {
return AndroidUtilities.compare(lhs.pts, rhs.pts);
} else if (ltype == 1) {
return AndroidUtilities.compare(lhs.qts, rhs.qts);
}
return 0;
}
});
for (int a = 0; a < updates.updates.size(); a++) {
TLRPC.Update update = updates.updates.get(a);
if (update instanceof TLRPC.TL_updateNewMessage || update instanceof TLRPC.TL_updateReadMessages || update instanceof TLRPC.TL_updateReadHistoryInbox ||
update instanceof TLRPC.TL_updateReadHistoryOutbox || update instanceof TLRPC.TL_updateDeleteMessages) {
if (getUpdateType(update) == 0) {
TLRPC.TL_updates updatesNew = new TLRPC.TL_updates();
updatesNew.updates.add(update);
updatesNew.pts = update.pts;
updatesNew.pts_count = update.pts_count;
if (MessagesStorage.lastPtsValue + update.pts_count == update.pts) {
for (int b = a + 1; b < updates.updates.size(); b++) {
TLRPC.Update update2 = updates.updates.get(b);
if (getUpdateType(update2) == 0 && updatesNew.pts + update2.pts_count == update2.pts) {
updatesNew.updates.add(update2);
updatesNew.pts = update2.pts;
updatesNew.pts_count += update2.pts_count;
updates.updates.remove(b);
b--;
} else {
break;
}
}
if (MessagesStorage.lastPtsValue + updatesNew.pts_count == updatesNew.pts) {
if (!processUpdateArray(updatesNew.updates, updates.users, updates.chats)) {
FileLog.e("tmessages", "need get diff inner TL_updates, seq: " + MessagesStorage.lastSeqValue + " " + updates.seq);
needGetDiff = true;
} else {
MessagesStorage.lastPtsValue = update.pts;
MessagesStorage.lastPtsValue = updatesNew.pts;
}
} else if (MessagesStorage.lastPtsValue != update.pts) {
FileLog.e("tmessages", update + " need get diff, pts: " + MessagesStorage.lastPtsValue + " " + update.pts + " count = " + update.pts_count);
} else if (MessagesStorage.lastPtsValue != updatesNew.pts) {
FileLog.e("tmessages", update + " need get diff, pts: " + MessagesStorage.lastPtsValue + " " + updatesNew.pts + " count = " + updatesNew.pts_count);
if (gettingDifference || updatesStartWaitTimePts == 0 || updatesStartWaitTimePts != 0 && updatesStartWaitTimePts + 1500 > System.currentTimeMillis()) {
if (updatesStartWaitTimePts == 0) {
updatesStartWaitTimePts = System.currentTimeMillis();
}
FileLog.e("tmessages", "add short message to queue");
FileLog.e("tmessages", "add to queue");
updatesQueuePts.add(updatesNew);
} else {
needGetDiff = true;
}
}
} else if (update instanceof TLRPC.TL_updateNewEncryptedMessage) {
} else if (getUpdateType(update) == 1) {
TLRPC.TL_updates updatesNew = new TLRPC.TL_updates();
updatesNew.updates.add(update);
updatesNew.qts = update.qts;
if (MessagesStorage.lastQtsValue == 0 || MessagesStorage.lastQtsValue + 1 == update.qts) {
for (int b = a + 1; b < updates.updates.size(); b++) {
TLRPC.Update update2 = updates.updates.get(b);
if (getUpdateType(update2) == 1 && updatesNew.qts + 1 == update2.qts) {
updatesNew.updates.add(update2);
updatesNew.qts = update2.qts;
updates.updates.remove(b);
b--;
} else {
break;
}
}
if (MessagesStorage.lastQtsValue == 0 || MessagesStorage.lastQtsValue + updatesNew.updates.size() == updatesNew.qts) {
processUpdateArray(updatesNew.updates, updates.users, updates.chats);
MessagesStorage.lastQtsValue = update.qts;
MessagesStorage.lastQtsValue = updatesNew.qts;
needReceivedQueue = true;
} else if (MessagesStorage.lastPtsValue != update.qts) {
FileLog.e("tmessages", update + " need get diff, qts: " + MessagesStorage.lastQtsValue + " " + update.qts);
} else if (MessagesStorage.lastPtsValue != updatesNew.qts) {
FileLog.e("tmessages", update + " need get diff, qts: " + MessagesStorage.lastQtsValue + " " + updatesNew.qts);
if (gettingDifference || updatesStartWaitTimeQts == 0 || updatesStartWaitTimeQts != 0 && updatesStartWaitTimeQts + 1500 > System.currentTimeMillis()) {
if (updatesStartWaitTimeQts == 0) {
updatesStartWaitTimeQts = System.currentTimeMillis();
}
FileLog.e("tmessages", "add short message to queue");
FileLog.e("tmessages", "add to queue");
updatesQueueQts.add(updatesNew);
} else {
needGetDiff = true;
}
}
} else {
continue;
break;
}
updates.updates.remove(a);
a--;
@ -2996,6 +3181,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter
final ArrayList<TLRPC.Message> messagesArr = new ArrayList<>();
final HashMap<Integer, Integer> markAsReadMessagesInbox = new HashMap<>();
final HashMap<Integer, Integer> markAsReadMessagesOutbox = new HashMap<>();
final ArrayList<Integer> markAsReadMessages = new ArrayList<>();
final HashMap<Integer, Integer> markAsReadEncrypted = new HashMap<>();
final ArrayList<Integer> deletedMessages = new ArrayList<>();
boolean printChanged = false;
@ -3078,8 +3264,8 @@ public class MessagesController implements NotificationCenter.NotificationCenter
if (!obj.isOut() && obj.isUnread()) {
pushMessages.add(obj);
}
} else if (update instanceof TLRPC.TL_updateReadMessages) {
//markAsReadMessages.addAll(update.messages); disabled for now
} else if (update instanceof TLRPC.TL_updateReadMessagesContents) {
markAsReadMessages.addAll(update.messages);
} else if (update instanceof TLRPC.TL_updateReadHistoryInbox) {
TLRPC.Peer peer = ((TLRPC.TL_updateReadHistoryInbox) update).peer;
if (peer.chat_id != 0) {
@ -3097,30 +3283,48 @@ public class MessagesController implements NotificationCenter.NotificationCenter
} else if (update instanceof TLRPC.TL_updateDeleteMessages) {
deletedMessages.addAll(update.messages);
} else if (update instanceof TLRPC.TL_updateUserTyping || update instanceof TLRPC.TL_updateChatUserTyping) {
if (update.action instanceof TLRPC.TL_sendMessageTypingAction && update.user_id != UserConfig.getClientUserId()) {
if (update.user_id != UserConfig.getClientUserId()) {
long uid = -update.chat_id;
if (uid == 0) {
uid = update.user_id;
}
ArrayList<PrintingUser> arr = printingUsers.get(uid);
if (arr == null) {
arr = new ArrayList<>();
printingUsers.put(uid, arr);
}
boolean exist = false;
for (PrintingUser u : arr) {
if (u.userId == update.user_id) {
exist = true;
u.lastTime = currentTime;
break;
if (update.action instanceof TLRPC.TL_sendMessageCancelAction) {
if (arr != null) {
for (int a = 0; a < arr.size(); a++) {
PrintingUser pu = arr.get(a);
if (pu.userId == update.user_id) {
arr.remove(a);
printChanged = true;
break;
}
}
if (arr.isEmpty()) {
printingUsers.remove(uid);
}
}
} else {
if (arr == null) {
arr = new ArrayList<>();
printingUsers.put(uid, arr);
}
boolean exist = false;
for (PrintingUser u : arr) {
if (u.userId == update.user_id) {
exist = true;
u.lastTime = currentTime;
u.action = update.action;
break;
}
}
if (!exist) {
PrintingUser newUser = new PrintingUser();
newUser.userId = update.user_id;
newUser.lastTime = currentTime;
newUser.action = update.action;
arr.add(newUser);
printChanged = true;
}
}
if (!exist) {
PrintingUser newUser = new PrintingUser();
newUser.userId = update.user_id;
newUser.lastTime = currentTime;
arr.add(newUser);
printChanged = true;
}
onlinePrivacy.put(update.user_id, ConnectionsManager.getInstance().getCurrentTime());
}
@ -3181,8 +3385,6 @@ public class MessagesController implements NotificationCenter.NotificationCenter
contactsIds.add(-update.user_id);
}
}
} else if (update instanceof TLRPC.TL_updateActivation) {
//DEPRECATED
} else if (update instanceof TLRPC.TL_updateNewAuthorization) {
AndroidUtilities.runOnUIThread(new Runnable() {
@Override
@ -3247,6 +3449,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter
if (u.userId == update.user_id) {
exist = true;
u.lastTime = currentTime;
u.action = new TLRPC.TL_sendMessageTypingAction();
break;
}
}
@ -3254,6 +3457,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter
PrintingUser newUser = new PrintingUser();
newUser.userId = update.user_id;
newUser.lastTime = currentTime;
newUser.action = new TLRPC.TL_sendMessageTypingAction();
arr.add(newUser);
printChanged = true;
}
@ -3304,7 +3508,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter
newMessage.local_id = newMessage.id = UserConfig.getNewMessageId();
UserConfig.saveConfig(false);
newMessage.flags = TLRPC.MESSAGE_FLAG_UNREAD;
newMessage.date = update.date;
newMessage.date = ConnectionsManager.getInstance().getCurrentTime();
newMessage.from_id = 777000;
newMessage.to_id = new TLRPC.TL_peerUser();
newMessage.to_id.user_id = UserConfig.getClientUserId();
@ -3403,14 +3607,16 @@ public class MessagesController implements NotificationCenter.NotificationCenter
}
} else if (update instanceof TLRPC.TL_updateUserName) {
if (currentUser != null) {
if (!(currentUser instanceof TLRPC.TL_userContact)) {
currentUser.first_name = update.first_name;
currentUser.last_name = update.last_name;
}
if (currentUser.username != null && currentUser.username.length() > 0) {
usersByUsernames.remove(currentUser.username);
}
if (update.username != null && update.username.length() > 0) {
usersByUsernames.put(update.username, currentUser);
}
currentUser.first_name = update.first_name;
currentUser.last_name = update.last_name;
currentUser.username = update.username;
}
toDbUser.first_name = update.first_name;
@ -3427,7 +3633,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter
} else if (update instanceof TLRPC.TL_updateUserPhone) {
if (currentUser != null) {
currentUser.phone = update.phone;
Utilities.photoBookQueue.postRunnable(new Runnable() {
Utilities.phoneBookQueue.postRunnable(new Runnable() {
@Override
public void run() {
ContactsController.getInstance().addContactToPhoneBook(currentUser, true);
@ -3563,7 +3769,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter
}
if (!markAsReadEncrypted.isEmpty()) {
for (HashMap.Entry<Integer, Integer> entry : markAsReadEncrypted.entrySet()) {
NotificationCenter.getInstance().postNotificationName(NotificationCenter.messagesReadedEncrypted, entry.getKey(), entry.getValue());
NotificationCenter.getInstance().postNotificationName(NotificationCenter.messagesReadEncrypted, entry.getKey(), entry.getValue());
long dialog_id = (long) (entry.getKey()) << 32;
TLRPC.TL_dialog dialog = dialogs_dict.get(dialog_id);
if (dialog != null) {
@ -3575,6 +3781,9 @@ public class MessagesController implements NotificationCenter.NotificationCenter
}
}
}
if (!markAsReadMessages.isEmpty()) {
NotificationCenter.getInstance().postNotificationName(NotificationCenter.messagesReadContent, markAsReadMessages);
}
if (!deletedMessages.isEmpty()) {
NotificationCenter.getInstance().postNotificationName(NotificationCenter.messagesDeleted, deletedMessages);
for (Integer id : deletedMessages) {
@ -3601,6 +3810,9 @@ public class MessagesController implements NotificationCenter.NotificationCenter
}
MessagesStorage.getInstance().markMessagesAsRead(markAsReadMessagesInbox, markAsReadMessagesOutbox, markAsReadEncrypted, true);
}
if (!markAsReadMessages.isEmpty()) {
MessagesStorage.getInstance().markMessagesContentAsRead(markAsReadMessages);
}
if (!deletedMessages.isEmpty()) {
MessagesStorage.getInstance().markMessagesAsDeleted(deletedMessages, true);
}
@ -3684,7 +3896,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter
NotificationCenter.getInstance().postNotificationName(NotificationCenter.didReceivedNewMessages, uid, messages);
for (MessageObject message : messages) {
if (lastMessage == null || (!isEncryptedChat && message.getId() > lastMessage.getId() || isEncryptedChat && message.getId() < lastMessage.getId()) || message.messageOwner.date > lastMessage.messageOwner.date) {
if (lastMessage == null || (!isEncryptedChat && message.getId() > lastMessage.getId() || (isEncryptedChat || message.getId() < 0 && lastMessage.getId() < 0) && message.getId() < lastMessage.getId()) || message.messageOwner.date > lastMessage.messageOwner.date) {
lastMessage = message;
}
}

View File

@ -106,7 +106,7 @@ public class MessagesStorage {
database.executeFast("CREATE TABLE messages(mid INTEGER PRIMARY KEY, uid INTEGER, read_state INTEGER, send_state INTEGER, date INTEGER, data BLOB, out INTEGER, ttl INTEGER, media INTEGER, replydata BLOB)").stepThis().dispose();
database.executeFast("CREATE TABLE chats(uid INTEGER PRIMARY KEY, name TEXT, data BLOB)").stepThis().dispose();
database.executeFast("CREATE TABLE enc_chats(uid INTEGER PRIMARY KEY, user INTEGER, name TEXT, data BLOB, g BLOB, authkey BLOB, ttl INTEGER, layer INTEGER, seq_in INTEGER, seq_out INTEGER, use_count INTEGER, exchange_id INTEGER, key_date INTEGER, fprint INTEGER, fauthkey BLOB, khash BLOB)").stepThis().dispose();
database.executeFast("CREATE TABLE dialogs(did INTEGER PRIMARY KEY, date INTEGER, unread_count INTEGER, last_mid INTEGER)").stepThis().dispose();
database.executeFast("CREATE TABLE dialogs(did INTEGER PRIMARY KEY, date INTEGER, unread_count INTEGER, last_mid INTEGER, inbox_max INTEGER, outbox_max INTEGER)").stepThis().dispose();
database.executeFast("CREATE TABLE chat_settings(uid INTEGER PRIMARY KEY, participants BLOB)").stepThis().dispose();
database.executeFast("CREATE TABLE contacts(uid INTEGER PRIMARY KEY, mutual INTEGER)").stepThis().dispose();
database.executeFast("CREATE TABLE pending_read(uid INTEGER PRIMARY KEY, max_id INTEGER)").stepThis().dispose();
@ -164,7 +164,7 @@ public class MessagesStorage {
database.executeFast("CREATE TABLE keyvalue(id TEXT PRIMARY KEY, value TEXT)").stepThis().dispose();
//version
database.executeFast("PRAGMA user_version = 16").stepThis().dispose();
database.executeFast("PRAGMA user_version = 17").stepThis().dispose();
} else {
try {
SQLiteCursor cursor = database.queryFinalized("SELECT seq, pts, date, qts, lsv, sg, pbytes FROM params WHERE id = 1");
@ -195,7 +195,7 @@ public class MessagesStorage {
}
}
int version = database.executeInt("PRAGMA user_version");
if (version < 16) {
if (version < 17) {
updateDbToLastVersion(version);
}
}
@ -295,7 +295,7 @@ public class MessagesStorage {
if ((length = cursor.byteBufferValue(1, data.buffer)) != 0) {
for (int a = 0; a < length / 4; a++) {
state.requery();
state.bindInteger(1, data.readInt32());
state.bindInteger(1, data.readInt32(false));
state.bindInteger(2, date);
state.step();
}
@ -385,6 +385,12 @@ public class MessagesStorage {
database.executeFast("PRAGMA user_version = 16").stepThis().dispose();
version = 16;
}
if (version == 16 && version < 17) {
database.executeFast("ALTER TABLE dialogs ADD COLUMN inbox_max INTEGER default 0").stepThis().dispose();
database.executeFast("ALTER TABLE dialogs ADD COLUMN outbox_max INTEGER default 0").stepThis().dispose();
database.executeFast("PRAGMA user_version = 17").stepThis().dispose();
version = 17;
}
} catch (Exception e) {
FileLog.e("tmessages", e);
}
@ -523,12 +529,12 @@ public class MessagesStorage {
ArrayList<Integer> chatIds = new ArrayList<>();
ArrayList<Integer> encryptedChatIds = new ArrayList<>();
cursor = database.queryFinalized("SELECT read_state, data, send_state, mid, date, uid FROM messages WHERE uid IN (" + ids.toString() + ") AND out = 0 AND read_state = 0 ORDER BY date DESC LIMIT 50");
cursor = database.queryFinalized("SELECT read_state, data, send_state, mid, date, uid FROM messages WHERE uid IN (" + ids.toString() + ") AND out = 0 AND read_state IN(0,2) ORDER BY date DESC LIMIT 50");
while (cursor.next()) {
ByteBufferDesc data = buffersStorage.getFreeBuffer(cursor.byteArrayLength(1));
if (data != null && cursor.byteBufferValue(1, data.buffer) != 0) {
TLRPC.Message message = (TLRPC.Message)TLClassStore.Instance().TLdeserialize(data, data.readInt32());
MessageObject.setIsUnread(message, cursor.intValue(0) != 1);
TLRPC.Message message = TLRPC.Message.TLdeserialize(data, data.readInt32(false), false);
MessageObject.setUnreadFlags(message, cursor.intValue(0));
message.id = cursor.intValue(3);
message.date = cursor.intValue(4);
message.dialog_id = cursor.longValue(5);
@ -762,7 +768,7 @@ public class MessagesStorage {
while (cursor.next()) {
ByteBufferDesc data = buffersStorage.getFreeBuffer(cursor.byteArrayLength(0));
if (data != null && cursor.byteBufferValue(0, data.buffer) != 0) {
TLRPC.WallPaper wallPaper = (TLRPC.WallPaper) TLClassStore.Instance().TLdeserialize(data, data.readInt32());
TLRPC.WallPaper wallPaper = TLRPC.WallPaper.TLdeserialize(data, data.readInt32(false), false);
wallPapers.add(wallPaper);
}
buffersStorage.reuseFreeBuffer(data);
@ -876,7 +882,7 @@ public class MessagesStorage {
while (cursor.next()) {
ByteBufferDesc data = buffersStorage.getFreeBuffer(cursor.byteArrayLength(0));
if (data != null && cursor.byteBufferValue(0, data.buffer) != 0) {
TLRPC.Message message = (TLRPC.Message) TLClassStore.Instance().TLdeserialize(data, data.readInt32());
TLRPC.Message message = TLRPC.Message.TLdeserialize(data, data.readInt32(false), false);
if (message == null || message.media == null) {
continue;
}
@ -950,7 +956,7 @@ public class MessagesStorage {
while (cursor.next()) {
ByteBufferDesc data = buffersStorage.getFreeBuffer(cursor.byteArrayLength(0));
if (data != null && cursor.byteBufferValue(0, data.buffer) != 0) {
TLRPC.Photo photo = (TLRPC.Photo)TLClassStore.Instance().TLdeserialize(data, data.readInt32());
TLRPC.Photo photo = TLRPC.Photo.TLdeserialize(data, data.readInt32(false), false);
res.photos.add(photo);
}
buffersStorage.reuseFreeBuffer(data);
@ -1065,7 +1071,7 @@ public class MessagesStorage {
StringBuilder mids = new StringBuilder();
SQLiteCursor cursor = null;
if (random_ids == null) {
cursor = database.queryFinalized(String.format(Locale.US, "SELECT mid, ttl FROM messages WHERE uid = %d AND out = %d AND read_state = 1 AND ttl > 0 AND date <= %d AND send_state = 0 AND media != 1", ((long) chat_id) << 32, isOut, time));
cursor = database.queryFinalized(String.format(Locale.US, "SELECT mid, ttl FROM messages WHERE uid = %d AND out = %d AND read_state != 0 AND ttl > 0 AND date <= %d AND send_state = 0 AND media != 1", ((long) chat_id) << 32, isOut, time));
} else {
String ids = TextUtils.join(",", random_ids);
cursor = database.queryFinalized(String.format(Locale.US, "SELECT m.mid, m.ttl FROM messages as m INNER JOIN randoms as r ON m.mid = r.mid WHERE r.random_id IN (%s)", ids));
@ -1147,7 +1153,7 @@ public class MessagesStorage {
cursor.dispose();
} else if (inbox != null && !inbox.isEmpty()) {
for (HashMap.Entry<Integer, Integer> entry : inbox.entrySet()) {
SQLiteCursor cursor = database.queryFinalized(String.format(Locale.US, "SELECT COUNT(mid) FROM messages WHERE uid = %d AND mid <= %d AND read_state = 0 AND out = 0", entry.getKey(), entry.getValue()));
SQLiteCursor cursor = database.queryFinalized(String.format(Locale.US, "SELECT COUNT(mid) FROM messages WHERE uid = %d AND mid <= %d AND read_state IN(0,2) AND out = 0", entry.getKey(), entry.getValue()));
if (cursor.next()) {
int count = cursor.intValue(0);
if (count == 0) {
@ -1255,7 +1261,7 @@ public class MessagesStorage {
if (cursor.next()) {
ByteBufferDesc data = buffersStorage.getFreeBuffer(cursor.byteArrayLength(0));
if (data != null && cursor.byteBufferValue(0, data.buffer) != 0) {
info = (TLRPC.ChatParticipants)TLClassStore.Instance().TLdeserialize(data, data.readInt32());
info = TLRPC.ChatParticipants.TLdeserialize(data, data.readInt32(false), false);
}
buffersStorage.reuseFreeBuffer(data);
}
@ -1318,7 +1324,7 @@ public class MessagesStorage {
if (cursor.next()) {
ByteBufferDesc data = buffersStorage.getFreeBuffer(cursor.byteArrayLength(0));
if (data != null && cursor.byteBufferValue(0, data.buffer) != 0) {
info = (TLRPC.ChatParticipants)TLClassStore.Instance().TLdeserialize(data, data.readInt32());
info = TLRPC.ChatParticipants.TLdeserialize(data, data.readInt32(false), false);
}
buffersStorage.reuseFreeBuffer(data);
}
@ -1383,14 +1389,14 @@ public class MessagesStorage {
int lower_id = (int)dialog_id;
if (lower_id != 0) {
state = database.executeFast("UPDATE messages SET read_state = 1 WHERE uid = ? AND mid <= ? AND read_state = 0 AND out = 0");
state = database.executeFast("UPDATE messages SET read_state = read_state | 1 WHERE uid = ? AND mid <= ? AND read_state IN(0,2) AND out = 0");
state.requery();
state.bindLong(1, dialog_id);
state.bindInteger(2, max_id);
state.step();
state.dispose();
} else {
state = database.executeFast("UPDATE messages SET read_state = 1 WHERE uid = ? AND date <= ? AND read_state = 0 AND out = 0");
state = database.executeFast("UPDATE messages SET read_state = read_state | 1 WHERE uid = ? AND date <= ? AND read_state IN(0,2) AND out = 0");
state.requery();
state.bindLong(1, dialog_id);
state.bindInteger(2, max_date);
@ -1613,9 +1619,9 @@ public class MessagesStorage {
while (cursor.next()) {
ByteBufferDesc data = buffersStorage.getFreeBuffer(cursor.byteArrayLength(1));
if (data != null && cursor.byteBufferValue(1, data.buffer) != 0) {
TLRPC.Message message = (TLRPC.Message)TLClassStore.Instance().TLdeserialize(data, data.readInt32());
TLRPC.Message message = TLRPC.Message.TLdeserialize(data, data.readInt32(false), false);
if (!messageHashMap.containsKey(message.id)) {
MessageObject.setIsUnread(message, cursor.intValue(0) != 1);
MessageObject.setUnreadFlags(message, cursor.intValue(0));
message.id = cursor.intValue(3);
message.date = cursor.intValue(4);
if (!cursor.isNull(5)) {
@ -1798,14 +1804,14 @@ public class MessagesStorage {
}
cursor.dispose();
cursor = database.queryFinalized(String.format(Locale.US, "SELECT min(mid), max(date) FROM messages WHERE uid = %d AND out = 0 AND read_state = 0 AND mid > 0", dialog_id));
cursor = database.queryFinalized(String.format(Locale.US, "SELECT min(mid), max(date) FROM messages WHERE uid = %d AND out = 0 AND read_state IN(0,2) AND mid > 0", dialog_id));
if (cursor.next()) {
min_unread_id = cursor.intValue(0);
max_unread_date = cursor.intValue(1);
}
cursor.dispose();
if (min_unread_id != 0) {
cursor = database.queryFinalized(String.format(Locale.US, "SELECT COUNT(*) FROM messages WHERE uid = %d AND mid >= %d AND out = 0 AND read_state = 0", dialog_id, min_unread_id));
cursor = database.queryFinalized(String.format(Locale.US, "SELECT COUNT(*) FROM messages WHERE uid = %d AND mid >= %d AND out = 0 AND read_state IN(0,2)", dialog_id, min_unread_id));
if (cursor.next()) {
count_unread = cursor.intValue(0);
}
@ -1843,14 +1849,14 @@ public class MessagesStorage {
}
cursor.dispose();
cursor = database.queryFinalized(String.format(Locale.US, "SELECT max(mid), max(date) FROM messages WHERE uid = %d AND out = 0 AND read_state = 0 AND mid < 0", dialog_id));
cursor = database.queryFinalized(String.format(Locale.US, "SELECT max(mid), max(date) FROM messages WHERE uid = %d AND out = 0 AND read_state IN(0,2) AND mid < 0", dialog_id));
if (cursor.next()) {
min_unread_id = cursor.intValue(0);
max_unread_date = cursor.intValue(1);
}
cursor.dispose();
if (min_unread_id != 0) {
cursor = database.queryFinalized(String.format(Locale.US, "SELECT COUNT(*) FROM messages WHERE uid = %d AND mid <= %d AND out = 0 AND read_state = 0", dialog_id, min_unread_id));
cursor = database.queryFinalized(String.format(Locale.US, "SELECT COUNT(*) FROM messages WHERE uid = %d AND mid <= %d AND out = 0 AND read_state IN(0,2)", dialog_id, min_unread_id));
if (cursor.next()) {
count_unread = cursor.intValue(0);
}
@ -1876,8 +1882,8 @@ public class MessagesStorage {
while (cursor.next()) {
ByteBufferDesc data = buffersStorage.getFreeBuffer(cursor.byteArrayLength(1));
if (data != null && cursor.byteBufferValue(1, data.buffer) != 0) {
TLRPC.Message message = (TLRPC.Message) TLClassStore.Instance().TLdeserialize(data, data.readInt32());
MessageObject.setIsUnread(message, cursor.intValue(0) != 1);
TLRPC.Message message = TLRPC.Message.TLdeserialize(data, data.readInt32(false), false);
MessageObject.setUnreadFlags(message, cursor.intValue(0));
message.id = cursor.intValue(3);
message.date = cursor.intValue(4);
message.dialog_id = dialog_id;
@ -1900,7 +1906,7 @@ public class MessagesStorage {
if (!cursor.isNull(6)) {
ByteBufferDesc data2 = buffersStorage.getFreeBuffer(cursor.byteArrayLength(6));
if (data2 != null && cursor.byteBufferValue(6, data2.buffer) != 0) {
message.replyMessage = (TLRPC.Message) TLClassStore.Instance().TLdeserialize(data2, data2.readInt32());
message.replyMessage = TLRPC.Message.TLdeserialize(data2, data2.readInt32(false), false);
if (message.replyMessage != null) {
fromUser.add(message.replyMessage.from_id);
if (message.replyMessage.action != null && message.replyMessage.action.user_id != 0) {
@ -1987,7 +1993,7 @@ public class MessagesStorage {
while (cursor.next()) {
ByteBufferDesc data = buffersStorage.getFreeBuffer(cursor.byteArrayLength(0));
if (data != null && cursor.byteBufferValue(0, data.buffer) != 0) {
TLRPC.Message message = (TLRPC.Message) TLClassStore.Instance().TLdeserialize(data, data.readInt32());
TLRPC.Message message = TLRPC.Message.TLdeserialize(data, data.readInt32(false), false);
message.id = cursor.intValue(1);
message.date = cursor.intValue(2);
message.dialog_id = dialog_id;
@ -2091,7 +2097,7 @@ public class MessagesStorage {
if (cursor.next()) {
ByteBufferDesc data = buffersStorage.getFreeBuffer(cursor.byteArrayLength(0));
if (data != null && cursor.byteBufferValue(0, data.buffer) != 0) {
TLObject file = TLClassStore.Instance().TLdeserialize(data, data.readInt32());
TLObject file = TLClassStore.Instance().TLdeserialize(data, data.readInt32(false), false);
if (file != null) {
result.add(file);
}
@ -2374,11 +2380,13 @@ public class MessagesStorage {
buffersStorage.reuseFreeBuffer(data5);
if (dialog != null) {
state = database.executeFast("REPLACE INTO dialogs(did, date, unread_count, last_mid) VALUES(?, ?, ?, ?)");
state = database.executeFast("REPLACE INTO dialogs(did, date, unread_count, last_mid, inbox_max, outbox_max) VALUES(?, ?, ?, ?, ?, ?)");
state.bindLong(1, dialog.id);
state.bindInteger(2, dialog.last_message_date);
state.bindInteger(3, dialog.unread_count);
state.bindInteger(4, dialog.top_message);
state.bindInteger(5, dialog.read_inbox_max_id);
state.bindInteger(6, 0);
state.step();
state.dispose();
}
@ -2469,7 +2477,7 @@ public class MessagesStorage {
try {
ByteBufferDesc data = buffersStorage.getFreeBuffer(cursor.byteArrayLength(0));
if (data != null && cursor.byteBufferValue(0, data.buffer) != 0) {
TLRPC.User user = (TLRPC.User)TLClassStore.Instance().TLdeserialize(data, data.readInt32());
TLRPC.User user = TLRPC.User.TLdeserialize(data, data.readInt32(false), false);
if (user != null) {
if (user.status != null) {
user.status.expires = cursor.intValue(1);
@ -2494,7 +2502,7 @@ public class MessagesStorage {
try {
ByteBufferDesc data = buffersStorage.getFreeBuffer(cursor.byteArrayLength(0));
if (data != null && cursor.byteBufferValue(0, data.buffer) != 0) {
TLRPC.Chat chat = (TLRPC.Chat)TLClassStore.Instance().TLdeserialize(data, data.readInt32());
TLRPC.Chat chat = TLRPC.Chat.TLdeserialize(data, data.readInt32(false), false);
if (chat != null) {
result.add(chat);
}
@ -2511,13 +2519,12 @@ public class MessagesStorage {
if (chatsToLoad == null || chatsToLoad.length() == 0 || result == null) {
return;
}
//use_count INTEGER, exchange_id INTEGER, key_date INTEGER, fprint INTEGER, fauthkey BLOB
SQLiteCursor cursor = database.queryFinalized(String.format(Locale.US, "SELECT data, user, g, authkey, ttl, layer, seq_in, seq_out, use_count, exchange_id, key_date, fprint, fauthkey, khash FROM enc_chats WHERE uid IN(%s)", chatsToLoad));
while (cursor.next()) {
try {
ByteBufferDesc data = buffersStorage.getFreeBuffer(cursor.byteArrayLength(0));
if (data != null && cursor.byteBufferValue(0, data.buffer) != 0) {
TLRPC.EncryptedChat chat = (TLRPC.EncryptedChat)TLClassStore.Instance().TLdeserialize(data, data.readInt32());
TLRPC.EncryptedChat chat = TLRPC.EncryptedChat.TLdeserialize(data, data.readInt32(false), false);
if (chat != null) {
chat.user_id = cursor.intValue(1);
if (usersToLoad != null && !usersToLoad.contains(chat.user_id)) {
@ -2634,7 +2641,7 @@ public class MessagesStorage {
downloadObject.id = cursor.longValue(0);
ByteBufferDesc data = buffersStorage.getFreeBuffer(cursor.byteArrayLength(2));
if (data != null && cursor.byteBufferValue(2, data.buffer) != 0) {
downloadObject.object = TLClassStore.Instance().TLdeserialize(data, data.readInt32());
downloadObject.object = TLClassStore.Instance().TLdeserialize(data, data.readInt32(false), false);
}
buffersStorage.reuseFreeBuffer(data);
objects.add(downloadObject);
@ -2691,7 +2698,7 @@ public class MessagesStorage {
int mid = cursor.intValue(0);
ByteBufferDesc data = buffersStorage.getFreeBuffer(cursor.byteArrayLength(1));
if (data != null && cursor.byteBufferValue(1, data.buffer) != 0) {
TLRPC.Message message = (TLRPC.Message)TLClassStore.Instance().TLdeserialize(data, data.readInt32());
TLRPC.Message message = TLRPC.Message.TLdeserialize(data, data.readInt32(false), false);
if (message.media instanceof TLRPC.TL_messageMediaWebPage) {
message.id = mid;
message.media.webpage = webPages.get(message.media.webpage.id);
@ -2865,7 +2872,7 @@ public class MessagesStorage {
state.bindInteger(1, messageId);
state.bindLong(2, dialog_id);
state.bindInteger(3, (MessageObject.isUnread(message) ? 0 : 1));
state.bindInteger(3, MessageObject.getUnreadFlags(message));
state.bindInteger(4, message.send_state);
state.bindInteger(5, message.date);
state.bindByteBuffer(6, data.buffer);
@ -2954,7 +2961,7 @@ public class MessagesStorage {
state4.dispose();
state5.dispose();
state = database.executeFast("REPLACE INTO dialogs(did, date, unread_count, last_mid) VALUES(?, ?, ?, ?)");
state = database.executeFast("REPLACE INTO dialogs(did, date, unread_count, last_mid, inbox_max, outbox_max) VALUES(?, ?, ?, ?, ?, ?)");
for (HashMap.Entry<Long, TLRPC.Message> pair : messagesMap.entrySet()) {
Long key = pair.getKey();
@ -2987,6 +2994,8 @@ public class MessagesStorage {
}
state.bindInteger(3, old_unread_count + unread_count);
state.bindInteger(4, messageId);
state.bindInteger(5, 0);
state.bindInteger(6, 0);
state.step();
}
state.dispose();
@ -3294,8 +3303,10 @@ public class MessagesStorage {
TLRPC.User updateUser = usersDict.get(user.id);
if (updateUser != null) {
if (updateUser.first_name != null && updateUser.last_name != null) {
user.first_name = updateUser.first_name;
user.last_name = updateUser.last_name;
if (!(user instanceof TLRPC.TL_userContact)) {
user.first_name = updateUser.first_name;
user.last_name = updateUser.last_name;
}
user.username = updateUser.username;
} else if (updateUser.photo != null) {
user.photo = updateUser.photo;
@ -3337,25 +3348,22 @@ public class MessagesStorage {
}
private void markMessagesAsReadInternal(HashMap<Integer, Integer> inbox, HashMap<Integer, Integer> outbox, HashMap<Integer, Integer> encryptedMessages) {
if (Thread.currentThread().getId() != storageQueue.getId()) {
throw new RuntimeException("wrong db thread");
}
try {
if (inbox != null) {
for (HashMap.Entry<Integer, Integer> entry : inbox.entrySet()) {
database.executeFast(String.format(Locale.US, "UPDATE messages SET read_state = 1 WHERE uid = %d AND mid <= %d AND read_state = 0 AND out = 0", entry.getKey(), entry.getValue())).stepThis().dispose();
database.executeFast(String.format(Locale.US, "UPDATE messages SET read_state = read_state | 1 WHERE uid = %d AND mid <= %d AND read_state IN(0,2) AND out = 0", entry.getKey(), entry.getValue())).stepThis().dispose();
}
}
if (outbox != null) {
for (HashMap.Entry<Integer, Integer> entry : outbox.entrySet()) {
database.executeFast(String.format(Locale.US, "UPDATE messages SET read_state = 1 WHERE uid = %d AND mid <= %d AND read_state = 0 AND out = 1", entry.getKey(), entry.getValue())).stepThis().dispose();
database.executeFast(String.format(Locale.US, "UPDATE messages SET read_state = read_state | 1 WHERE uid = %d AND mid <= %d AND read_state IN(0,2) AND out = 1", entry.getKey(), entry.getValue())).stepThis().dispose();
}
}
if (encryptedMessages != null && !encryptedMessages.isEmpty()) {
for (HashMap.Entry<Integer, Integer> entry : encryptedMessages.entrySet()) {
long dialog_id = ((long)entry.getKey()) << 32;
int max_date = entry.getValue();
SQLitePreparedStatement state = database.executeFast("UPDATE messages SET read_state = 1 WHERE uid = ? AND date <= ? AND read_state = 0 AND out = 1");
SQLitePreparedStatement state = database.executeFast("UPDATE messages SET read_state = read_state | 1 WHERE uid = ? AND date <= ? AND read_state IN(0,2) AND out = 1");
state.requery();
state.bindLong(1, dialog_id);
state.bindInteger(2, max_date);
@ -3368,6 +3376,22 @@ public class MessagesStorage {
}
}
public void markMessagesContentAsRead(final ArrayList<Integer> mids) {
if (mids == null || mids.isEmpty()) {
return;
}
storageQueue.postRunnable(new Runnable() {
@Override
public void run() {
try {
database.executeFast(String.format(Locale.US, "UPDATE messages SET read_state = read_state | 2 WHERE mid IN (%s)", TextUtils.join(",", mids))).stepThis().dispose();
} catch (Exception e) {
FileLog.e("tmessages", e);
}
}
});
}
public void markMessagesAsRead(final HashMap<Integer, Integer> inbox, final HashMap<Integer, Integer> outbox, final HashMap<Integer, Integer> encryptedMessages, boolean useQueue) {
if (useQueue) {
storageQueue.postRunnable(new Runnable() {
@ -3433,7 +3457,7 @@ public class MessagesStorage {
}
ByteBufferDesc data = buffersStorage.getFreeBuffer(cursor.byteArrayLength(1));
if (data != null && cursor.byteBufferValue(1, data.buffer) != 0) {
TLRPC.Message message = (TLRPC.Message)TLClassStore.Instance().TLdeserialize(data, data.readInt32());
TLRPC.Message message = TLRPC.Message.TLdeserialize(data, data.readInt32(false), false);
if (message == null || message.media == null) {
continue;
}
@ -3527,8 +3551,8 @@ public class MessagesStorage {
ByteBufferDesc data = buffersStorage.getFreeBuffer(cursor.byteArrayLength(4));
if (data != null && cursor.byteBufferValue(4, data.buffer) != 0) {
TLRPC.Message message = (TLRPC.Message)TLClassStore.Instance().TLdeserialize(data, data.readInt32());
MessageObject.setIsUnread(message, cursor.intValue(5) != 1);
TLRPC.Message message = TLRPC.Message.TLdeserialize(data, data.readInt32(false), false);
MessageObject.setUnreadFlags(message, cursor.intValue(5));
message.id = cursor.intValue(6);
message.send_state = cursor.intValue(7);
int date = cursor.intValue(8);
@ -3667,7 +3691,7 @@ public class MessagesStorage {
message.serializeToStream(data);
state.bindInteger(1, message.id);
state.bindLong(2, dialog_id);
state.bindInteger(3, (MessageObject.isUnread(message) ? 0 : 1));
state.bindInteger(3, MessageObject.getUnreadFlags(message));
state.bindInteger(4, message.send_state);
state.bindInteger(5, message.date);
state.bindByteBuffer(6, data.buffer);
@ -3732,9 +3756,9 @@ public class MessagesStorage {
ByteBufferDesc data = buffersStorage.getFreeBuffer(cursor.byteArrayLength(4));
if (data != null && cursor.byteBufferValue(4, data.buffer) != 0) {
TLRPC.Message message = (TLRPC.Message)TLClassStore.Instance().TLdeserialize(data, data.readInt32());
TLRPC.Message message = TLRPC.Message.TLdeserialize(data, data.readInt32(false), false);
if (message != null) {
MessageObject.setIsUnread(message, cursor.intValue(5) != 1);
MessageObject.setUnreadFlags(message, cursor.intValue(5));
message.id = cursor.intValue(6);
int date = cursor.intValue(9);
if (date != 0) {
@ -3831,7 +3855,7 @@ public class MessagesStorage {
if (!dialogs.dialogs.isEmpty()) {
SQLitePreparedStatement state = database.executeFast("REPLACE INTO messages VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, NULL)");
SQLitePreparedStatement state2 = database.executeFast("REPLACE INTO dialogs(did, date, unread_count, last_mid) VALUES(?, ?, ?, ?)");
SQLitePreparedStatement state2 = database.executeFast("REPLACE INTO dialogs(did, date, unread_count, last_mid, inbox_max, outbox_max) VALUES(?, ?, ?, ?, ?, ?)");
SQLitePreparedStatement state3 = database.executeFast("REPLACE INTO media_v2 VALUES(?, ?, ?, ?, ?)");
SQLitePreparedStatement state4 = database.executeFast("REPLACE INTO dialog_settings VALUES(?, ?)");
@ -3850,7 +3874,7 @@ public class MessagesStorage {
state.bindInteger(1, message.id);
state.bindInteger(2, uid);
state.bindInteger(3, (MessageObject.isUnread(message) ? 0 : 1));
state.bindInteger(3, MessageObject.getUnreadFlags(message));
state.bindInteger(4, message.send_state);
state.bindInteger(5, message.date);
state.bindByteBuffer(6, data.buffer);
@ -3863,6 +3887,8 @@ public class MessagesStorage {
state2.bindInteger(2, message.date);
state2.bindInteger(3, dialog.unread_count);
state2.bindInteger(4, dialog.top_message);
state2.bindInteger(5, dialog.read_inbox_max_id);
state2.bindInteger(6, 0);
state2.step();
state4.bindLong(1, uid);

View File

@ -23,7 +23,7 @@ import java.util.zip.ZipFile;
public class NativeLoader {
private final static int LIB_VERSION = 7;
private final static int LIB_VERSION = 8;
private final static String LIB_NAME = "tmessages." + LIB_VERSION;
private final static String LIB_SO_NAME = "lib" + LIB_NAME + ".so";
private final static String LOCALE_LIB_SO_NAME = "lib" + LIB_NAME + "loc.so";

View File

@ -32,7 +32,7 @@ public class NotificationCenter {
public static final int mediaDidLoaded = totalEvents++;
public static final int mediaCountDidLoaded = totalEvents++;
public static final int encryptedChatUpdated = totalEvents++;
public static final int messagesReadedEncrypted = totalEvents++;
public static final int messagesReadEncrypted = totalEvents++;
public static final int encryptedChatCreated = totalEvents++;
public static final int userPhotosLoaded = totalEvents++;
public static final int removeAllMessagesFromDialog = totalEvents++;
@ -56,6 +56,9 @@ public class NotificationCenter {
public static final int newSessionReceived = totalEvents++;
public static final int didReceivedWebpages = totalEvents++;
public static final int didReceivedWebpagesInUpdates = totalEvents++;
public static final int stickersDidLoaded = totalEvents++;
public static final int didReplacedPhotoInMemCache = totalEvents++;
public static final int messagesReadContent = totalEvents++;
public static final int httpFileDidLoaded = totalEvents++;
public static final int httpFileDidFailedLoad = totalEvents++;

View File

@ -17,10 +17,10 @@ import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.res.AssetFileDescriptor;
import android.graphics.Point;
import android.graphics.drawable.BitmapDrawable;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.SoundPool;
import android.net.Uri;
import android.os.Build;
import android.os.SystemClock;
@ -54,7 +54,9 @@ public class NotificationsController {
private DispatchQueue notificationsQueue = new DispatchQueue("notificationsQueue");
private ArrayList<MessageObject> pushMessages = new ArrayList<>();
private ArrayList<MessageObject> delayedPushMessages = new ArrayList<>();
private HashMap<Integer, MessageObject> pushMessagesDict = new HashMap<>();
private HashMap<Long, Point> smartNotificationsDialogs = new HashMap<>();
private NotificationManagerCompat notificationManager = null;
private HashMap<Long, Integer> pushDialogs = new HashMap<>();
private HashMap<Long, Integer> wearNoticationsIds = new HashMap<>();
@ -67,10 +69,14 @@ public class NotificationsController {
private boolean notifyCheck = false;
private int lastOnlineFromOtherDevice = 0;
private boolean inChatSoundEnabled = true;
private int lastBadgeCount;
private long lastSoundPlay;
private MediaPlayer mediaPlayerIn;
private MediaPlayer mediaPlayerOut;
//private MediaPlayer mediaPlayerIn;
//private MediaPlayer mediaPlayerOut;
private SoundPool soundPool;
private int soundIn;
private int soundOut;
protected AudioManager audioManager;
private static volatile NotificationsController Instance = null;
@ -110,6 +116,7 @@ public class NotificationsController {
popupMessages.clear();
wearNoticationsIds.clear();
notifyCheck = false;
lastBadgeCount = 0;
SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("Notifications", Context.MODE_PRIVATE);
SharedPreferences.Editor editor = preferences.edit();
editor.clear();
@ -187,7 +194,7 @@ public class NotificationsController {
msg = LocaleController.formatString("NotificationMessageVideo", R.string.NotificationMessageVideo, ContactsController.formatName(user.first_name, user.last_name));
} else if (messageObject.messageOwner.media instanceof TLRPC.TL_messageMediaContact) {
msg = LocaleController.formatString("NotificationMessageContact", R.string.NotificationMessageContact, ContactsController.formatName(user.first_name, user.last_name));
} else if (messageObject.messageOwner.media instanceof TLRPC.TL_messageMediaGeo) {
} else if (messageObject.messageOwner.media instanceof TLRPC.TL_messageMediaGeo || messageObject.messageOwner.media instanceof TLRPC.TL_messageMediaVenue) {
msg = LocaleController.formatString("NotificationMessageMap", R.string.NotificationMessageMap, ContactsController.formatName(user.first_name, user.last_name));
} else if (messageObject.messageOwner.media instanceof TLRPC.TL_messageMediaDocument) {
if (messageObject.isSticker()) {
@ -214,8 +221,14 @@ public class NotificationsController {
if (u2 == null) {
return null;
}
msg = LocaleController.formatString("NotificationGroupAddMember", R.string.NotificationGroupAddMember, ContactsController.formatName(user.first_name, user.last_name), chat.title, ContactsController.formatName(u2.first_name, u2.last_name));
if (user.id == u2.id) {
msg = LocaleController.formatString("NotificationGroupAddSelf", R.string.NotificationGroupAddSelf, ContactsController.formatName(user.first_name, user.last_name), chat.title);
} else {
msg = LocaleController.formatString("NotificationGroupAddMember", R.string.NotificationGroupAddMember, ContactsController.formatName(user.first_name, user.last_name), chat.title, ContactsController.formatName(u2.first_name, u2.last_name));
}
}
} else if (messageObject.messageOwner.action instanceof TLRPC.TL_messageActionChatJoinedByLink) {
msg = LocaleController.formatString("NotificationInvitedToGroupByLink", R.string.NotificationInvitedToGroupByLink, ContactsController.formatName(user.first_name, user.last_name), chat.title);
} else if (messageObject.messageOwner.action instanceof TLRPC.TL_messageActionChatEditTitle) {
msg = LocaleController.formatString("NotificationEditedGroupName", R.string.NotificationEditedGroupName, ContactsController.formatName(user.first_name, user.last_name), messageObject.messageOwner.action.title);
} else if (messageObject.messageOwner.action instanceof TLRPC.TL_messageActionChatEditPhoto || messageObject.messageOwner.action instanceof TLRPC.TL_messageActionChatDeletePhoto) {
@ -248,7 +261,7 @@ public class NotificationsController {
msg = LocaleController.formatString("NotificationMessageGroupVideo", R.string.NotificationMessageGroupVideo, ContactsController.formatName(user.first_name, user.last_name), chat.title);
} else if (messageObject.messageOwner.media instanceof TLRPC.TL_messageMediaContact) {
msg = LocaleController.formatString("NotificationMessageGroupContact", R.string.NotificationMessageGroupContact, ContactsController.formatName(user.first_name, user.last_name), chat.title);
} else if (messageObject.messageOwner.media instanceof TLRPC.TL_messageMediaGeo) {
} else if (messageObject.messageOwner.media instanceof TLRPC.TL_messageMediaGeo || messageObject.messageOwner.media instanceof TLRPC.TL_messageMediaVenue) {
msg = LocaleController.formatString("NotificationMessageGroupMap", R.string.NotificationMessageGroupMap, ContactsController.formatName(user.first_name, user.last_name), chat.title);
} else if (messageObject.messageOwner.media instanceof TLRPC.TL_messageMediaDocument) {
if (messageObject.isSticker()) {
@ -286,14 +299,14 @@ public class NotificationsController {
private void scheduleNotificationDelay(boolean onlineReason) {
try {
FileLog.e("tmessages", "delay notification start");
FileLog.e("tmessages", "delay notification start, onlineReason = " + onlineReason);
AlarmManager alarm = (AlarmManager) ApplicationLoader.applicationContext.getSystemService(Context.ALARM_SERVICE);
PendingIntent pintent = PendingIntent.getService(ApplicationLoader.applicationContext, 0, new Intent(ApplicationLoader.applicationContext, NotificationDelay.class), 0);
SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("Notifications", Activity.MODE_PRIVATE);
if (onlineReason) {
alarm.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + 3 * 1000, pintent);
} else {
alarm.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + 500, pintent);
alarm.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + 1000, pintent);
}
} catch (Exception e) {
FileLog.e("tmessages", e);
@ -302,7 +315,10 @@ public class NotificationsController {
protected void notificationDelayReached() {
FileLog.e("tmessages", "delay reached");
showOrUpdateNotification(true);
if (!delayedPushMessages.isEmpty()) {
showOrUpdateNotification(true);
delayedPushMessages.clear();
}
}
protected void repeatNotificationMaybe() {
@ -319,6 +335,7 @@ public class NotificationsController {
AndroidUtilities.runOnUIThread(new Runnable() {
@Override
public void run() {
FileLog.e("tmessages", "set last online from other device = " + time);
lastOnlineFromOtherDevice = time;
}
});
@ -364,29 +381,47 @@ public class NotificationsController {
boolean inAppPreview = false;
boolean inAppPriority = false;
int priority = 0;
int priority_override = 0;
int vibrate_override = 0;
int priorityOverride = 0;
int vibrateOverride = 0;
SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("Notifications", Context.MODE_PRIVATE);
int notify_override = preferences.getInt("notify2_" + override_dialog_id, 0);
if (notify_override == 3) {
int mute_until = preferences.getInt("notifyuntil_" + override_dialog_id, 0);
if (mute_until >= ConnectionsManager.getInstance().getCurrentTime()) {
notify_override = 2;
}
}
if (!notifyAboutLast || notify_override == 2 || (!preferences.getBoolean("EnableAll", true) || chat_id != 0 && !preferences.getBoolean("EnableGroup", true)) && notify_override == 0) {
int notifyOverride = getNotifyOverride(preferences, override_dialog_id);
if (!notifyAboutLast || notifyOverride == 2 || (!preferences.getBoolean("EnableAll", true) || chat_id != 0 && !preferences.getBoolean("EnableGroup", true)) && notifyOverride == 0) {
notifyDisabled = true;
}
if (!notifyDisabled && dialog_id == override_dialog_id && chat != null) {
int notifyMaxCount = preferences.getInt("smart_max_count_" + dialog_id, 2);
int notifyDelay = preferences.getInt("smart_delay_" + dialog_id, 3 * 60);
if (notifyMaxCount != 0) {
Point dialogInfo = smartNotificationsDialogs.get(dialog_id);
if (dialogInfo == null) {
dialogInfo = new Point(1, (int) (System.currentTimeMillis() / 1000));
smartNotificationsDialogs.put(dialog_id, dialogInfo);
} else {
int lastTime = dialogInfo.y;
if (lastTime + notifyDelay < System.currentTimeMillis() / 1000) {
dialogInfo.set(1, (int) (System.currentTimeMillis() / 1000));
} else {
int count = dialogInfo.x;
if (count < notifyMaxCount) {
dialogInfo.set(count + 1, (int) (System.currentTimeMillis() / 1000));
} else {
notifyDisabled = true;
}
}
}
}
}
String defaultPath = Settings.System.DEFAULT_NOTIFICATION_URI.getPath();
if (!notifyDisabled) {
inAppSounds = preferences.getBoolean("EnableInAppSounds", true);
inAppVibrate = preferences.getBoolean("EnableInAppVibrate", true);
inAppPreview = preferences.getBoolean("EnableInAppPreview", true);
inAppPriority = preferences.getBoolean("EnableInAppPriority", false);
vibrate_override = preferences.getInt("vibrate_" + dialog_id, 0);
priority_override = preferences.getInt("priority_" + dialog_id, 3);
vibrateOverride = preferences.getInt("vibrate_" + dialog_id, 0);
priorityOverride = preferences.getInt("priority_" + dialog_id, 3);
boolean vibrateOnlyIfSilent = false;
choosenSoundPath = preferences.getString("sound_path_" + dialog_id, null);
@ -413,16 +448,16 @@ public class NotificationsController {
ledColor = preferences.getInt("color_" + dialog_id, 0);
}
if (priority_override != 3) {
priority = priority_override;
if (priorityOverride != 3) {
priority = priorityOverride;
}
if (needVibrate == 4) {
vibrateOnlyIfSilent = true;
needVibrate = 0;
}
if (needVibrate == 2 && (vibrate_override == 1 || vibrate_override == 3 || vibrate_override == 5) || needVibrate != 2 && vibrate_override == 2 || vibrate_override != 0) {
needVibrate = vibrate_override;
if (needVibrate == 2 && (vibrateOverride == 1 || vibrateOverride == 3 || vibrateOverride == 5) || needVibrate != 2 && vibrateOverride == 2 || vibrateOverride != 0) {
needVibrate = vibrateOverride;
}
if (!ApplicationLoader.mainInterfacePaused) {
if (!inAppSounds) {
@ -524,9 +559,6 @@ public class NotificationsController {
if (chat == null && user != null && user.phone != null && user.phone.length() > 0) {
mBuilder.addPerson("tel:+" + user.phone);
}
/*Bundle bundle = new Bundle();
bundle.putString(NotificationCompat.EXTRA_PEOPLE, );
mBuilder.setExtras()*/
String lastMessage = null;
String lastMessageFull = null;
@ -558,7 +590,6 @@ public class NotificationsController {
}
if (i == 0) {
lastMessageFull = message;
//lastMessage = getStringForMessage(pushMessages.get(i), true);
lastMessage = lastMessageFull;
}
if (pushDialogs.size() == 1) {
@ -790,6 +821,7 @@ public class NotificationsController {
}
popupMessages.remove(messageObject);
pushMessagesDict.remove(messageObject.getId());
delayedPushMessages.remove(messageObject);
pushMessages.remove(a);
a--;
}
@ -821,6 +853,7 @@ public class NotificationsController {
personal_count--;
}
pushMessages.remove(a);
delayedPushMessages.remove(messageObject);
popupMessages.remove(messageObject);
pushMessagesDict.remove(messageObject.getId());
a--;
@ -847,14 +880,8 @@ public class NotificationsController {
try {
SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("Notifications", Context.MODE_PRIVATE);
int notify_override = preferences.getInt("notify2_" + openned_dialog_id, 0);
if (notify_override == 3) {
int mute_until = preferences.getInt("notifyuntil_" + openned_dialog_id, 0);
if (mute_until >= ConnectionsManager.getInstance().getCurrentTime()) {
notify_override = 2;
}
}
if (notify_override == 2) {
int notifyOverride = getNotifyOverride(preferences, openned_dialog_id);
if (notifyOverride == 2) {
return;
}
notificationsQueue.postRunnable(new Runnable() {
@ -864,7 +891,14 @@ public class NotificationsController {
return;
}
try {
if (mediaPlayerIn == null) {
if (soundPool == null) {
soundPool = new SoundPool(4, AudioManager.STREAM_SYSTEM, 0);
}
if (soundIn == 0) {
soundIn = soundPool.load(ApplicationLoader.applicationContext, R.raw.sound_in, 1);
}
soundPool.play(soundIn, 1.0f, 1.0f, 1, 0, 1.0f);
/*if (mediaPlayerIn == null) {
AssetFileDescriptor assetFileDescriptor = ApplicationLoader.applicationContext.getResources().openRawResourceFd(R.raw.sound_in);
if (assetFileDescriptor != null) {
mediaPlayerIn = new MediaPlayer();
@ -875,45 +909,18 @@ public class NotificationsController {
mediaPlayerIn.prepare();
}
}
mediaPlayerIn.start();
try {
mediaPlayerIn.pause();
mediaPlayerIn.seekTo(0);
} catch (Exception e) {
FileLog.e("tmessages", e);
}
mediaPlayerIn.start();*/
} catch (Exception e) {
FileLog.e("tmessages", e);
}
}
});
/*String choosenSoundPath = null;
String defaultPath = Settings.System.DEFAULT_NOTIFICATION_URI.getPath();
choosenSoundPath = preferences.getString("sound_path_" + openned_dialog_id, null);
boolean isChat = (int)(openned_dialog_id) < 0;
if (isChat) {
if (choosenSoundPath != null && choosenSoundPath.equals(defaultPath)) {
choosenSoundPath = null;
} else if (choosenSoundPath == null) {
choosenSoundPath = preferences.getString("GroupSoundPath", defaultPath);
}
} else {
if (choosenSoundPath != null && choosenSoundPath.equals(defaultPath)) {
choosenSoundPath = null;
} else if (choosenSoundPath == null) {
choosenSoundPath = preferences.getString("GlobalSoundPath", defaultPath);
}
}
if (choosenSoundPath != null && !choosenSoundPath.equals("NoSound")) {
if (lastMediaPlayerUri == null || !choosenSoundPath.equals(lastMediaPlayerUri)) {
lastMediaPlayerUri = choosenSoundPath;
mediaPlayer.reset();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_NOTIFICATION);
if (choosenSoundPath.equals(defaultPath)) {
mediaPlayer.setDataSource(ApplicationLoader.applicationContext, Settings.System.DEFAULT_NOTIFICATION_URI);
} else {
mediaPlayer.setDataSource(ApplicationLoader.applicationContext, Uri.parse(choosenSoundPath));
}
mediaPlayer.prepare();
}
mediaPlayer.start();
}*/
} catch (Exception e) {
FileLog.e("tmessages", e);
}
@ -934,7 +941,14 @@ public class NotificationsController {
@Override
public void run() {
try {
if (mediaPlayerOut == null) {
if (soundPool == null) {
soundPool = new SoundPool(4, AudioManager.STREAM_SYSTEM, 0);
}
if (soundOut == 0) {
soundOut = soundPool.load(ApplicationLoader.applicationContext, R.raw.sound_out, 1);
}
soundPool.play(soundOut, 1.0f, 1.0f, 1, 0, 1.0f);
/*if (mediaPlayerOut == null) {
AssetFileDescriptor assetFileDescriptor = ApplicationLoader.applicationContext.getResources().openRawResourceFd(R.raw.sound_out);
if (assetFileDescriptor != null) {
mediaPlayerOut = new MediaPlayer();
@ -945,7 +959,13 @@ public class NotificationsController {
mediaPlayerOut.prepare();
}
}
mediaPlayerOut.start();
try {
mediaPlayerOut.pause();
mediaPlayerOut.seekTo(0);
} catch (Exception e) {
FileLog.e("tmessages", e);
}
mediaPlayerOut.start();*/
} catch (Exception e) {
FileLog.e("tmessages", e);
}
@ -953,6 +973,17 @@ public class NotificationsController {
});
}
private int getNotifyOverride(SharedPreferences preferences, long dialog_id) {
int notifyOverride = preferences.getInt("notify2_" + dialog_id, 0);
if (notifyOverride == 3) {
int muteUntil = preferences.getInt("notifyuntil_" + dialog_id, 0);
if (muteUntil >= ConnectionsManager.getInstance().getCurrentTime()) {
notifyOverride = 2;
}
}
return notifyOverride;
}
public void processNewMessages(ArrayList<MessageObject> messageObjects, boolean isLast) {
if (messageObjects.isEmpty()) {
return;
@ -986,20 +1017,15 @@ public class NotificationsController {
boolean isChat = (int)dialog_id < 0;
popup = (int)dialog_id == 0 ? 0 : preferences.getInt(isChat ? "popupGroup" : "popupAll", 0);
if (value == null) {
int notify_override = preferences.getInt("notify2_" + dialog_id, 0);
if (notify_override == 3) {
int mute_until = preferences.getInt("notifyuntil_" + dialog_id, 0);
if (mute_until >= ConnectionsManager.getInstance().getCurrentTime()) {
notify_override = 2;
}
}
value = !(notify_override == 2 || (!preferences.getBoolean("EnableAll", true) || isChat && !preferences.getBoolean("EnableGroup", true)) && notify_override == 0);
int notifyOverride = getNotifyOverride(preferences, dialog_id);
value = !(notifyOverride == 2 || (!preferences.getBoolean("EnableAll", true) || isChat && !preferences.getBoolean("EnableGroup", true)) && notifyOverride == 0);
settingsCache.put(dialog_id, value);
}
if (value) {
if (popup != 0) {
popupMessages.add(0, messageObject);
}
delayedPushMessages.add(messageObject);
pushMessages.add(0, messageObject);
pushMessagesDict.put(messageObject.getId(), messageObject);
if (original_dialog_id != dialog_id) {
@ -1030,24 +1056,22 @@ public class NotificationsController {
for (HashMap.Entry<Long, Integer> entry : dialogsToUpdate.entrySet()) {
long dialog_id = entry.getKey();
int notify_override = preferences.getInt("notify2_" + dialog_id, 0);
if (notify_override == 3) {
int mute_until = preferences.getInt("notifyuntil_" + dialog_id, 0);
if (mute_until >= ConnectionsManager.getInstance().getCurrentTime()) {
notify_override = 2;
}
}
int notifyOverride = getNotifyOverride(preferences, dialog_id);
if (notifyCheck) {
Integer override = pushDialogsOverrideMention.get(dialog_id);
if (override != null && override == 1) {
pushDialogsOverrideMention.put(dialog_id, 0);
notify_override = 1;
notifyOverride = 1;
}
}
boolean canAddValue = !(notify_override == 2 || (!preferences.getBoolean("EnableAll", true) || ((int)dialog_id < 0) && !preferences.getBoolean("EnableGroup", true)) && notify_override == 0);
boolean canAddValue = !(notifyOverride == 2 || (!preferences.getBoolean("EnableAll", true) || ((int)dialog_id < 0) && !preferences.getBoolean("EnableGroup", true)) && notifyOverride == 0);
Integer currentCount = pushDialogs.get(dialog_id);
Integer newCount = entry.getValue();
if (newCount == 0) {
smartNotificationsDialogs.remove(dialog_id);
}
if (newCount < 0) {
if (currentCount == null) {
continue;
@ -1070,6 +1094,7 @@ public class NotificationsController {
}
pushMessages.remove(a);
a--;
delayedPushMessages.remove(messageObject);
pushMessagesDict.remove(messageObject.getId());
popupMessages.remove(messageObject);
}
@ -1079,17 +1104,18 @@ public class NotificationsController {
pushDialogs.put(dialog_id, newCount);
}
}
/*if (old_unread_count != total_unread_count) { TODO
if (lastOnlineFromOtherDevice > ConnectionsManager.getInstance().getCurrentTime()) {
showOrUpdateNotification(false);
scheduleNotificationDelay(true);
} else {
showOrUpdateNotification(notifyCheck);
}
}*/
if (old_unread_count != total_unread_count) {
showOrUpdateNotification(notifyCheck);
if (!notifyCheck) {
delayedPushMessages.clear();
showOrUpdateNotification(notifyCheck);
} else {
showOrUpdateNotification(false);
scheduleNotificationDelay(lastOnlineFromOtherDevice > ConnectionsManager.getInstance().getCurrentTime());
}
}
/*if (old_unread_count != total_unread_count) {
showOrUpdateNotification(notifyCheck);
}*/
notifyCheck = false;
if (preferences.getBoolean("badgeNumber", true)) {
setBadge(ApplicationLoader.applicationContext, total_unread_count);
@ -1125,14 +1151,8 @@ public class NotificationsController {
}
Boolean value = settingsCache.get(dialog_id);
if (value == null) {
int notify_override = preferences.getInt("notify2_" + dialog_id, 0);
if (notify_override == 3) {
int mute_until = preferences.getInt("notifyuntil_" + dialog_id, 0);
if (mute_until >= ConnectionsManager.getInstance().getCurrentTime()) {
notify_override = 2;
}
}
value = !(notify_override == 2 || (!preferences.getBoolean("EnableAll", true) || ((int) dialog_id < 0) && !preferences.getBoolean("EnableGroup", true)) && notify_override == 0);
int notifyOverride = getNotifyOverride(preferences, dialog_id);
value = !(notifyOverride == 2 || (!preferences.getBoolean("EnableAll", true) || ((int) dialog_id < 0) && !preferences.getBoolean("EnableGroup", true)) && notifyOverride == 0);
settingsCache.put(dialog_id, value);
}
if (!value || dialog_id == openned_dialog_id && ApplicationLoader.isScreenOn) {
@ -1149,19 +1169,13 @@ public class NotificationsController {
long dialog_id = entry.getKey();
Boolean value = settingsCache.get(dialog_id);
if (value == null) {
int notify_override = preferences.getInt("notify2_" + dialog_id, 0);
if (notify_override == 3) {
int mute_until = preferences.getInt("notifyuntil_" + dialog_id, 0);
if (mute_until >= ConnectionsManager.getInstance().getCurrentTime()) {
notify_override = 2;
}
}
int notifyOverride = getNotifyOverride(preferences, dialog_id);
Integer override = pushDialogsOverrideMention.get(dialog_id);
if (override != null && override == 1) {
pushDialogsOverrideMention.put(dialog_id, 0);
notify_override = 1;
notifyOverride = 1;
}
value = !(notify_override == 2 || (!preferences.getBoolean("EnableAll", true) || ((int) dialog_id < 0) && !preferences.getBoolean("EnableGroup", true)) && notify_override == 0);
value = !(notifyOverride == 2 || (!preferences.getBoolean("EnableAll", true) || ((int) dialog_id < 0) && !preferences.getBoolean("EnableGroup", true)) && notifyOverride == 0);
settingsCache.put(dialog_id, value);
}
if (!value) {
@ -1190,6 +1204,10 @@ public class NotificationsController {
notificationsQueue.postRunnable(new Runnable() {
@Override
public void run() {
if (lastBadgeCount == count) {
return;
}
lastBadgeCount = count;
try {
ContentValues cv = new ContentValues();
cv.put("tag", "org.telegram.messenger/org.telegram.ui.LaunchActivity");

View File

@ -542,7 +542,7 @@ public class SecretChatHelper {
File cacheFile = new File(FileLoader.getInstance().getDirectory(FileLoader.MEDIA_DIR_CACHE), fileName + ".jpg");
File cacheFile2 = FileLoader.getPathToAttach(size);
cacheFile.renameTo(cacheFile2);
ImageLoader.getInstance().replaceImageInCache(fileName, fileName2);
ImageLoader.getInstance().replaceImageInCache(fileName, fileName2, size.location);
ArrayList<TLRPC.Message> arr = new ArrayList<>();
arr.add(newMsg);
MessagesStorage.getInstance().putMessages(arr, false, true, false, 0);
@ -557,7 +557,7 @@ public class SecretChatHelper {
newMsg.media.video.w = video.w;
newMsg.media.video.h = video.h;
newMsg.media.video.date = video.date;
newMsg.media.video.caption = "";
newMsg.media.caption = video.caption != null ? video.caption : "";
newMsg.media.video.user_id = video.user_id;
newMsg.media.video.size = file.size;
newMsg.media.video.id = file.id;
@ -565,6 +565,7 @@ public class SecretChatHelper {
newMsg.media.video.key = decryptedMessage.media.key;
newMsg.media.video.iv = decryptedMessage.media.iv;
newMsg.media.video.mime_type = video.mime_type;
newMsg.media.video.caption = video.caption != null ? video.caption : "";
if (newMsg.attachPath != null && newMsg.attachPath.startsWith(FileLoader.getInstance().getDirectory(FileLoader.MEDIA_DIR_CACHE).getAbsolutePath())) {
File cacheFile = new File(newMsg.attachPath);
@ -893,10 +894,10 @@ public class SecretChatHelper {
return null;
}
newMessage.media = new TLRPC.TL_messageMediaPhoto();
newMessage.media.caption = "";
newMessage.media.photo = new TLRPC.TL_photo();
newMessage.media.photo.user_id = newMessage.from_id;
newMessage.media.photo.date = newMessage.date;
newMessage.media.photo.caption = "";
newMessage.media.photo.geo = new TLRPC.TL_geoPointEmpty();
if (decryptedMessage.media.thumb.length != 0 && decryptedMessage.media.thumb.length <= 6000 && decryptedMessage.media.thumb_w <= 100 && decryptedMessage.media.thumb_h <= 100) {
TLRPC.TL_photoCachedSize small = new TLRPC.TL_photoCachedSize();
@ -926,6 +927,7 @@ public class SecretChatHelper {
return null;
}
newMessage.media = new TLRPC.TL_messageMediaVideo();
newMessage.media.caption = "";
newMessage.media.video = new TLRPC.TL_videoEncrypted();
if (decryptedMessage.media.thumb.length != 0 && decryptedMessage.media.thumb.length <= 6000 && decryptedMessage.media.thumb_w <= 100 && decryptedMessage.media.thumb_h <= 100) {
newMessage.media.video.thumb = new TLRPC.TL_photoCachedSize();
@ -943,7 +945,6 @@ public class SecretChatHelper {
newMessage.media.video.w = decryptedMessage.media.w;
newMessage.media.video.h = decryptedMessage.media.h;
newMessage.media.video.date = date;
newMessage.media.video.caption = "";
newMessage.media.video.user_id = from_id;
newMessage.media.video.size = file.size;
newMessage.media.video.id = file.id;
@ -951,6 +952,7 @@ public class SecretChatHelper {
newMessage.media.video.key = decryptedMessage.media.key;
newMessage.media.video.iv = decryptedMessage.media.iv;
newMessage.media.video.mime_type = decryptedMessage.media.mime_type;
newMessage.media.video.caption = "";
if (newMessage.ttl != 0) {
newMessage.ttl = Math.max(newMessage.media.video.duration + 1, newMessage.ttl);
}
@ -1300,7 +1302,7 @@ public class SecretChatHelper {
ByteBufferDesc is = BuffersStorage.getInstance().getFreeBuffer(message.bytes.length);
is.writeRaw(message.bytes);
is.position(0);
long fingerprint = is.readInt64();
long fingerprint = is.readInt64(false);
byte[] keyToDecrypt = null;
boolean new_key_used = false;
if (chat.key_fingerprint == fingerprint) {
@ -1311,12 +1313,12 @@ public class SecretChatHelper {
}
if (keyToDecrypt != null) {
byte[] messageKey = is.readData(16);
byte[] messageKey = is.readData(16, false);
MessageKeyData keyData = Utilities.generateMessageKeyData(keyToDecrypt, messageKey, false);
Utilities.aesIgeEncryption(is.buffer, keyData.aesKey, keyData.aesIv, false, false, 24, is.limit() - 24);
int len = is.readInt32();
int len = is.readInt32(false);
if (len < 0 || len > is.limit() - 28) {
return null;
}
@ -1325,7 +1327,13 @@ public class SecretChatHelper {
return null;
}
TLObject object = TLClassStore.Instance().TLdeserialize(is, is.readInt32());
TLObject object = null;
try {
object = TLClassStore.Instance().TLdeserialize(is, is.readInt32(true), true);
} catch (Exception e) {
FileLog.e("tmessages", e);
}
BuffersStorage.getInstance().reuseFreeBuffer(is);
if (!new_key_used && AndroidUtilities.getPeerLayerVersion(chat.layer) >= 20) {
chat.key_use_count_in++;

View File

@ -44,7 +44,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter
private HashMap<Integer, MessageObject> unsentMessages = new HashMap<>();
private HashMap<Integer, TLRPC.Message> sendingMessages = new HashMap<>();
private class DelayedMessage {
protected class DelayedMessage {
public TLObject sendRequest;
public TLRPC.TL_decryptedMessage sendEncryptedRequest;
public int type;
@ -458,8 +458,8 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter
sendMessage(video, null, messageObject.messageOwner.attachPath, did, messageObject.replyMessageObject);
} else if (messageObject.messageOwner.media.document instanceof TLRPC.TL_document) {
sendMessage((TLRPC.TL_document) messageObject.messageOwner.media.document, null, messageObject.messageOwner.attachPath, did, messageObject.replyMessageObject);
} else if (messageObject.messageOwner.media.geo instanceof TLRPC.TL_geoPoint) {
sendMessage(messageObject.messageOwner.media.geo.lat, messageObject.messageOwner.media.geo._long, did, messageObject.replyMessageObject);
} else if (messageObject.messageOwner.media instanceof TLRPC.TL_messageMediaVenue || messageObject.messageOwner.media instanceof TLRPC.TL_messageMediaGeo) {
sendMessage(messageObject.messageOwner.media, did, messageObject.replyMessageObject);
} else if (messageObject.messageOwner.media.phone_number != null) {
TLRPC.User user = new TLRPC.TL_userContact();
user.phone = messageObject.messageOwner.media.phone_number;
@ -481,8 +481,53 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter
}
}
public void sendSticker(TLRPC.Document document, long peer, MessageObject replyingMessageObject) {
if (document == null) {
return;
}
if (((int) peer) == 0 && document.thumb instanceof TLRPC.TL_photoSize) {
File file = FileLoader.getPathToAttach(document.thumb, true);
if (file.exists()) {
try {
int len = (int) file.length();
byte[] arr = new byte[(int) file.length()];
RandomAccessFile reader = new RandomAccessFile(file, "r");
reader.readFully(arr);
TLRPC.TL_document newDocument = new TLRPC.TL_document();
newDocument.thumb = new TLRPC.TL_photoCachedSize();
newDocument.thumb.location = document.thumb.location;
newDocument.thumb.size = document.thumb.size;
newDocument.thumb.w = document.thumb.w;
newDocument.thumb.h = document.thumb.h;
newDocument.thumb.type = document.thumb.type;
newDocument.thumb.bytes = arr;
newDocument.id = document.id;
newDocument.access_hash = document.access_hash;
newDocument.date = document.date;
newDocument.mime_type = document.mime_type;
newDocument.size = document.size;
newDocument.dc_id = document.dc_id;
newDocument.attributes = document.attributes;
document = newDocument;
} catch (Exception e) {
FileLog.e("tmessages", e);
}
}
}
for (int a = 0; a < document.attributes.size(); a++) {
TLRPC.DocumentAttribute attribute = document.attributes.get(a);
if (attribute instanceof TLRPC.TL_documentAttributeSticker) {
document.attributes.remove(a);
document.attributes.add(new TLRPC.TL_documentAttributeSticker_old());
break;
}
}
SendMessagesHelper.getInstance().sendMessage((TLRPC.TL_document) document, null, null, peer, replyingMessageObject);
}
public void sendMessage(TLRPC.User user, long peer, MessageObject reply_to_msg) {
sendMessage(null, null, null, null, null, null, user, null, null, null, peer, false, null, reply_to_msg, null, true);
sendMessage(null, null, null, null, null, user, null, null, null, peer, false, null, reply_to_msg, null, true);
}
public void sendMessage(ArrayList<MessageObject> messages, long peer) {
@ -550,6 +595,9 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter
ids.add(newMsg.fwd_msg_id);
newMsg.date = ConnectionsManager.getInstance().getCurrentTime();
newMsg.flags |= TLRPC.MESSAGE_FLAG_UNREAD;
if (newMsg.media instanceof TLRPC.TL_messageMediaAudio) {
newMsg.flags |= TLRPC.MESSAGE_FLAG_CONTENT_UNREAD;
}
newMsg.dialog_id = peer;
newMsg.to_id = to_id;
MessageObject newMsgObj = new MessageObject(newMsg, null, true);
@ -560,7 +608,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter
putToSendingMessages(newMsg);
if (arr.size() == 100 || a == messages.size() - 1) {
MessagesStorage.getInstance().putMessages(arr, false, true, false, 0);
MessagesStorage.getInstance().putMessages(new ArrayList<>(arr), false, true, false, 0);
MessagesController.getInstance().updateInterfaceWithMessages(peer, objArr);
NotificationCenter.getInstance().postNotificationName(NotificationCenter.dialogsNeedReload);
UserConfig.saveConfig(false);
@ -655,38 +703,38 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter
}
public void sendMessage(MessageObject message) {
sendMessage(null, null, null, null, null, message, null, null, null, null, message.getDialogId(), true, message.messageOwner.attachPath, null, null, true);
sendMessage(null, null, null, null, message, null, null, null, null, message.getDialogId(), true, message.messageOwner.attachPath, null, null, true);
}
public void sendMessage(MessageObject message, long peer) {
sendMessage(null, null, null, null, null, message, null, null, null, null, peer, false, message.messageOwner.attachPath, null, null, true);
sendMessage(null, null, null, null, message, null, null, null, null, peer, false, message.messageOwner.attachPath, null, null, true);
}
public void sendMessage(TLRPC.TL_document document, String originalPath, String path, long peer, MessageObject reply_to_msg) {
sendMessage(null, null, null, null, null, null, null, document, null, originalPath, peer, false, path, reply_to_msg, null, true);
sendMessage(null, null, null, null, null, null, document, null, originalPath, peer, false, path, reply_to_msg, null, true);
}
public void sendMessage(String message, long peer, MessageObject reply_to_msg, TLRPC.WebPage webPage, boolean searchLinks) {
sendMessage(message, null, null, null, null, null, null, null, null, null, peer, false, null, reply_to_msg, webPage, searchLinks);
sendMessage(message, null, null, null, null, null, null, null, null, peer, false, null, reply_to_msg, webPage, searchLinks);
}
public void sendMessage(double lat, double lon, long peer, MessageObject reply_to_msg) {
sendMessage(null, lat, lon, null, null, null, null, null, null, null, peer, false, null, reply_to_msg, null, true);
public void sendMessage(TLRPC.MessageMedia location, long peer, MessageObject reply_to_msg) {
sendMessage(null, location, null, null, null, null, null, null, null, peer, false, null, reply_to_msg, null, true);
}
public void sendMessage(TLRPC.TL_photo photo, String originalPath, String path, long peer, MessageObject reply_to_msg) {
sendMessage(null, null, null, photo, null, null, null, null, null, originalPath, peer, false, path, reply_to_msg, null, true);
sendMessage(null, null, photo, null, null, null, null, null, originalPath, peer, false, path, reply_to_msg, null, true);
}
public void sendMessage(TLRPC.TL_video video, String originalPath, String path, long peer, MessageObject reply_to_msg) {
sendMessage(null, null, null, null, video, null, null, null, null, originalPath, peer, false, path, reply_to_msg, null, true);
sendMessage(null, null, null, video, null, null, null, null, originalPath, peer, false, path, reply_to_msg, null, true);
}
public void sendMessage(TLRPC.TL_audio audio, String path, long peer, MessageObject reply_to_msg) {
sendMessage(null, null, null, null, null, null, null, null, audio, null, peer, false, path, reply_to_msg, null, true);
sendMessage(null, null, null, null, null, null, null, audio, null, peer, false, path, reply_to_msg, null, true);
}
private void sendMessage(String message, Double lat, Double lon, TLRPC.TL_photo photo, TLRPC.TL_video video, MessageObject msgObj, TLRPC.User user, TLRPC.TL_document document, TLRPC.TL_audio audio, String originalPath, long peer, boolean retry, String path, MessageObject reply_to_msg, TLRPC.WebPage webPage, boolean searchLinks) {
private void sendMessage(String message, TLRPC.MessageMedia location, TLRPC.TL_photo photo, TLRPC.TL_video video, MessageObject msgObj, TLRPC.User user, TLRPC.TL_document document, TLRPC.TL_audio audio, String originalPath, long peer, boolean retry, String path, MessageObject reply_to_msg, TLRPC.WebPage webPage, boolean searchLinks) {
if (peer == 0) {
return;
}
@ -713,8 +761,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter
type = 0;
}
} else if (msgObj.type == 4) {
lat = newMsg.media.geo.lat;
lon = newMsg.media.geo._long;
location = newMsg.media;
type = 1;
} else if (msgObj.type == 1) {
if (msgObj.isForwarded()) {
@ -760,16 +807,13 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter
}
type = 0;
newMsg.message = message;
} else if (lat != null && lon != null) {
} else if (location != null) {
if (encryptedChat != null && AndroidUtilities.getPeerLayerVersion(encryptedChat.layer) >= 17) {
newMsg = new TLRPC.TL_message_secret();
} else {
newMsg = new TLRPC.TL_message();
}
newMsg.media = new TLRPC.TL_messageMediaGeo();
newMsg.media.geo = new TLRPC.TL_geoPoint();
newMsg.media.geo.lat = lat;
newMsg.media.geo._long = lon;
newMsg.media = location;
newMsg.message = "";
type = 1;
} else if (photo != null) {
@ -779,6 +823,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter
newMsg = new TLRPC.TL_message();
}
newMsg.media = new TLRPC.TL_messageMediaPhoto();
newMsg.media.caption = photo.caption != null ? photo.caption : "";
newMsg.media.photo = photo;
type = 2;
newMsg.message = "-1";
@ -795,6 +840,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter
newMsg = new TLRPC.TL_message();
}
newMsg.media = new TLRPC.TL_messageMediaVideo();
newMsg.media.caption = video.caption != null ? video.caption : "";
newMsg.media.video = video;
newMsg.videoEditedInfo = video.videoEditedInfo;
type = 3;
@ -868,6 +914,9 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter
}
newMsg.date = ConnectionsManager.getInstance().getCurrentTime();
newMsg.flags |= TLRPC.MESSAGE_FLAG_UNREAD;
if (encryptedChat == null && high_id != 1 && newMsg.media instanceof TLRPC.TL_messageMediaAudio) {
newMsg.flags |= TLRPC.MESSAGE_FLAG_CONTENT_UNREAD;
}
newMsg.dialog_id = peer;
if (reply_to_msg != null) {
newMsg.flags |= TLRPC.MESSAGE_FLAG_REPLY;
@ -993,13 +1042,22 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter
TLRPC.InputMedia inputMedia = null;
DelayedMessage delayedMessage = null;
if (type == 1) {
inputMedia = new TLRPC.TL_inputMediaGeoPoint();
if (location instanceof TLRPC.TL_messageMediaVenue) {
inputMedia = new TLRPC.TL_inputMediaVenue();
inputMedia.address = location.address;
inputMedia.title = location.title;
inputMedia.provider = location.provider;
inputMedia.venue_id = location.venue_id;
} else {
inputMedia = new TLRPC.TL_inputMediaGeoPoint();
}
inputMedia.geo_point = new TLRPC.TL_inputGeoPoint();
inputMedia.geo_point.lat = lat;
inputMedia.geo_point._long = lon;
inputMedia.geo_point.lat = location.geo.lat;
inputMedia.geo_point._long = location.geo._long;
} else if (type == 2) {
if (photo.access_hash == 0) {
inputMedia = new TLRPC.TL_inputMediaUploadedPhoto();
inputMedia.caption = photo.caption != null ? photo.caption : "";
delayedMessage = new DelayedMessage();
delayedMessage.originalPath = originalPath;
delayedMessage.type = 0;
@ -1012,6 +1070,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter
} else {
TLRPC.TL_inputMediaPhoto media = new TLRPC.TL_inputMediaPhoto();
media.id = new TLRPC.TL_inputPhoto();
media.caption = photo.caption != null ? photo.caption : "";
media.id.id = photo.id;
media.id.access_hash = photo.access_hash;
inputMedia = media;
@ -1023,6 +1082,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter
} else {
inputMedia = new TLRPC.TL_inputMediaUploadedVideo();
}
inputMedia.caption = video.caption != null ? video.caption : "";
inputMedia.duration = video.duration;
inputMedia.w = video.w;
inputMedia.h = video.h;
@ -1036,6 +1096,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter
} else {
TLRPC.TL_inputMediaVideo media = new TLRPC.TL_inputMediaVideo();
media.id = new TLRPC.TL_inputVideo();
media.caption = video.caption != null ? video.caption : "";
media.id.id = video.id;
media.id.access_hash = video.access_hash;
inputMedia = media;
@ -1161,8 +1222,8 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter
reqSend.message = "";
if (type == 1) {
reqSend.media = new TLRPC.TL_decryptedMessageMediaGeoPoint();
reqSend.media.lat = lat;
reqSend.media._long = lon;
reqSend.media.lat = location.geo.lat;
reqSend.media._long = location.geo._long;
SecretChatHelper.getInstance().performSendEncryptedRequest(reqSend, newMsgObj.messageOwner, encryptedChat, null, null);
} else if (type == 2) {
TLRPC.PhotoSize small = photo.sizes.get(0);
@ -1589,7 +1650,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter
cacheFile2 = new File(FileLoader.getInstance().getDirectory(FileLoader.MEDIA_DIR_CACHE), fileName2 + ".jpg");
}
cacheFile.renameTo(cacheFile2);
ImageLoader.getInstance().replaceImageInCache(fileName, fileName2);
ImageLoader.getInstance().replaceImageInCache(fileName, fileName2, size.location);
size2.location = size.location;
break;
}
@ -1611,7 +1672,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter
File cacheFile = new File(FileLoader.getInstance().getDirectory(FileLoader.MEDIA_DIR_CACHE), fileName + ".jpg");
File cacheFile2 = new File(FileLoader.getInstance().getDirectory(FileLoader.MEDIA_DIR_CACHE), fileName2 + ".jpg");
cacheFile.renameTo(cacheFile2);
ImageLoader.getInstance().replaceImageInCache(fileName, fileName2);
ImageLoader.getInstance().replaceImageInCache(fileName, fileName2, size.location);
size2.location = size.location;
}
}
@ -1642,7 +1703,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter
File cacheFile = new File(FileLoader.getInstance().getDirectory(FileLoader.MEDIA_DIR_CACHE), fileName + ".jpg");
File cacheFile2 = new File(FileLoader.getInstance().getDirectory(FileLoader.MEDIA_DIR_CACHE), fileName2 + ".jpg");
cacheFile.renameTo(cacheFile2);
ImageLoader.getInstance().replaceImageInCache(fileName, fileName2);
ImageLoader.getInstance().replaceImageInCache(fileName, fileName2, size.location);
size2.location = size.location;
}
} else if (MessageObject.isStickerMessage(sentMessage) && size2.location != null) {
@ -1697,6 +1758,10 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter
arrayList.add(message);
}
protected ArrayList<DelayedMessage> getDelayedMessages(String location) {
return delayedMessages.get(location);
}
protected long getNextRandomId() {
long val = 0;
while (val == 0) {
@ -1749,7 +1814,6 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter
photo.user_id = UserConfig.getClientUserId();
photo.date = ConnectionsManager.getInstance().getCurrentTime();
photo.sizes = sizes;
photo.caption = "";
photo.geo = new TLRPC.TL_geoPointEmpty();
return photo;
}
@ -1811,11 +1875,15 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter
document.size = (int)f.length();
document.dc_id = 0;
if (ext.length() != 0) {
String mimeType = myMime.getMimeTypeFromExtension(ext.toLowerCase());
if (mimeType != null) {
document.mime_type = mimeType;
if (ext.toLowerCase().equals("webp")) {
document.mime_type = "image/webp";
} else {
document.mime_type = "application/octet-stream";
String mimeType = myMime.getMimeTypeFromExtension(ext.toLowerCase());
if (mimeType != null) {
document.mime_type = mimeType;
} else {
document.mime_type = "application/octet-stream";
}
}
} else {
document.mime_type = "application/octet-stream";
@ -1929,9 +1997,10 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter
}).start();
}
public static void prepareSendingPhoto(String imageFilePath, Uri imageUri, long dialog_id, MessageObject reply_to_msg) {
public static void prepareSendingPhoto(String imageFilePath, Uri imageUri, long dialog_id, MessageObject reply_to_msg, CharSequence caption) {
ArrayList<String> paths = null;
ArrayList<Uri> uris = null;
ArrayList<String> captions = null;
if (imageFilePath != null && imageFilePath.length() != 0) {
paths = new ArrayList<>();
paths.add(imageFilePath);
@ -1940,7 +2009,11 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter
uris = new ArrayList<>();
uris.add(imageUri);
}
prepareSendingPhotos(paths, uris, dialog_id, reply_to_msg);
if (caption != null) {
captions = new ArrayList<>();
captions.add(caption.toString());
}
prepareSendingPhotos(paths, uris, dialog_id, reply_to_msg, captions);
}
public static void prepareSendingPhotosSearch(final ArrayList<MediaController.SearchImage> photos, final long dialog_id, final MessageObject reply_to_msg) {
@ -1951,7 +2024,8 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter
@Override
public void run() {
boolean isEncrypted = (int)dialog_id == 0;
for (final MediaController.SearchImage searchImage : photos) {
for (int a = 0; a < photos.size(); a++) {
final MediaController.SearchImage searchImage = photos.get(a);
if (searchImage.type == 1) {
TLRPC.TL_document document = null;
if (!isEncrypted) {
@ -2029,7 +2103,6 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter
photo = new TLRPC.TL_photo();
photo.user_id = UserConfig.getClientUserId();
photo.date = ConnectionsManager.getInstance().getCurrentTime();
photo.caption = "";
photo.geo = new TLRPC.TL_geoPointEmpty();
TLRPC.TL_photoSize photoSize = new TLRPC.TL_photoSize();
photoSize.w = searchImage.width;
@ -2042,6 +2115,9 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter
}
}
if (photo != null) {
if (searchImage.caption != null) {
photo.caption = searchImage.caption.toString();
}
final String originalPathFinal = searchImage.imageUrl;
final TLRPC.TL_photo photoFinal = photo;
final boolean needDownloadHttpFinal = needDownloadHttp;
@ -2058,7 +2134,32 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter
}).start();
}
public static void prepareSendingPhotos(ArrayList<String> paths, ArrayList<Uri> uris, final long dialog_id, final MessageObject reply_to_msg) {
private static String getTrimmedString(String src) {
String result = src.trim();
if (result.length() == 0) {
return result;
}
while (src.startsWith("\n")) {
src = src.substring(1);
}
while (src.endsWith("\n")) {
src = src.substring(0, src.length() - 1);
}
return src;
}
public static void prepareSendingText(String text, long dialog_id) {
text = getTrimmedString(text);
if (text.length() != 0) {
int count = (int) Math.ceil(text.length() / 4096.0f);
for (int a = 0; a < count; a++) {
String mess = text.substring(a * 4096, Math.min((a + 1) * 4096, text.length()));
SendMessagesHelper.getInstance().sendMessage(mess, dialog_id, null, null, true);
}
}
}
public static void prepareSendingPhotos(ArrayList<String> paths, ArrayList<Uri> uris, final long dialog_id, final MessageObject reply_to_msg, final ArrayList<String> captions) {
if (paths == null && uris == null || paths != null && paths.isEmpty() || uris != null && uris.isEmpty()) {
return;
}
@ -2080,6 +2181,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter
int count = !pathsCopy.isEmpty() ? pathsCopy.size() : urisCopy.size();
String path = null;
Uri uri = null;
String extension = null;
for (int a = 0; a < count; a++) {
if (!pathsCopy.isEmpty()) {
path = pathsCopy.get(a);
@ -2096,16 +2198,23 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter
boolean isDocument = false;
if (tempPath != null && (tempPath.endsWith(".gif") || tempPath.endsWith(".webp"))) {
if (tempPath.endsWith(".gif")) {
extension = "gif";
} else {
extension = "webp";
}
isDocument = true;
} else if (tempPath == null && uri != null) {
if (MediaController.isGif(uri)) {
isDocument = true;
originalPath = uri.toString();
tempPath = MediaController.copyDocumentToCache(uri, "gif");
extension = "gif";
} else if (MediaController.isWebp(uri)) {
isDocument = true;
originalPath = uri.toString();
tempPath = MediaController.copyDocumentToCache(uri, "webp");
extension = "webp";
}
}
@ -2134,6 +2243,9 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter
photo = SendMessagesHelper.getInstance().generatePhotoSizes(path, uri);
}
if (photo != null) {
if (captions != null) {
photo.caption = captions.get(a);
}
final String originalPathFinal = originalPath;
final TLRPC.TL_photo photoFinal = photo;
AndroidUtilities.runOnUIThread(new Runnable() {
@ -2147,7 +2259,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter
}
if (sendAsDocuments != null && !sendAsDocuments.isEmpty()) {
for (int a = 0; a < sendAsDocuments.size(); a++) {
prepareSendingDocumentInternal(sendAsDocuments.get(a), sendAsDocumentsOriginal.get(a), null, "gif", dialog_id, reply_to_msg);
prepareSendingDocumentInternal(sendAsDocuments.get(a), sendAsDocumentsOriginal.get(a), null, extension, dialog_id, reply_to_msg);
}
}
}
@ -2190,7 +2302,6 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter
} else {
video.thumb.type = "s";
}
video.caption = "";
video.mime_type = "video/mp4";
video.id = 0;
UserConfig.saveConfig(false);

View File

@ -21,7 +21,6 @@ import org.telegram.messenger.ByteBufferDesc;
import org.telegram.messenger.ConnectionsManager;
import org.telegram.messenger.FileLog;
import org.telegram.messenger.RPCRequest;
import org.telegram.messenger.TLClassStore;
import org.telegram.messenger.TLObject;
import org.telegram.messenger.TLRPC;
@ -65,7 +64,7 @@ public class ReplyMessageQuery {
while (cursor.next()) {
ByteBufferDesc data = MessagesStorage.getInstance().getBuffersStorage().getFreeBuffer(cursor.byteArrayLength(0));
if (data != null && cursor.byteBufferValue(0, data.buffer) != 0) {
TLRPC.Message message = (TLRPC.Message) TLClassStore.Instance().TLdeserialize(data, data.readInt32());
TLRPC.Message message = TLRPC.Message.TLdeserialize(data, data.readInt32(false), false);
message.id = cursor.intValue(1);
message.date = cursor.intValue(2);
message.dialog_id = dialog_id;

View File

@ -20,7 +20,6 @@ import org.telegram.messenger.ByteBufferDesc;
import org.telegram.messenger.ConnectionsManager;
import org.telegram.messenger.FileLog;
import org.telegram.messenger.RPCRequest;
import org.telegram.messenger.TLClassStore;
import org.telegram.messenger.TLObject;
import org.telegram.messenger.TLRPC;
@ -178,7 +177,7 @@ public class SharedMediaQuery {
}
final ArrayList<MessageObject> objects = new ArrayList<>();
for (TLRPC.Message message : res.messages) {
objects.add(new MessageObject(message, usersLocal, false));
objects.add(new MessageObject(message, usersLocal, true));
}
AndroidUtilities.runOnUIThread(new Runnable() {
@ -324,7 +323,7 @@ public class SharedMediaQuery {
while (cursor.next()) {
ByteBufferDesc data = MessagesStorage.getInstance().getBuffersStorage().getFreeBuffer(cursor.byteArrayLength(0));
if (data != null && cursor.byteBufferValue(0, data.buffer) != 0) {
TLRPC.Message message = (TLRPC.Message) TLClassStore.Instance().TLdeserialize(data, data.readInt32());
TLRPC.Message message = TLRPC.Message.TLdeserialize(data, data.readInt32(false), false);
message.id = cursor.intValue(1);
message.dialog_id = uid;
if ((int)uid == 0) {

View File

@ -0,0 +1,182 @@
/*
* This is the source code of Telegram for Android v. 2.x
* It is licensed under GNU GPL v. 2 or later.
* You should have received a copy of the license in this archive (see LICENSE).
*
* Copyright Nikolai Kudashov, 2013-2015.
*/
package org.telegram.android.query;
import org.telegram.SQLite.SQLiteCursor;
import org.telegram.SQLite.SQLitePreparedStatement;
import org.telegram.android.AndroidUtilities;
import org.telegram.android.MessagesStorage;
import org.telegram.android.NotificationCenter;
import org.telegram.messenger.ByteBufferDesc;
import org.telegram.messenger.ConnectionsManager;
import org.telegram.messenger.FileLog;
import org.telegram.messenger.RPCRequest;
import org.telegram.messenger.TLObject;
import org.telegram.messenger.TLRPC;
import org.telegram.messenger.Utilities;
import java.util.ArrayList;
import java.util.HashMap;
public class StickersQuery {
private static String hash;
private static int loadDate;
private static ArrayList<TLRPC.Document> stickers = new ArrayList<>();
private static HashMap<String, ArrayList<TLRPC.Document>> allStickers = new HashMap<>();
private static boolean loadingStickers;
public static void checkStickers() {
if (!loadingStickers && (allStickers.isEmpty() || loadDate < (System.currentTimeMillis() / 1000 - 60 * 60))) {
loadStickers(true);
}
}
public static ArrayList<TLRPC.Document> getStickers() {
return stickers;
}
private static void loadStickers(boolean cache) {
if (loadingStickers) {
return;
}
loadingStickers = true;
if (cache) {
MessagesStorage.getInstance().getStorageQueue().postRunnable(new Runnable() {
@Override
public void run() {
TLRPC.messages_AllStickers result = null;
int date = 0;
try {
SQLiteCursor cursor = MessagesStorage.getInstance().getDatabase().queryFinalized("SELECT data, date FROM stickers WHERE 1");
ArrayList<TLRPC.User> loadedUsers = new ArrayList<>();
if (cursor.next()) {
ByteBufferDesc data = MessagesStorage.getInstance().getBuffersStorage().getFreeBuffer(cursor.byteArrayLength(0));
if (data != null && cursor.byteBufferValue(0, data.buffer) != 0) {
result = TLRPC.messages_AllStickers.TLdeserialize(data, data.readInt32(false), false);
}
date = cursor.intValue(1);
MessagesStorage.getInstance().getBuffersStorage().reuseFreeBuffer(data);
}
cursor.dispose();
} catch (Exception e) {
FileLog.e("tmessages", e);
}
processLoadedStickers(result, true, date);
}
});
} else {
TLRPC.TL_messages_getAllStickers req = new TLRPC.TL_messages_getAllStickers();
req.hash = hash;
if (req.hash == null) {
req.hash = "";
}
ConnectionsManager.getInstance().performRpc(req, new RPCRequest.RPCRequestDelegate() {
@Override
public void run(final TLObject response, final TLRPC.TL_error error) {
AndroidUtilities.runOnUIThread(new Runnable() {
@Override
public void run() {
processLoadedStickers((TLRPC.messages_AllStickers) response, false, (int) (System.currentTimeMillis() / 1000));
}
});
}
});
}
}
private static void putStickersToCache(final TLRPC.TL_messages_allStickers stickers) {
MessagesStorage.getInstance().getStorageQueue().postRunnable(new Runnable() {
@Override
public void run() {
try {
SQLitePreparedStatement state = MessagesStorage.getInstance().getDatabase().executeFast("REPLACE INTO stickers VALUES(?, ?, ?)");
state.requery();
ByteBufferDesc data = MessagesStorage.getInstance().getBuffersStorage().getFreeBuffer(stickers.getObjectSize());
stickers.serializeToStream(data);
state.bindInteger(1, 1);
state.bindByteBuffer(2, data.buffer);
state.bindInteger(3, (int) (System.currentTimeMillis() / 1000));
state.step();
MessagesStorage.getInstance().getBuffersStorage().reuseFreeBuffer(data);
state.dispose();
} catch (Exception e) {
FileLog.e("tmessages", e);
}
}
});
}
private static void processLoadedStickers(final TLRPC.messages_AllStickers res, final boolean cache, final int date) {
AndroidUtilities.runOnUIThread(new Runnable() {
@Override
public void run() {
loadingStickers = false;
}
});
Utilities.stageQueue.postRunnable(new Runnable() {
@Override
public void run() {
if ((res == null || date < (int) (System.currentTimeMillis() / 1000 - 60 * 60)) && cache) {
AndroidUtilities.runOnUIThread(new Runnable() {
@Override
public void run() {
loadStickers(false);
}
});
if (res == null) {
return;
}
}
if (res instanceof TLRPC.TL_messages_allStickers) {
if (!cache) {
putStickersToCache((TLRPC.TL_messages_allStickers) res);
}
HashMap<Long, TLRPC.Document> documents = new HashMap<>();
for (TLRPC.Document document : res.documents) {
if (document == null) {
continue;
}
documents.put(document.id, document);
if (document.thumb != null && document.thumb.location != null) {
document.thumb.location.ext = "webp";
}
}
final HashMap<String, ArrayList<TLRPC.Document>> result = new HashMap<>();
for (TLRPC.TL_stickerPack stickerPack : res.packs) {
if (stickerPack != null && stickerPack.emoticon != null) {
stickerPack.emoticon = stickerPack.emoticon.replace("\uFE0F", "");
ArrayList<TLRPC.Document> arrayList = result.get(stickerPack.emoticon);
for (Long id : stickerPack.documents) {
TLRPC.Document document = documents.get(id);
if (document != null) {
if (arrayList == null) {
arrayList = new ArrayList<>();
result.put(stickerPack.emoticon, arrayList);
}
arrayList.add(document);
}
}
}
}
AndroidUtilities.runOnUIThread(new Runnable() {
@Override
public void run() {
allStickers = result;
stickers = res.documents;
hash = res.hash;
loadDate = date;
NotificationCenter.getInstance().postNotificationName(NotificationCenter.stickersDidLoaded);
}
});
}
}
});
}
}

View File

@ -0,0 +1,633 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.support.v7.util;
import java.lang.reflect.Array;
/**
* A Sorted list implementation that can keep items in order and also notify for changes in the
* list
* such that it can be bound to a {@link android.support.v7.widget.RecyclerView.Adapter
* RecyclerView.Adapter}.
* <p>
* It keeps items ordered using the {@link Callback#compare(Object, Object)} method and uses
* binary search to retrieve items. If the sorting criteria of your items may change, make sure you
* call appropriate methods while editing them to avoid data inconsistencies.
* <p>
* You can control the order of items and change notifications via the {@link Callback} parameter.
*/
@SuppressWarnings("unchecked")
public class SortedList<T> {
/**
* Used by {@link #indexOf(Object)} when he item cannot be found in the list.
*/
public static final int INVALID_POSITION = -1;
private static final int MIN_CAPACITY = 10;
private static final int CAPACITY_GROWTH = MIN_CAPACITY;
private static final int INSERTION = 1;
private static final int DELETION = 1 << 1;
private static final int LOOKUP = 1 << 2;
T[] mData;
/**
* The callback instance that controls the behavior of the SortedList and get notified when
* changes happen.
*/
private Callback mCallback;
private BatchedCallback mBatchedCallback;
private int mSize;
private final Class<T> mTClass;
/**
* Creates a new SortedList of type T.
*
* @param klass The class of the contents of the SortedList.
* @param callback The callback that controls the behavior of SortedList.
*/
public SortedList(Class<T> klass, Callback<T> callback) {
this(klass, callback, MIN_CAPACITY);
}
/**
* Creates a new SortedList of type T.
*
* @param klass The class of the contents of the SortedList.
* @param callback The callback that controls the behavior of SortedList.
* @param initialCapacity The initial capacity to hold items.
*/
public SortedList(Class<T> klass, Callback<T> callback, int initialCapacity) {
mTClass = klass;
mData = (T[]) Array.newInstance(klass, initialCapacity);
mCallback = callback;
mSize = 0;
}
/**
* The number of items in the list.
*
* @return The number of items in the list.
*/
public int size() {
return mSize;
}
/**
* Adds the given item to the list. If this is a new item, SortedList calls
* {@link Callback#onInserted(int, int)}.
* <p>
* If the item already exists in the list and its sorting criteria is not changed, it is
* replaced with the existing Item. SortedList uses
* {@link Callback#areItemsTheSame(Object, Object)} to check if two items are the same item
* and uses {@link Callback#areContentsTheSame(Object, Object)} to decide whether it should
* call {@link Callback#onChanged(int, int)} or not. In both cases, it always removes the
* reference to the old item and puts the new item into the backing array even if
* {@link Callback#areContentsTheSame(Object, Object)} returns false.
* <p>
* If the sorting criteria of the item is changed, SortedList won't be able to find
* its duplicate in the list which will result in having a duplicate of the Item in the list.
* If you need to update sorting criteria of an item that already exists in the list,
* use {@link #updateItemAt(int, Object)}. You can find the index of the item using
* {@link #indexOf(Object)} before you update the object.
*
* @param item The item to be added into the list.
* @return The index of the newly added item.
* @see {@link Callback#compare(Object, Object)}
* @see {@link Callback#areItemsTheSame(Object, Object)}
* @see {@link Callback#areContentsTheSame(Object, Object)}}
*/
public int add(T item) {
return add(item, true);
}
/**
* Batches adapter updates that happen between calling this method until calling
* {@link #endBatchedUpdates()}. For example, if you add multiple items in a loop
* and they are placed into consecutive indices, SortedList calls
* {@link Callback#onInserted(int, int)} only once with the proper item count. If an event
* cannot be merged with the previous event, the previous event is dispatched
* to the callback instantly.
* <p>
* After running your data updates, you <b>must</b> call {@link #endBatchedUpdates()}
* which will dispatch any deferred data change event to the current callback.
* <p>
* A sample implementation may look like this:
* <pre>
* mSortedList.beginBatchedUpdates();
* try {
* mSortedList.add(item1)
* mSortedList.add(item2)
* mSortedList.remove(item3)
* ...
* } finally {
* mSortedList.endBatchedUpdates();
* }
* </pre>
* <p>
* Instead of using this method to batch calls, you can use a Callback that extends
* {@link BatchedCallback}. In that case, you must make sure that you are manually calling
* {@link BatchedCallback#dispatchLastEvent()} right after you complete your data changes.
* Failing to do so may create data inconsistencies with the Callback.
* <p>
* If the current Callback in an instance of {@link BatchedCallback}, calling this method
* has no effect.
*/
public void beginBatchedUpdates() {
if (mCallback instanceof BatchedCallback) {
return;
}
if (mBatchedCallback == null) {
mBatchedCallback = new BatchedCallback(mCallback);
}
mCallback = mBatchedCallback;
}
/**
* Ends the update transaction and dispatches any remaining event to the callback.
*/
public void endBatchedUpdates() {
if (mCallback instanceof BatchedCallback) {
((BatchedCallback) mCallback).dispatchLastEvent();
}
if (mCallback == mBatchedCallback) {
mCallback = mBatchedCallback.mWrappedCallback;
}
}
private int add(T item, boolean notify) {
int index = findIndexOf(item, INSERTION);
if (index == INVALID_POSITION) {
index = 0;
} else if (index < mSize) {
T existing = mData[index];
if (mCallback.areItemsTheSame(existing, item)) {
if (mCallback.areContentsTheSame(existing, item)) {
//no change but still replace the item
mData[index] = item;
return index;
} else {
mData[index] = item;
mCallback.onChanged(index, 1);
return index;
}
}
}
addToData(index, item);
if (notify) {
mCallback.onInserted(index, 1);
}
return index;
}
/**
* Removes the provided item from the list and calls {@link Callback#onRemoved(int, int)}.
*
* @param item The item to be removed from the list.
* @return True if item is removed, false if item cannot be found in the list.
*/
public boolean remove(T item) {
return remove(item, true);
}
/**
* Removes the item at the given index and calls {@link Callback#onRemoved(int, int)}.
*
* @param index The index of the item to be removed.
* @return The removed item.
*/
public T removeItemAt(int index) {
T item = get(index);
removeItemAtIndex(index, true);
return item;
}
private boolean remove(T item, boolean notify) {
int index = findIndexOf(item, DELETION);
if (index == INVALID_POSITION) {
return false;
}
removeItemAtIndex(index, notify);
return true;
}
private void removeItemAtIndex(int index, boolean notify) {
System.arraycopy(mData, index + 1, mData, index, mSize - index - 1);
mSize--;
mData[mSize] = null;
if (notify) {
mCallback.onRemoved(index, 1);
}
}
/**
* Updates the item at the given index and calls {@link Callback#onChanged(int, int)} and/or
* {@link Callback#onMoved(int, int)} if necessary.
* <p>
* You can use this method if you need to change an existing Item such that its position in the
* list may change.
* <p>
* If the new object is a different object (<code>get(index) != item</code>) and
* {@link Callback#areContentsTheSame(Object, Object)} returns <code>true</code>, SortedList
* avoids calling {@link Callback#onChanged(int, int)} otherwise it calls
* {@link Callback#onChanged(int, int)}.
* <p>
* If the new position of the item is different than the provided <code>index</code>,
* SortedList
* calls {@link Callback#onMoved(int, int)}.
*
* @param index The index of the item to replace
* @param item The item to replace the item at the given Index.
* @see #add(Object)
*/
public void updateItemAt(int index, T item) {
final T existing = get(index);
// assume changed if the same object is given back
boolean contentsChanged = existing == item || !mCallback.areContentsTheSame(existing, item);
if (existing != item) {
// different items, we can use comparison and may avoid lookup
final int cmp = mCallback.compare(existing, item);
if (cmp == 0) {
mData[index] = item;
if (contentsChanged) {
mCallback.onChanged(index, 1);
}
return;
}
}
if (contentsChanged) {
mCallback.onChanged(index, 1);
}
// TODO this done in 1 pass to avoid shifting twice.
removeItemAtIndex(index, false);
int newIndex = add(item, false);
if (index != newIndex) {
mCallback.onMoved(index, newIndex);
}
}
/**
* This method can be used to recalculate the position of the item at the given index, without
* triggering an {@link Callback#onChanged(int, int)} callback.
* <p>
* If you are editing objects in the list such that their position in the list may change but
* you don't want to trigger an onChange animation, you can use this method to re-position it.
* If the item changes position, SortedList will call {@link Callback#onMoved(int, int)}
* without
* calling {@link Callback#onChanged(int, int)}.
* <p>
* A sample usage may look like:
*
* <pre>
* final int position = mSortedList.indexOf(item);
* item.incrementPriority(); // assume items are sorted by priority
* mSortedList.recalculatePositionOfItemAt(position);
* </pre>
* In the example above, because the sorting criteria of the item has been changed,
* mSortedList.indexOf(item) will not be able to find the item. This is why the code above
* first
* gets the position before editing the item, edits it and informs the SortedList that item
* should be repositioned.
*
* @param index The current index of the Item whose position should be re-calculated.
* @see #updateItemAt(int, Object)
* @see #add(Object)
*/
public void recalculatePositionOfItemAt(int index) {
// TODO can be improved
final T item = get(index);
removeItemAtIndex(index, false);
int newIndex = add(item, false);
if (index != newIndex) {
mCallback.onMoved(index, newIndex);
}
}
/**
* Returns the item at the given index.
*
* @param index The index of the item to retrieve.
* @return The item at the given index.
* @throws java.lang.IndexOutOfBoundsException if provided index is negative or larger than the
* size of the list.
*/
public T get(int index) throws IndexOutOfBoundsException {
if (index >= mSize || index < 0) {
throw new IndexOutOfBoundsException("Asked to get item at " + index + " but size is "
+ mSize);
}
return mData[index];
}
/**
* Returns the position of the provided item.
*
* @param item The item to query for position.
* @return The position of the provided item or {@link #INVALID_POSITION} if item is not in the
* list.
*/
public int indexOf(T item) {
return findIndexOf(item, LOOKUP);
}
private int findIndexOf(T item, int reason) {
int left = 0;
int right = mSize;
while (left < right) {
final int middle = (left + right) / 2;
T myItem = mData[middle];
final int cmp = mCallback.compare(myItem, item);
if (cmp < 0) {
left = middle + 1;
} else if (cmp == 0) {
if (mCallback.areItemsTheSame(myItem, item)) {
return middle;
} else {
int exact = linearEqualitySearch(item, middle, left, right);
if (reason == INSERTION) {
return exact == INVALID_POSITION ? middle : exact;
} else {
return exact;
}
}
} else {
right = middle;
}
}
return reason == INSERTION ? left : INVALID_POSITION;
}
private int linearEqualitySearch(T item, int middle, int left, int right) {
// go left
for (int next = middle - 1; next >= left; next--) {
T nextItem = mData[next];
int cmp = mCallback.compare(nextItem, item);
if (cmp != 0) {
break;
}
if (mCallback.areItemsTheSame(nextItem, item)) {
return next;
}
}
for (int next = middle + 1; next < right; next++) {
T nextItem = mData[next];
int cmp = mCallback.compare(nextItem, item);
if (cmp != 0) {
break;
}
if (mCallback.areItemsTheSame(nextItem, item)) {
return next;
}
}
return INVALID_POSITION;
}
private void addToData(int index, T item) {
if (index > mSize) {
throw new IndexOutOfBoundsException(
"cannot add item to " + index + " because size is " + mSize);
}
if (mSize == mData.length) {
// we are at the limit enlarge
T[] newData = (T[]) Array.newInstance(mTClass, mData.length + CAPACITY_GROWTH);
System.arraycopy(mData, 0, newData, 0, index);
newData[index] = item;
System.arraycopy(mData, index, newData, index + 1, mSize - index);
mData = newData;
} else {
// just shift, we fit
System.arraycopy(mData, index, mData, index + 1, mSize - index);
mData[index] = item;
}
mSize++;
}
/**
* The class that controls the behavior of the {@link SortedList}.
* <p>
* It defines how items should be sorted and how duplicates should be handled.
* <p>
* SortedList calls the callback methods on this class to notify changes about the underlying
* data.
*/
public static abstract class Callback<T2> {
/**
* Similar to {@link java.util.Comparator#compare(Object, Object)}, should compare two and
* return how they should be ordered.
*
* @param o1 The first object to compare.
* @param o2 The second object to compare.
* @return a negative integer, zero, or a positive integer as the
* first argument is less than, equal to, or greater than the
* second.
*/
abstract public int compare(T2 o1, T2 o2);
/**
* Called by the SortedList when an item is inserted at the given position.
*
* @param position The position of the new item.
* @param count The number of items that have been added.
*/
abstract public void onInserted(int position, int count);
/**
* Called by the SortedList when an item is removed from the given position.
*
* @param position The position of the item which has been removed.
* @param count The number of items which have been removed.
*/
abstract public void onRemoved(int position, int count);
/**
* Called by the SortedList when an item changes its position in the list.
*
* @param fromPosition The previous position of the item before the move.
* @param toPosition The new position of the item.
*/
abstract public void onMoved(int fromPosition, int toPosition);
/**
* Called by the SortedList when the item at the given position is updated.
*
* @param position The position of the item which has been updated.
* @param count The number of items which has changed.
*/
abstract public void onChanged(int position, int count);
/**
* Called by the SortedList when it wants to check whether two items have the same data
* or not. SortedList uses this information to decide whether it should call
* {@link #onChanged(int, int)} or not.
* <p>
* SortedList uses this method to check equality instead of {@link Object#equals(Object)}
* so
* that you can change its behavior depending on your UI.
* <p>
* For example, if you are using SortedList with a {@link android.support.v7.widget.RecyclerView.Adapter
* RecyclerView.Adapter}, you should
* return whether the items' visual representations are the same or not.
*
* @param oldItem The previous representation of the object.
* @param newItem The new object that replaces the previous one.
* @return True if the contents of the items are the same or false if they are different.
*/
abstract public boolean areContentsTheSame(T2 oldItem, T2 newItem);
/**
* Called by the SortedList to decide whether two object represent the same Item or not.
* <p>
* For example, if your items have unique ids, this method should check their equality.
*
* @param item1 The first item to check.
* @param item2 The second item to check.
* @return True if the two items represent the same object or false if they are different.
*/
abstract public boolean areItemsTheSame(T2 item1, T2 item2);
}
/**
* A callback implementation that can batch notify events dispatched by the SortedList.
* <p>
* This class can be useful if you want to do multiple operations on a SortedList but don't
* want to dispatch each event one by one, which may result in a performance issue.
* <p>
* For example, if you are going to add multiple items to a SortedList, BatchedCallback call
* convert individual <code>onInserted(index, 1)</code> calls into one
* <code>onInserted(index, N)</code> if items are added into consecutive indices. This change
* can help RecyclerView resolve changes much more easily.
* <p>
* If consecutive changes in the SortedList are not suitable for batching, BatchingCallback
* dispatches them as soon as such case is detected. After your edits on the SortedList is
* complete, you <b>must</b> always call {@link BatchedCallback#dispatchLastEvent()} to flush
* all changes to the Callback.
*/
public static class BatchedCallback<T2> extends Callback<T2> {
private final Callback<T2> mWrappedCallback;
static final int TYPE_NONE = 0;
static final int TYPE_ADD = 1;
static final int TYPE_REMOVE = 2;
static final int TYPE_CHANGE = 3;
static final int TYPE_MOVE = 4;
int mLastEventType = TYPE_NONE;
int mLastEventPosition = -1;
int mLastEventCount = -1;
/**
* Creates a new BatchedCallback that wraps the provided Callback.
*
* @param wrappedCallback The Callback which should received the data change callbacks.
* Other method calls (e.g. {@link #compare(Object, Object)} from
* the SortedList are directly forwarded to this Callback.
*/
public BatchedCallback(Callback<T2> wrappedCallback) {
mWrappedCallback = wrappedCallback;
}
@Override
public int compare(T2 o1, T2 o2) {
return mWrappedCallback.compare(o1, o2);
}
@Override
public void onInserted(int position, int count) {
if (mLastEventType == TYPE_ADD && position >= mLastEventPosition
&& position <= mLastEventPosition + mLastEventCount) {
mLastEventCount += count;
mLastEventPosition = Math.min(position, mLastEventPosition);
return;
}
dispatchLastEvent();
mLastEventPosition = position;
mLastEventCount = count;
mLastEventType = TYPE_ADD;
}
@Override
public void onRemoved(int position, int count) {
if (mLastEventType == TYPE_REMOVE && mLastEventPosition == position) {
mLastEventCount += count;
return;
}
dispatchLastEvent();
mLastEventPosition = position;
mLastEventCount = count;
mLastEventType = TYPE_REMOVE;
}
@Override
public void onMoved(int fromPosition, int toPosition) {
dispatchLastEvent();//moves are not merged
mWrappedCallback.onMoved(fromPosition, toPosition);
}
@Override
public void onChanged(int position, int count) {
if (mLastEventType == TYPE_CHANGE &&
!(position > mLastEventPosition + mLastEventCount
|| position + count < mLastEventPosition)) {
// take potential overlap into account
int previousEnd = mLastEventPosition + mLastEventCount;
mLastEventPosition = Math.min(position, mLastEventPosition);
mLastEventCount = Math.max(previousEnd, position + count) - mLastEventPosition;
return;
}
dispatchLastEvent();
mLastEventPosition = position;
mLastEventCount = count;
mLastEventType = TYPE_CHANGE;
}
@Override
public boolean areContentsTheSame(T2 oldItem, T2 newItem) {
return mWrappedCallback.areContentsTheSame(oldItem, newItem);
}
@Override
public boolean areItemsTheSame(T2 item1, T2 item2) {
return mWrappedCallback.areItemsTheSame(item1, item2);
}
/**
* This method dispatches any pending event notifications to the wrapped Callback.
* You <b>must</b> always call this method after you are done with editing the SortedList.
*/
public void dispatchLastEvent() {
if (mLastEventType == TYPE_NONE) {
return;
}
switch (mLastEventType) {
case TYPE_ADD:
mWrappedCallback.onInserted(mLastEventPosition, mLastEventCount);
break;
case TYPE_REMOVE:
mWrappedCallback.onRemoved(mLastEventPosition, mLastEventCount);
break;
case TYPE_CHANGE:
mWrappedCallback.onChanged(mLastEventPosition, mLastEventCount);
break;
}
mLastEventType = TYPE_NONE;
}
}
}

View File

@ -0,0 +1,733 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.telegram.android.support.widget;
import android.support.v4.util.Pools;
import android.util.Log;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static org.telegram.android.support.widget.RecyclerView.*;
/**
* Helper class that can enqueue and process adapter update operations.
* <p>
* To support animations, RecyclerView presents an older version the Adapter to best represent
* previous state of the layout. Sometimes, this is not trivial when items are removed that were
* not laid out, in which case, RecyclerView has no way of providing that item's view for
* animations.
* <p>
* AdapterHelper creates an UpdateOp for each adapter data change then pre-processes them. During
* pre processing, AdapterHelper finds out which UpdateOps can be deferred to second layout pass
* and which cannot. For the UpdateOps that cannot be deferred, AdapterHelper will change them
* according to previously deferred operation and dispatch them before the first layout pass. It
* also takes care of updating deferred UpdateOps since order of operations is changed by this
* process.
* <p>
* Although operations may be forwarded to LayoutManager in different orders, resulting data set
* is guaranteed to be the consistent.
*/
class AdapterHelper implements OpReorderer.Callback {
final static int POSITION_TYPE_INVISIBLE = 0;
final static int POSITION_TYPE_NEW_OR_LAID_OUT = 1;
private static final boolean DEBUG = false;
private static final String TAG = "AHT";
private Pools.Pool<UpdateOp> mUpdateOpPool = new Pools.SimplePool<UpdateOp>(UpdateOp.POOL_SIZE);
final ArrayList<UpdateOp> mPendingUpdates = new ArrayList<UpdateOp>();
final ArrayList<UpdateOp> mPostponedList = new ArrayList<UpdateOp>();
final Callback mCallback;
Runnable mOnItemProcessedCallback;
final boolean mDisableRecycler;
final OpReorderer mOpReorderer;
AdapterHelper(Callback callback) {
this(callback, false);
}
AdapterHelper(Callback callback, boolean disableRecycler) {
mCallback = callback;
mDisableRecycler = disableRecycler;
mOpReorderer = new OpReorderer(this);
}
AdapterHelper addUpdateOp(UpdateOp... ops) {
Collections.addAll(mPendingUpdates, ops);
return this;
}
void reset() {
recycleUpdateOpsAndClearList(mPendingUpdates);
recycleUpdateOpsAndClearList(mPostponedList);
}
void preProcess() {
mOpReorderer.reorderOps(mPendingUpdates);
final int count = mPendingUpdates.size();
for (int i = 0; i < count; i++) {
UpdateOp op = mPendingUpdates.get(i);
switch (op.cmd) {
case UpdateOp.ADD:
applyAdd(op);
break;
case UpdateOp.REMOVE:
applyRemove(op);
break;
case UpdateOp.UPDATE:
applyUpdate(op);
break;
case UpdateOp.MOVE:
applyMove(op);
break;
}
if (mOnItemProcessedCallback != null) {
mOnItemProcessedCallback.run();
}
}
mPendingUpdates.clear();
}
void consumePostponedUpdates() {
final int count = mPostponedList.size();
for (int i = 0; i < count; i++) {
mCallback.onDispatchSecondPass(mPostponedList.get(i));
}
recycleUpdateOpsAndClearList(mPostponedList);
}
private void applyMove(UpdateOp op) {
// MOVE ops are pre-processed so at this point, we know that item is still in the adapter.
// otherwise, it would be converted into a REMOVE operation
postponeAndUpdateViewHolders(op);
}
private void applyRemove(UpdateOp op) {
int tmpStart = op.positionStart;
int tmpCount = 0;
int tmpEnd = op.positionStart + op.itemCount;
int type = -1;
for (int position = op.positionStart; position < tmpEnd; position++) {
boolean typeChanged = false;
ViewHolder vh = mCallback.findViewHolder(position);
if (vh != null || canFindInPreLayout(position)) {
// If a ViewHolder exists or this is a newly added item, we can defer this update
// to post layout stage.
// * For existing ViewHolders, we'll fake its existence in the pre-layout phase.
// * For items that are added and removed in the same process cycle, they won't
// have any effect in pre-layout since their add ops are already deferred to
// post-layout pass.
if (type == POSITION_TYPE_INVISIBLE) {
// Looks like we have other updates that we cannot merge with this one.
// Create an UpdateOp and dispatch it to LayoutManager.
UpdateOp newOp = obtainUpdateOp(UpdateOp.REMOVE, tmpStart, tmpCount);
dispatchAndUpdateViewHolders(newOp);
typeChanged = true;
}
type = POSITION_TYPE_NEW_OR_LAID_OUT;
} else {
// This update cannot be recovered because we don't have a ViewHolder representing
// this position. Instead, post it to LayoutManager immediately
if (type == POSITION_TYPE_NEW_OR_LAID_OUT) {
// Looks like we have other updates that we cannot merge with this one.
// Create UpdateOp op and dispatch it to LayoutManager.
UpdateOp newOp = obtainUpdateOp(UpdateOp.REMOVE, tmpStart, tmpCount);
postponeAndUpdateViewHolders(newOp);
typeChanged = true;
}
type = POSITION_TYPE_INVISIBLE;
}
if (typeChanged) {
position -= tmpCount; // also equal to tmpStart
tmpEnd -= tmpCount;
tmpCount = 1;
} else {
tmpCount++;
}
}
if (tmpCount != op.itemCount) { // all 1 effect
recycleUpdateOp(op);
op = obtainUpdateOp(UpdateOp.REMOVE, tmpStart, tmpCount);
}
if (type == POSITION_TYPE_INVISIBLE) {
dispatchAndUpdateViewHolders(op);
} else {
postponeAndUpdateViewHolders(op);
}
}
private void applyUpdate(UpdateOp op) {
int tmpStart = op.positionStart;
int tmpCount = 0;
int tmpEnd = op.positionStart + op.itemCount;
int type = -1;
for (int position = op.positionStart; position < tmpEnd; position++) {
ViewHolder vh = mCallback.findViewHolder(position);
if (vh != null || canFindInPreLayout(position)) { // deferred
if (type == POSITION_TYPE_INVISIBLE) {
UpdateOp newOp = obtainUpdateOp(UpdateOp.UPDATE, tmpStart, tmpCount);
dispatchAndUpdateViewHolders(newOp);
tmpCount = 0;
tmpStart = position;
}
type = POSITION_TYPE_NEW_OR_LAID_OUT;
} else { // applied
if (type == POSITION_TYPE_NEW_OR_LAID_OUT) {
UpdateOp newOp = obtainUpdateOp(UpdateOp.UPDATE, tmpStart, tmpCount);
postponeAndUpdateViewHolders(newOp);
tmpCount = 0;
tmpStart = position;
}
type = POSITION_TYPE_INVISIBLE;
}
tmpCount++;
}
if (tmpCount != op.itemCount) { // all 1 effect
recycleUpdateOp(op);
op = obtainUpdateOp(UpdateOp.UPDATE, tmpStart, tmpCount);
}
if (type == POSITION_TYPE_INVISIBLE) {
dispatchAndUpdateViewHolders(op);
} else {
postponeAndUpdateViewHolders(op);
}
}
private void dispatchAndUpdateViewHolders(UpdateOp op) {
// tricky part.
// traverse all postpones and revert their changes on this op if necessary, apply updated
// dispatch to them since now they are after this op.
if (op.cmd == UpdateOp.ADD || op.cmd == UpdateOp.MOVE) {
throw new IllegalArgumentException("should not dispatch add or move for pre layout");
}
if (DEBUG) {
Log.d(TAG, "dispatch (pre)" + op);
Log.d(TAG, "postponed state before:");
for (UpdateOp updateOp : mPostponedList) {
Log.d(TAG, updateOp.toString());
}
Log.d(TAG, "----");
}
// handle each pos 1 by 1 to ensure continuity. If it breaks, dispatch partial
// TODO Since move ops are pushed to end, we should not need this anymore
int tmpStart = updatePositionWithPostponed(op.positionStart, op.cmd);
if (DEBUG) {
Log.d(TAG, "pos:" + op.positionStart + ",updatedPos:" + tmpStart);
}
int tmpCnt = 1;
int offsetPositionForPartial = op.positionStart;
final int positionMultiplier;
switch (op.cmd) {
case UpdateOp.UPDATE:
positionMultiplier = 1;
break;
case UpdateOp.REMOVE:
positionMultiplier = 0;
break;
default:
throw new IllegalArgumentException("op should be remove or update." + op);
}
for (int p = 1; p < op.itemCount; p++) {
final int pos = op.positionStart + (positionMultiplier * p);
int updatedPos = updatePositionWithPostponed(pos, op.cmd);
if (DEBUG) {
Log.d(TAG, "pos:" + pos + ",updatedPos:" + updatedPos);
}
boolean continuous = false;
switch (op.cmd) {
case UpdateOp.UPDATE:
continuous = updatedPos == tmpStart + 1;
break;
case UpdateOp.REMOVE:
continuous = updatedPos == tmpStart;
break;
}
if (continuous) {
tmpCnt++;
} else {
// need to dispatch this separately
UpdateOp tmp = obtainUpdateOp(op.cmd, tmpStart, tmpCnt);
if (DEBUG) {
Log.d(TAG, "need to dispatch separately " + tmp);
}
dispatchFirstPassAndUpdateViewHolders(tmp, offsetPositionForPartial);
recycleUpdateOp(tmp);
if (op.cmd == UpdateOp.UPDATE) {
offsetPositionForPartial += tmpCnt;
}
tmpStart = updatedPos;// need to remove previously dispatched
tmpCnt = 1;
}
}
recycleUpdateOp(op);
if (tmpCnt > 0) {
UpdateOp tmp = obtainUpdateOp(op.cmd, tmpStart, tmpCnt);
if (DEBUG) {
Log.d(TAG, "dispatching:" + tmp);
}
dispatchFirstPassAndUpdateViewHolders(tmp, offsetPositionForPartial);
recycleUpdateOp(tmp);
}
if (DEBUG) {
Log.d(TAG, "post dispatch");
Log.d(TAG, "postponed state after:");
for (UpdateOp updateOp : mPostponedList) {
Log.d(TAG, updateOp.toString());
}
Log.d(TAG, "----");
}
}
void dispatchFirstPassAndUpdateViewHolders(UpdateOp op, int offsetStart) {
mCallback.onDispatchFirstPass(op);
switch (op.cmd) {
case UpdateOp.REMOVE:
mCallback.offsetPositionsForRemovingInvisible(offsetStart, op.itemCount);
break;
case UpdateOp.UPDATE:
mCallback.markViewHoldersUpdated(offsetStart, op.itemCount);
break;
default:
throw new IllegalArgumentException("only remove and update ops can be dispatched"
+ " in first pass");
}
}
private int updatePositionWithPostponed(int pos, int cmd) {
final int count = mPostponedList.size();
for (int i = count - 1; i >= 0; i--) {
UpdateOp postponed = mPostponedList.get(i);
if (postponed.cmd == UpdateOp.MOVE) {
int start, end;
if (postponed.positionStart < postponed.itemCount) {
start = postponed.positionStart;
end = postponed.itemCount;
} else {
start = postponed.itemCount;
end = postponed.positionStart;
}
if (pos >= start && pos <= end) {
//i'm affected
if (start == postponed.positionStart) {
if (cmd == UpdateOp.ADD) {
postponed.itemCount++;
} else if (cmd == UpdateOp.REMOVE) {
postponed.itemCount--;
}
// op moved to left, move it right to revert
pos++;
} else {
if (cmd == UpdateOp.ADD) {
postponed.positionStart++;
} else if (cmd == UpdateOp.REMOVE) {
postponed.positionStart--;
}
// op was moved right, move left to revert
pos--;
}
} else if (pos < postponed.positionStart) {
// postponed MV is outside the dispatched OP. if it is before, offset
if (cmd == UpdateOp.ADD) {
postponed.positionStart++;
postponed.itemCount++;
} else if (cmd == UpdateOp.REMOVE) {
postponed.positionStart--;
postponed.itemCount--;
}
}
} else {
if (postponed.positionStart <= pos) {
if (postponed.cmd == UpdateOp.ADD) {
pos -= postponed.itemCount;
} else if (postponed.cmd == UpdateOp.REMOVE) {
pos += postponed.itemCount;
}
} else {
if (cmd == UpdateOp.ADD) {
postponed.positionStart++;
} else if (cmd == UpdateOp.REMOVE) {
postponed.positionStart--;
}
}
}
if (DEBUG) {
Log.d(TAG, "dispath (step" + i + ")");
Log.d(TAG, "postponed state:" + i + ", pos:" + pos);
for (UpdateOp updateOp : mPostponedList) {
Log.d(TAG, updateOp.toString());
}
Log.d(TAG, "----");
}
}
for (int i = mPostponedList.size() - 1; i >= 0; i--) {
UpdateOp op = mPostponedList.get(i);
if (op.cmd == UpdateOp.MOVE) {
if (op.itemCount == op.positionStart || op.itemCount < 0) {
mPostponedList.remove(i);
recycleUpdateOp(op);
}
} else if (op.itemCount <= 0) {
mPostponedList.remove(i);
recycleUpdateOp(op);
}
}
return pos;
}
private boolean canFindInPreLayout(int position) {
final int count = mPostponedList.size();
for (int i = 0; i < count; i++) {
UpdateOp op = mPostponedList.get(i);
if (op.cmd == UpdateOp.MOVE) {
if (findPositionOffset(op.itemCount, i + 1) == position) {
return true;
}
} else if (op.cmd == UpdateOp.ADD) {
// TODO optimize.
final int end = op.positionStart + op.itemCount;
for (int pos = op.positionStart; pos < end; pos++) {
if (findPositionOffset(pos, i + 1) == position) {
return true;
}
}
}
}
return false;
}
private void applyAdd(UpdateOp op) {
postponeAndUpdateViewHolders(op);
}
private void postponeAndUpdateViewHolders(UpdateOp op) {
if (DEBUG) {
Log.d(TAG, "postponing " + op);
}
mPostponedList.add(op);
switch (op.cmd) {
case UpdateOp.ADD:
mCallback.offsetPositionsForAdd(op.positionStart, op.itemCount);
break;
case UpdateOp.MOVE:
mCallback.offsetPositionsForMove(op.positionStart, op.itemCount);
break;
case UpdateOp.REMOVE:
mCallback.offsetPositionsForRemovingLaidOutOrNewView(op.positionStart,
op.itemCount);
break;
case UpdateOp.UPDATE:
mCallback.markViewHoldersUpdated(op.positionStart, op.itemCount);
break;
default:
throw new IllegalArgumentException("Unknown update op type for " + op);
}
}
boolean hasPendingUpdates() {
return mPendingUpdates.size() > 0;
}
int findPositionOffset(int position) {
return findPositionOffset(position, 0);
}
int findPositionOffset(int position, int firstPostponedItem) {
int count = mPostponedList.size();
for (int i = firstPostponedItem; i < count; ++i) {
UpdateOp op = mPostponedList.get(i);
if (op.cmd == UpdateOp.MOVE) {
if (op.positionStart == position) {
position = op.itemCount;
} else {
if (op.positionStart < position) {
position--; // like a remove
}
if (op.itemCount <= position) {
position++; // like an add
}
}
} else if (op.positionStart <= position) {
if (op.cmd == UpdateOp.REMOVE) {
if (position < op.positionStart + op.itemCount) {
return -1;
}
position -= op.itemCount;
} else if (op.cmd == UpdateOp.ADD) {
position += op.itemCount;
}
}
}
return position;
}
/**
* @return True if updates should be processed.
*/
boolean onItemRangeChanged(int positionStart, int itemCount) {
mPendingUpdates.add(obtainUpdateOp(UpdateOp.UPDATE, positionStart, itemCount));
return mPendingUpdates.size() == 1;
}
/**
* @return True if updates should be processed.
*/
boolean onItemRangeInserted(int positionStart, int itemCount) {
mPendingUpdates.add(obtainUpdateOp(UpdateOp.ADD, positionStart, itemCount));
return mPendingUpdates.size() == 1;
}
/**
* @return True if updates should be processed.
*/
boolean onItemRangeRemoved(int positionStart, int itemCount) {
mPendingUpdates.add(obtainUpdateOp(UpdateOp.REMOVE, positionStart, itemCount));
return mPendingUpdates.size() == 1;
}
/**
* @return True if updates should be processed.
*/
boolean onItemRangeMoved(int from, int to, int itemCount) {
if (from == to) {
return false;//no-op
}
if (itemCount != 1) {
throw new IllegalArgumentException("Moving more than 1 item is not supported yet");
}
mPendingUpdates.add(obtainUpdateOp(UpdateOp.MOVE, from, to));
return mPendingUpdates.size() == 1;
}
/**
* Skips pre-processing and applies all updates in one pass.
*/
void consumeUpdatesInOnePass() {
// we still consume postponed updates (if there is) in case there was a pre-process call
// w/o a matching consumePostponedUpdates.
consumePostponedUpdates();
final int count = mPendingUpdates.size();
for (int i = 0; i < count; i++) {
UpdateOp op = mPendingUpdates.get(i);
switch (op.cmd) {
case UpdateOp.ADD:
mCallback.onDispatchSecondPass(op);
mCallback.offsetPositionsForAdd(op.positionStart, op.itemCount);
break;
case UpdateOp.REMOVE:
mCallback.onDispatchSecondPass(op);
mCallback.offsetPositionsForRemovingInvisible(op.positionStart, op.itemCount);
break;
case UpdateOp.UPDATE:
mCallback.onDispatchSecondPass(op);
mCallback.markViewHoldersUpdated(op.positionStart, op.itemCount);
break;
case UpdateOp.MOVE:
mCallback.onDispatchSecondPass(op);
mCallback.offsetPositionsForMove(op.positionStart, op.itemCount);
break;
}
if (mOnItemProcessedCallback != null) {
mOnItemProcessedCallback.run();
}
}
recycleUpdateOpsAndClearList(mPendingUpdates);
}
public int applyPendingUpdatesToPosition(int position) {
final int size = mPendingUpdates.size();
for (int i = 0; i < size; i ++) {
UpdateOp op = mPendingUpdates.get(i);
switch (op.cmd) {
case UpdateOp.ADD:
if (op.positionStart <= position) {
position += op.itemCount;
}
break;
case UpdateOp.REMOVE:
if (op.positionStart <= position) {
final int end = op.positionStart + op.itemCount;
if (end > position) {
return RecyclerView.NO_POSITION;
}
position -= op.itemCount;
}
break;
case UpdateOp.MOVE:
if (op.positionStart == position) {
position = op.itemCount;//position end
} else {
if (op.positionStart < position) {
position -= 1;
}
if (op.itemCount <= position) {
position += 1;
}
}
break;
}
}
return position;
}
/**
* Queued operation to happen when child views are updated.
*/
static class UpdateOp {
static final int ADD = 0;
static final int REMOVE = 1;
static final int UPDATE = 2;
static final int MOVE = 3;
static final int POOL_SIZE = 30;
int cmd;
int positionStart;
// holds the target position if this is a MOVE
int itemCount;
UpdateOp(int cmd, int positionStart, int itemCount) {
this.cmd = cmd;
this.positionStart = positionStart;
this.itemCount = itemCount;
}
String cmdToString() {
switch (cmd) {
case ADD:
return "add";
case REMOVE:
return "rm";
case UPDATE:
return "up";
case MOVE:
return "mv";
}
return "??";
}
@Override
public String toString() {
return "[" + cmdToString() + ",s:" + positionStart + "c:" + itemCount + "]";
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
UpdateOp op = (UpdateOp) o;
if (cmd != op.cmd) {
return false;
}
if (cmd == MOVE && Math.abs(itemCount - positionStart) == 1) {
// reverse of this is also true
if (itemCount == op.positionStart && positionStart == op.itemCount) {
return true;
}
}
if (itemCount != op.itemCount) {
return false;
}
if (positionStart != op.positionStart) {
return false;
}
return true;
}
@Override
public int hashCode() {
int result = cmd;
result = 31 * result + positionStart;
result = 31 * result + itemCount;
return result;
}
}
@Override
public UpdateOp obtainUpdateOp(int cmd, int positionStart, int itemCount) {
UpdateOp op = mUpdateOpPool.acquire();
if (op == null) {
op = new UpdateOp(cmd, positionStart, itemCount);
} else {
op.cmd = cmd;
op.positionStart = positionStart;
op.itemCount = itemCount;
}
return op;
}
@Override
public void recycleUpdateOp(UpdateOp op) {
if (!mDisableRecycler) {
mUpdateOpPool.release(op);
}
}
void recycleUpdateOpsAndClearList(List<UpdateOp> ops) {
final int count = ops.size();
for (int i = 0; i < count; i++) {
recycleUpdateOp(ops.get(i));
}
ops.clear();
}
/**
* Contract between AdapterHelper and RecyclerView.
*/
static interface Callback {
ViewHolder findViewHolder(int position);
void offsetPositionsForRemovingInvisible(int positionStart, int itemCount);
void offsetPositionsForRemovingLaidOutOrNewView(int positionStart, int itemCount);
void markViewHoldersUpdated(int positionStart, int itemCount);
void onDispatchFirstPass(UpdateOp updateOp);
void onDispatchSecondPass(UpdateOp updateOp);
void offsetPositionsForAdd(int positionStart, int itemCount);
void offsetPositionsForMove(int from, int to);
}
}

View File

@ -0,0 +1,487 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.telegram.android.support.widget;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import java.util.ArrayList;
import java.util.List;
/**
* Helper class to manage children.
* <p>
* It wraps a RecyclerView and adds ability to hide some children. There are two sets of methods
* provided by this class. <b>Regular</b> methods are the ones that replicate ViewGroup methods
* like getChildAt, getChildCount etc. These methods ignore hidden children.
* <p>
* When RecyclerView needs direct access to the view group children, it can call unfiltered
* methods like get getUnfilteredChildCount or getUnfilteredChildAt.
*/
class ChildHelper {
private static final boolean DEBUG = false;
private static final String TAG = "ChildrenHelper";
final Callback mCallback;
final Bucket mBucket;
final List<View> mHiddenViews;
ChildHelper(Callback callback) {
mCallback = callback;
mBucket = new Bucket();
mHiddenViews = new ArrayList<View>();
}
/**
* Adds a view to the ViewGroup
*
* @param child View to add.
* @param hidden If set to true, this item will be invisible from regular methods.
*/
void addView(View child, boolean hidden) {
addView(child, -1, hidden);
}
/**
* Add a view to the ViewGroup at an index
*
* @param child View to add.
* @param index Index of the child from the regular perspective (excluding hidden views).
* ChildHelper offsets this index to actual ViewGroup index.
* @param hidden If set to true, this item will be invisible from regular methods.
*/
void addView(View child, int index, boolean hidden) {
final int offset;
if (index < 0) {
offset = mCallback.getChildCount();
} else {
offset = getOffset(index);
}
mBucket.insert(offset, hidden);
if (hidden) {
mHiddenViews.add(child);
}
mCallback.addView(child, offset);
if (DEBUG) {
Log.d(TAG, "addViewAt " + index + ",h:" + hidden + ", " + this);
}
}
private int getOffset(int index) {
if (index < 0) {
return -1; //anything below 0 won't work as diff will be undefined.
}
final int limit = mCallback.getChildCount();
int offset = index;
while (offset < limit) {
final int removedBefore = mBucket.countOnesBefore(offset);
final int diff = index - (offset - removedBefore);
if (diff == 0) {
while (mBucket.get(offset)) { // ensure this offset is not hidden
offset ++;
}
return offset;
} else {
offset += diff;
}
}
return -1;
}
/**
* Removes the provided View from underlying RecyclerView.
*
* @param view The view to remove.
*/
void removeView(View view) {
int index = mCallback.indexOfChild(view);
if (index < 0) {
return;
}
if (mBucket.remove(index)) {
mHiddenViews.remove(view);
}
mCallback.removeViewAt(index);
if (DEBUG) {
Log.d(TAG, "remove View off:" + index + "," + this);
}
}
/**
* Removes the view at the provided index from RecyclerView.
*
* @param index Index of the child from the regular perspective (excluding hidden views).
* ChildHelper offsets this index to actual ViewGroup index.
*/
void removeViewAt(int index) {
final int offset = getOffset(index);
final View view = mCallback.getChildAt(offset);
if (view == null) {
return;
}
if (mBucket.remove(offset)) {
mHiddenViews.remove(view);
}
mCallback.removeViewAt(offset);
if (DEBUG) {
Log.d(TAG, "removeViewAt " + index + ", off:" + offset + ", " + this);
}
}
/**
* Returns the child at provided index.
*
* @param index Index of the child to return in regular perspective.
*/
View getChildAt(int index) {
final int offset = getOffset(index);
return mCallback.getChildAt(offset);
}
/**
* Removes all views from the ViewGroup including the hidden ones.
*/
void removeAllViewsUnfiltered() {
mBucket.reset();
mHiddenViews.clear();
mCallback.removeAllViews();
if (DEBUG) {
Log.d(TAG, "removeAllViewsUnfiltered");
}
}
/**
* This can be used to find a disappearing view by position.
*
* @param position The adapter position of the item.
* @param type View type, can be {@link RecyclerView#INVALID_TYPE}.
* @return A hidden view with a valid ViewHolder that matches the position and type.
*/
View findHiddenNonRemovedView(int position, int type) {
final int count = mHiddenViews.size();
for (int i = 0; i < count; i++) {
final View view = mHiddenViews.get(i);
RecyclerView.ViewHolder holder = mCallback.getChildViewHolder(view);
if (holder.getLayoutPosition() == position && !holder.isInvalid() &&
(type == RecyclerView.INVALID_TYPE || holder.getItemViewType() == type)) {
return view;
}
}
return null;
}
/**
* Attaches the provided view to the underlying ViewGroup.
*
* @param child Child to attach.
* @param index Index of the child to attach in regular perspective.
* @param layoutParams LayoutParams for the child.
* @param hidden If set to true, this item will be invisible to the regular methods.
*/
void attachViewToParent(View child, int index, ViewGroup.LayoutParams layoutParams,
boolean hidden) {
final int offset;
if (index < 0) {
offset = mCallback.getChildCount();
} else {
offset = getOffset(index);
}
mBucket.insert(offset, hidden);
if (hidden) {
mHiddenViews.add(child);
}
mCallback.attachViewToParent(child, offset, layoutParams);
if (DEBUG) {
Log.d(TAG, "attach view to parent index:" + index + ",off:" + offset + "," +
"h:" + hidden + ", " + this);
}
}
/**
* Returns the number of children that are not hidden.
*
* @return Number of children that are not hidden.
* @see #getChildAt(int)
*/
int getChildCount() {
return mCallback.getChildCount() - mHiddenViews.size();
}
/**
* Returns the total number of children.
*
* @return The total number of children including the hidden views.
* @see #getUnfilteredChildAt(int)
*/
int getUnfilteredChildCount() {
return mCallback.getChildCount();
}
/**
* Returns a child by ViewGroup offset. ChildHelper won't offset this index.
*
* @param index ViewGroup index of the child to return.
* @return The view in the provided index.
*/
View getUnfilteredChildAt(int index) {
return mCallback.getChildAt(index);
}
/**
* Detaches the view at the provided index.
*
* @param index Index of the child to return in regular perspective.
*/
void detachViewFromParent(int index) {
final int offset = getOffset(index);
mBucket.remove(offset);
mCallback.detachViewFromParent(offset);
if (DEBUG) {
Log.d(TAG, "detach view from parent " + index + ", off:" + offset);
}
}
/**
* Returns the index of the child in regular perspective.
*
* @param child The child whose index will be returned.
* @return The regular perspective index of the child or -1 if it does not exists.
*/
int indexOfChild(View child) {
final int index = mCallback.indexOfChild(child);
if (index == -1) {
return -1;
}
if (mBucket.get(index)) {
if (DEBUG) {
throw new IllegalArgumentException("cannot get index of a hidden child");
} else {
return -1;
}
}
// reverse the index
return index - mBucket.countOnesBefore(index);
}
/**
* Returns whether a View is visible to LayoutManager or not.
*
* @param view The child view to check. Should be a child of the Callback.
* @return True if the View is not visible to LayoutManager
*/
boolean isHidden(View view) {
return mHiddenViews.contains(view);
}
/**
* Marks a child view as hidden.
*
* @param view The view to hide.
*/
void hide(View view) {
final int offset = mCallback.indexOfChild(view);
if (offset < 0) {
throw new IllegalArgumentException("view is not a child, cannot hide " + view);
}
if (DEBUG && mBucket.get(offset)) {
throw new RuntimeException("trying to hide same view twice, how come ? " + view);
}
mBucket.set(offset);
mHiddenViews.add(view);
if (DEBUG) {
Log.d(TAG, "hiding child " + view + " at offset " + offset+ ", " + this);
}
}
@Override
public String toString() {
return mBucket.toString() + ", hidden list:" + mHiddenViews.size();
}
/**
* Removes a view from the ViewGroup if it is hidden.
*
* @param view The view to remove.
* @return True if the View is found and it is hidden. False otherwise.
*/
boolean removeViewIfHidden(View view) {
final int index = mCallback.indexOfChild(view);
if (index == -1) {
if (mHiddenViews.remove(view) && DEBUG) {
throw new IllegalStateException("view is in hidden list but not in view group");
}
return true;
}
if (mBucket.get(index)) {
mBucket.remove(index);
if (!mHiddenViews.remove(view) && DEBUG) {
throw new IllegalStateException(
"removed a hidden view but it is not in hidden views list");
}
mCallback.removeViewAt(index);
return true;
}
return false;
}
/**
* Bitset implementation that provides methods to offset indices.
*/
static class Bucket {
final static int BITS_PER_WORD = Long.SIZE;
final static long LAST_BIT = 1L << (Long.SIZE - 1);
long mData = 0;
Bucket next;
void set(int index) {
if (index >= BITS_PER_WORD) {
ensureNext();
next.set(index - BITS_PER_WORD);
} else {
mData |= 1L << index;
}
}
private void ensureNext() {
if (next == null) {
next = new Bucket();
}
}
void clear(int index) {
if (index >= BITS_PER_WORD) {
if (next != null) {
next.clear(index - BITS_PER_WORD);
}
} else {
mData &= ~(1L << index);
}
}
boolean get(int index) {
if (index >= BITS_PER_WORD) {
ensureNext();
return next.get(index - BITS_PER_WORD);
} else {
return (mData & (1L << index)) != 0;
}
}
void reset() {
mData = 0;
if (next != null) {
next.reset();
}
}
void insert(int index, boolean value) {
if (index >= BITS_PER_WORD) {
ensureNext();
next.insert(index - BITS_PER_WORD, value);
} else {
final boolean lastBit = (mData & LAST_BIT) != 0;
long mask = (1L << index) - 1;
final long before = mData & mask;
final long after = ((mData & ~mask)) << 1;
mData = before | after;
if (value) {
set(index);
} else {
clear(index);
}
if (lastBit || next != null) {
ensureNext();
next.insert(0, lastBit);
}
}
}
boolean remove(int index) {
if (index >= BITS_PER_WORD) {
ensureNext();
return next.remove(index - BITS_PER_WORD);
} else {
long mask = (1L << index);
final boolean value = (mData & mask) != 0;
mData &= ~mask;
mask = mask - 1;
final long before = mData & mask;
// cannot use >> because it adds one.
final long after = Long.rotateRight(mData & ~mask, 1);
mData = before | after;
if (next != null) {
if (next.get(0)) {
set(BITS_PER_WORD - 1);
}
next.remove(0);
}
return value;
}
}
int countOnesBefore(int index) {
if (next == null) {
if (index >= BITS_PER_WORD) {
return Long.bitCount(mData);
}
return Long.bitCount(mData & ((1L << index) - 1));
}
if (index < BITS_PER_WORD) {
return Long.bitCount(mData & ((1L << index) - 1));
} else {
return next.countOnesBefore(index - BITS_PER_WORD) + Long.bitCount(mData);
}
}
@Override
public String toString() {
return next == null ? Long.toBinaryString(mData)
: next.toString() + "xx" + Long.toBinaryString(mData);
}
}
static interface Callback {
int getChildCount();
void addView(View child, int index);
int indexOfChild(View view);
void removeViewAt(int index);
View getChildAt(int offset);
void removeAllViews();
RecyclerView.ViewHolder getChildViewHolder(View view);
void attachViewToParent(View child, int index, ViewGroup.LayoutParams layoutParams);
void detachViewFromParent(int offset);
}
}

View File

@ -0,0 +1,631 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.telegram.android.support.widget;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.ViewPropertyAnimatorCompat;
import android.support.v4.view.ViewPropertyAnimatorListener;
import org.telegram.android.support.widget.RecyclerView.ViewHolder;
import android.view.View;
import java.util.ArrayList;
import java.util.List;
/**
* This implementation of {@link RecyclerView.ItemAnimator} provides basic
* animations on remove, add, and move events that happen to the items in
* a RecyclerView. RecyclerView uses a DefaultItemAnimator by default.
*
* @see RecyclerView#setItemAnimator(RecyclerView.ItemAnimator)
*/
public class DefaultItemAnimator extends RecyclerView.ItemAnimator {
private static final boolean DEBUG = false;
private ArrayList<ViewHolder> mPendingRemovals = new ArrayList<ViewHolder>();
private ArrayList<ViewHolder> mPendingAdditions = new ArrayList<ViewHolder>();
private ArrayList<MoveInfo> mPendingMoves = new ArrayList<MoveInfo>();
private ArrayList<ChangeInfo> mPendingChanges = new ArrayList<ChangeInfo>();
private ArrayList<ArrayList<ViewHolder>> mAdditionsList =
new ArrayList<ArrayList<ViewHolder>>();
private ArrayList<ArrayList<MoveInfo>> mMovesList = new ArrayList<ArrayList<MoveInfo>>();
private ArrayList<ArrayList<ChangeInfo>> mChangesList = new ArrayList<ArrayList<ChangeInfo>>();
private ArrayList<ViewHolder> mAddAnimations = new ArrayList<ViewHolder>();
private ArrayList<ViewHolder> mMoveAnimations = new ArrayList<ViewHolder>();
private ArrayList<ViewHolder> mRemoveAnimations = new ArrayList<ViewHolder>();
private ArrayList<ViewHolder> mChangeAnimations = new ArrayList<ViewHolder>();
private static class MoveInfo {
public ViewHolder holder;
public int fromX, fromY, toX, toY;
private MoveInfo(ViewHolder holder, int fromX, int fromY, int toX, int toY) {
this.holder = holder;
this.fromX = fromX;
this.fromY = fromY;
this.toX = toX;
this.toY = toY;
}
}
private static class ChangeInfo {
public ViewHolder oldHolder, newHolder;
public int fromX, fromY, toX, toY;
private ChangeInfo(ViewHolder oldHolder, ViewHolder newHolder) {
this.oldHolder = oldHolder;
this.newHolder = newHolder;
}
private ChangeInfo(ViewHolder oldHolder, ViewHolder newHolder,
int fromX, int fromY, int toX, int toY) {
this(oldHolder, newHolder);
this.fromX = fromX;
this.fromY = fromY;
this.toX = toX;
this.toY = toY;
}
@Override
public String toString() {
return "ChangeInfo{" +
"oldHolder=" + oldHolder +
", newHolder=" + newHolder +
", fromX=" + fromX +
", fromY=" + fromY +
", toX=" + toX +
", toY=" + toY +
'}';
}
}
@Override
public void runPendingAnimations() {
boolean removalsPending = !mPendingRemovals.isEmpty();
boolean movesPending = !mPendingMoves.isEmpty();
boolean changesPending = !mPendingChanges.isEmpty();
boolean additionsPending = !mPendingAdditions.isEmpty();
if (!removalsPending && !movesPending && !additionsPending && !changesPending) {
// nothing to animate
return;
}
// First, remove stuff
for (ViewHolder holder : mPendingRemovals) {
animateRemoveImpl(holder);
}
mPendingRemovals.clear();
// Next, move stuff
if (movesPending) {
final ArrayList<MoveInfo> moves = new ArrayList<MoveInfo>();
moves.addAll(mPendingMoves);
mMovesList.add(moves);
mPendingMoves.clear();
Runnable mover = new Runnable() {
@Override
public void run() {
for (MoveInfo moveInfo : moves) {
animateMoveImpl(moveInfo.holder, moveInfo.fromX, moveInfo.fromY,
moveInfo.toX, moveInfo.toY);
}
moves.clear();
mMovesList.remove(moves);
}
};
if (removalsPending) {
View view = moves.get(0).holder.itemView;
ViewCompat.postOnAnimationDelayed(view, mover, getRemoveDuration());
} else {
mover.run();
}
}
// Next, change stuff, to run in parallel with move animations
if (changesPending) {
final ArrayList<ChangeInfo> changes = new ArrayList<ChangeInfo>();
changes.addAll(mPendingChanges);
mChangesList.add(changes);
mPendingChanges.clear();
Runnable changer = new Runnable() {
@Override
public void run() {
for (ChangeInfo change : changes) {
animateChangeImpl(change);
}
changes.clear();
mChangesList.remove(changes);
}
};
if (removalsPending) {
ViewHolder holder = changes.get(0).oldHolder;
ViewCompat.postOnAnimationDelayed(holder.itemView, changer, getRemoveDuration());
} else {
changer.run();
}
}
// Next, add stuff
if (additionsPending) {
final ArrayList<ViewHolder> additions = new ArrayList<ViewHolder>();
additions.addAll(mPendingAdditions);
mAdditionsList.add(additions);
mPendingAdditions.clear();
Runnable adder = new Runnable() {
public void run() {
for (ViewHolder holder : additions) {
animateAddImpl(holder);
}
additions.clear();
mAdditionsList.remove(additions);
}
};
if (removalsPending || movesPending || changesPending) {
long removeDuration = removalsPending ? getRemoveDuration() : 0;
long moveDuration = movesPending ? getMoveDuration() : 0;
long changeDuration = changesPending ? getChangeDuration() : 0;
long totalDelay = removeDuration + Math.max(moveDuration, changeDuration);
View view = additions.get(0).itemView;
ViewCompat.postOnAnimationDelayed(view, adder, totalDelay);
} else {
adder.run();
}
}
}
@Override
public boolean animateRemove(final ViewHolder holder) {
endAnimation(holder);
mPendingRemovals.add(holder);
return true;
}
private void animateRemoveImpl(final ViewHolder holder) {
final View view = holder.itemView;
final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view);
mRemoveAnimations.add(holder);
animation.setDuration(getRemoveDuration())
.alpha(0).setListener(new VpaListenerAdapter() {
@Override
public void onAnimationStart(View view) {
dispatchRemoveStarting(holder);
}
@Override
public void onAnimationEnd(View view) {
animation.setListener(null);
ViewCompat.setAlpha(view, 1);
dispatchRemoveFinished(holder);
mRemoveAnimations.remove(holder);
dispatchFinishedWhenDone();
}
}).start();
}
@Override
public boolean animateAdd(final ViewHolder holder) {
endAnimation(holder);
ViewCompat.setAlpha(holder.itemView, 0);
mPendingAdditions.add(holder);
return true;
}
private void animateAddImpl(final ViewHolder holder) {
final View view = holder.itemView;
final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view);
mAddAnimations.add(holder);
animation.alpha(1).setDuration(getAddDuration()).
setListener(new VpaListenerAdapter() {
@Override
public void onAnimationStart(View view) {
dispatchAddStarting(holder);
}
@Override
public void onAnimationCancel(View view) {
ViewCompat.setAlpha(view, 1);
}
@Override
public void onAnimationEnd(View view) {
animation.setListener(null);
dispatchAddFinished(holder);
mAddAnimations.remove(holder);
dispatchFinishedWhenDone();
}
}).start();
}
@Override
public boolean animateMove(final ViewHolder holder, int fromX, int fromY,
int toX, int toY) {
final View view = holder.itemView;
fromX += ViewCompat.getTranslationX(holder.itemView);
fromY += ViewCompat.getTranslationY(holder.itemView);
endAnimation(holder);
int deltaX = toX - fromX;
int deltaY = toY - fromY;
if (deltaX == 0 && deltaY == 0) {
dispatchMoveFinished(holder);
return false;
}
if (deltaX != 0) {
ViewCompat.setTranslationX(view, -deltaX);
}
if (deltaY != 0) {
ViewCompat.setTranslationY(view, -deltaY);
}
mPendingMoves.add(new MoveInfo(holder, fromX, fromY, toX, toY));
return true;
}
private void animateMoveImpl(final ViewHolder holder, int fromX, int fromY, int toX, int toY) {
final View view = holder.itemView;
final int deltaX = toX - fromX;
final int deltaY = toY - fromY;
if (deltaX != 0) {
ViewCompat.animate(view).translationX(0);
}
if (deltaY != 0) {
ViewCompat.animate(view).translationY(0);
}
// TODO: make EndActions end listeners instead, since end actions aren't called when
// vpas are canceled (and can't end them. why?)
// need listener functionality in VPACompat for this. Ick.
final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view);
mMoveAnimations.add(holder);
animation.setDuration(getMoveDuration()).setListener(new VpaListenerAdapter() {
@Override
public void onAnimationStart(View view) {
dispatchMoveStarting(holder);
}
@Override
public void onAnimationCancel(View view) {
if (deltaX != 0) {
ViewCompat.setTranslationX(view, 0);
}
if (deltaY != 0) {
ViewCompat.setTranslationY(view, 0);
}
}
@Override
public void onAnimationEnd(View view) {
animation.setListener(null);
dispatchMoveFinished(holder);
mMoveAnimations.remove(holder);
dispatchFinishedWhenDone();
}
}).start();
}
@Override
public boolean animateChange(ViewHolder oldHolder, ViewHolder newHolder,
int fromX, int fromY, int toX, int toY) {
final float prevTranslationX = ViewCompat.getTranslationX(oldHolder.itemView);
final float prevTranslationY = ViewCompat.getTranslationY(oldHolder.itemView);
final float prevAlpha = ViewCompat.getAlpha(oldHolder.itemView);
endAnimation(oldHolder);
int deltaX = (int) (toX - fromX - prevTranslationX);
int deltaY = (int) (toY - fromY - prevTranslationY);
// recover prev translation state after ending animation
ViewCompat.setTranslationX(oldHolder.itemView, prevTranslationX);
ViewCompat.setTranslationY(oldHolder.itemView, prevTranslationY);
ViewCompat.setAlpha(oldHolder.itemView, prevAlpha);
if (newHolder != null && newHolder.itemView != null) {
// carry over translation values
endAnimation(newHolder);
ViewCompat.setTranslationX(newHolder.itemView, -deltaX);
ViewCompat.setTranslationY(newHolder.itemView, -deltaY);
ViewCompat.setAlpha(newHolder.itemView, 0);
}
mPendingChanges.add(new ChangeInfo(oldHolder, newHolder, fromX, fromY, toX, toY));
return true;
}
private void animateChangeImpl(final ChangeInfo changeInfo) {
final ViewHolder holder = changeInfo.oldHolder;
final View view = holder == null ? null : holder.itemView;
final ViewHolder newHolder = changeInfo.newHolder;
final View newView = newHolder != null ? newHolder.itemView : null;
if (view != null) {
final ViewPropertyAnimatorCompat oldViewAnim = ViewCompat.animate(view).setDuration(
getChangeDuration());
mChangeAnimations.add(changeInfo.oldHolder);
oldViewAnim.translationX(changeInfo.toX - changeInfo.fromX);
oldViewAnim.translationY(changeInfo.toY - changeInfo.fromY);
oldViewAnim.alpha(0).setListener(new VpaListenerAdapter() {
@Override
public void onAnimationStart(View view) {
dispatchChangeStarting(changeInfo.oldHolder, true);
}
@Override
public void onAnimationEnd(View view) {
oldViewAnim.setListener(null);
ViewCompat.setAlpha(view, 1);
ViewCompat.setTranslationX(view, 0);
ViewCompat.setTranslationY(view, 0);
dispatchChangeFinished(changeInfo.oldHolder, true);
mChangeAnimations.remove(changeInfo.oldHolder);
dispatchFinishedWhenDone();
}
}).start();
}
if (newView != null) {
final ViewPropertyAnimatorCompat newViewAnimation = ViewCompat.animate(newView);
mChangeAnimations.add(changeInfo.newHolder);
newViewAnimation.translationX(0).translationY(0).setDuration(getChangeDuration()).
alpha(1).setListener(new VpaListenerAdapter() {
@Override
public void onAnimationStart(View view) {
dispatchChangeStarting(changeInfo.newHolder, false);
}
@Override
public void onAnimationEnd(View view) {
newViewAnimation.setListener(null);
ViewCompat.setAlpha(newView, 1);
ViewCompat.setTranslationX(newView, 0);
ViewCompat.setTranslationY(newView, 0);
dispatchChangeFinished(changeInfo.newHolder, false);
mChangeAnimations.remove(changeInfo.newHolder);
dispatchFinishedWhenDone();
}
}).start();
}
}
private void endChangeAnimation(List<ChangeInfo> infoList, ViewHolder item) {
for (int i = infoList.size() - 1; i >= 0; i--) {
ChangeInfo changeInfo = infoList.get(i);
if (endChangeAnimationIfNecessary(changeInfo, item)) {
if (changeInfo.oldHolder == null && changeInfo.newHolder == null) {
infoList.remove(changeInfo);
}
}
}
}
private void endChangeAnimationIfNecessary(ChangeInfo changeInfo) {
if (changeInfo.oldHolder != null) {
endChangeAnimationIfNecessary(changeInfo, changeInfo.oldHolder);
}
if (changeInfo.newHolder != null) {
endChangeAnimationIfNecessary(changeInfo, changeInfo.newHolder);
}
}
private boolean endChangeAnimationIfNecessary(ChangeInfo changeInfo, ViewHolder item) {
boolean oldItem = false;
if (changeInfo.newHolder == item) {
changeInfo.newHolder = null;
} else if (changeInfo.oldHolder == item) {
changeInfo.oldHolder = null;
oldItem = true;
} else {
return false;
}
ViewCompat.setAlpha(item.itemView, 1);
ViewCompat.setTranslationX(item.itemView, 0);
ViewCompat.setTranslationY(item.itemView, 0);
dispatchChangeFinished(item, oldItem);
return true;
}
@Override
public void endAnimation(ViewHolder item) {
final View view = item.itemView;
// this will trigger end callback which should set properties to their target values.
ViewCompat.animate(view).cancel();
// TODO if some other animations are chained to end, how do we cancel them as well?
for (int i = mPendingMoves.size() - 1; i >= 0; i--) {
MoveInfo moveInfo = mPendingMoves.get(i);
if (moveInfo.holder == item) {
ViewCompat.setTranslationY(view, 0);
ViewCompat.setTranslationX(view, 0);
dispatchMoveFinished(item);
mPendingMoves.remove(i);
}
}
endChangeAnimation(mPendingChanges, item);
if (mPendingRemovals.remove(item)) {
ViewCompat.setAlpha(view, 1);
dispatchRemoveFinished(item);
}
if (mPendingAdditions.remove(item)) {
ViewCompat.setAlpha(view, 1);
dispatchAddFinished(item);
}
for (int i = mChangesList.size() - 1; i >= 0; i--) {
ArrayList<ChangeInfo> changes = mChangesList.get(i);
endChangeAnimation(changes, item);
if (changes.isEmpty()) {
mChangesList.remove(i);
}
}
for (int i = mMovesList.size() - 1; i >= 0; i--) {
ArrayList<MoveInfo> moves = mMovesList.get(i);
for (int j = moves.size() - 1; j >= 0; j--) {
MoveInfo moveInfo = moves.get(j);
if (moveInfo.holder == item) {
ViewCompat.setTranslationY(view, 0);
ViewCompat.setTranslationX(view, 0);
dispatchMoveFinished(item);
moves.remove(j);
if (moves.isEmpty()) {
mMovesList.remove(i);
}
break;
}
}
}
for (int i = mAdditionsList.size() - 1; i >= 0; i--) {
ArrayList<ViewHolder> additions = mAdditionsList.get(i);
if (additions.remove(item)) {
ViewCompat.setAlpha(view, 1);
dispatchAddFinished(item);
if (additions.isEmpty()) {
mAdditionsList.remove(i);
}
}
}
// animations should be ended by the cancel above.
if (mRemoveAnimations.remove(item) && DEBUG) {
throw new IllegalStateException("after animation is cancelled, item should not be in "
+ "mRemoveAnimations list");
}
if (mAddAnimations.remove(item) && DEBUG) {
throw new IllegalStateException("after animation is cancelled, item should not be in "
+ "mAddAnimations list");
}
if (mChangeAnimations.remove(item) && DEBUG) {
throw new IllegalStateException("after animation is cancelled, item should not be in "
+ "mChangeAnimations list");
}
if (mMoveAnimations.remove(item) && DEBUG) {
throw new IllegalStateException("after animation is cancelled, item should not be in "
+ "mMoveAnimations list");
}
dispatchFinishedWhenDone();
}
@Override
public boolean isRunning() {
return (!mPendingAdditions.isEmpty() ||
!mPendingChanges.isEmpty() ||
!mPendingMoves.isEmpty() ||
!mPendingRemovals.isEmpty() ||
!mMoveAnimations.isEmpty() ||
!mRemoveAnimations.isEmpty() ||
!mAddAnimations.isEmpty() ||
!mChangeAnimations.isEmpty() ||
!mMovesList.isEmpty() ||
!mAdditionsList.isEmpty() ||
!mChangesList.isEmpty());
}
/**
* Check the state of currently pending and running animations. If there are none
* pending/running, call {@link #dispatchAnimationsFinished()} to notify any
* listeners.
*/
private void dispatchFinishedWhenDone() {
if (!isRunning()) {
dispatchAnimationsFinished();
}
}
@Override
public void endAnimations() {
int count = mPendingMoves.size();
for (int i = count - 1; i >= 0; i--) {
MoveInfo item = mPendingMoves.get(i);
View view = item.holder.itemView;
ViewCompat.setTranslationY(view, 0);
ViewCompat.setTranslationX(view, 0);
dispatchMoveFinished(item.holder);
mPendingMoves.remove(i);
}
count = mPendingRemovals.size();
for (int i = count - 1; i >= 0; i--) {
ViewHolder item = mPendingRemovals.get(i);
dispatchRemoveFinished(item);
mPendingRemovals.remove(i);
}
count = mPendingAdditions.size();
for (int i = count - 1; i >= 0; i--) {
ViewHolder item = mPendingAdditions.get(i);
View view = item.itemView;
ViewCompat.setAlpha(view, 1);
dispatchAddFinished(item);
mPendingAdditions.remove(i);
}
count = mPendingChanges.size();
for (int i = count - 1; i >= 0; i--) {
endChangeAnimationIfNecessary(mPendingChanges.get(i));
}
mPendingChanges.clear();
if (!isRunning()) {
return;
}
int listCount = mMovesList.size();
for (int i = listCount - 1; i >= 0; i--) {
ArrayList<MoveInfo> moves = mMovesList.get(i);
count = moves.size();
for (int j = count - 1; j >= 0; j--) {
MoveInfo moveInfo = moves.get(j);
ViewHolder item = moveInfo.holder;
View view = item.itemView;
ViewCompat.setTranslationY(view, 0);
ViewCompat.setTranslationX(view, 0);
dispatchMoveFinished(moveInfo.holder);
moves.remove(j);
if (moves.isEmpty()) {
mMovesList.remove(moves);
}
}
}
listCount = mAdditionsList.size();
for (int i = listCount - 1; i >= 0; i--) {
ArrayList<ViewHolder> additions = mAdditionsList.get(i);
count = additions.size();
for (int j = count - 1; j >= 0; j--) {
ViewHolder item = additions.get(j);
View view = item.itemView;
ViewCompat.setAlpha(view, 1);
dispatchAddFinished(item);
additions.remove(j);
if (additions.isEmpty()) {
mAdditionsList.remove(additions);
}
}
}
listCount = mChangesList.size();
for (int i = listCount - 1; i >= 0; i--) {
ArrayList<ChangeInfo> changes = mChangesList.get(i);
count = changes.size();
for (int j = count - 1; j >= 0; j--) {
endChangeAnimationIfNecessary(changes.get(j));
if (changes.isEmpty()) {
mChangesList.remove(changes);
}
}
}
cancelAll(mRemoveAnimations);
cancelAll(mMoveAnimations);
cancelAll(mAddAnimations);
cancelAll(mChangeAnimations);
dispatchAnimationsFinished();
}
void cancelAll(List<ViewHolder> viewHolders) {
for (int i = viewHolders.size() - 1; i >= 0; i--) {
ViewCompat.animate(viewHolders.get(i).itemView).cancel();
}
}
private static class VpaListenerAdapter implements ViewPropertyAnimatorListener {
@Override
public void onAnimationStart(View view) {}
@Override
public void onAnimationEnd(View view) {}
@Override
public void onAnimationCancel(View view) {}
};
}

View File

@ -0,0 +1,887 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific languag`e governing permissions and
* limitations under the License.
*/
package org.telegram.android.support.widget;
import android.content.Context;
import android.graphics.Rect;
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
import android.util.AttributeSet;
import android.util.Log;
import android.util.SparseIntArray;
import android.view.View;
import android.view.ViewGroup;
import java.util.Arrays;
/**
* A {@link RecyclerView.LayoutManager} implementations that lays out items in a grid.
* <p>
* By default, each item occupies 1 span. You can change it by providing a custom
* {@link SpanSizeLookup} instance via {@link #setSpanSizeLookup(SpanSizeLookup)}.
*/
public class GridLayoutManager extends LinearLayoutManager {
private static final boolean DEBUG = false;
private static final String TAG = "GridLayoutManager";
public static final int DEFAULT_SPAN_COUNT = -1;
/**
* The measure spec for the scroll direction.
*/
static final int MAIN_DIR_SPEC =
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
/**
* Span size have been changed but we've not done a new layout calculation.
*/
boolean mPendingSpanCountChange = false;
int mSpanCount = DEFAULT_SPAN_COUNT;
/**
* Right borders for each span.
* <p>For <b>i-th</b> item start is {@link #mCachedBorders}[i-1] + 1
* and end is {@link #mCachedBorders}[i].
*/
int [] mCachedBorders;
/**
* Temporary array to keep views in layoutChunk method
*/
View[] mSet;
final SparseIntArray mPreLayoutSpanSizeCache = new SparseIntArray();
final SparseIntArray mPreLayoutSpanIndexCache = new SparseIntArray();
SpanSizeLookup mSpanSizeLookup = new DefaultSpanSizeLookup();
// re-used variable to acquire decor insets from RecyclerView
final Rect mDecorInsets = new Rect();
/**
* Creates a vertical GridLayoutManager
*
* @param context Current context, will be used to access resources.
* @param spanCount The number of columns in the grid
*/
public GridLayoutManager(Context context, int spanCount) {
super(context);
setSpanCount(spanCount);
}
/**
* @param context Current context, will be used to access resources.
* @param spanCount The number of columns or rows in the grid
* @param orientation Layout orientation. Should be {@link #HORIZONTAL} or {@link
* #VERTICAL}.
* @param reverseLayout When set to true, layouts from end to start.
*/
public GridLayoutManager(Context context, int spanCount, int orientation,
boolean reverseLayout) {
super(context, orientation, reverseLayout);
setSpanCount(spanCount);
}
/**
* stackFromEnd is not supported by GridLayoutManager. Consider using
* {@link #setReverseLayout(boolean)}.
*/
@Override
public void setStackFromEnd(boolean stackFromEnd) {
if (stackFromEnd) {
throw new UnsupportedOperationException(
"GridLayoutManager does not support stack from end."
+ " Consider using reverse layout");
}
super.setStackFromEnd(false);
}
@Override
public int getRowCountForAccessibility(RecyclerView.Recycler recycler,
RecyclerView.State state) {
if (mOrientation == HORIZONTAL) {
return mSpanCount;
}
if (state.getItemCount() < 1) {
return 0;
}
return getSpanGroupIndex(recycler, state, state.getItemCount() - 1);
}
@Override
public int getColumnCountForAccessibility(RecyclerView.Recycler recycler,
RecyclerView.State state) {
if (mOrientation == VERTICAL) {
return mSpanCount;
}
if (state.getItemCount() < 1) {
return 0;
}
return getSpanGroupIndex(recycler, state, state.getItemCount() - 1);
}
@Override
public void onInitializeAccessibilityNodeInfoForItem(RecyclerView.Recycler recycler,
RecyclerView.State state, View host, AccessibilityNodeInfoCompat info) {
ViewGroup.LayoutParams lp = host.getLayoutParams();
if (!(lp instanceof LayoutParams)) {
super.onInitializeAccessibilityNodeInfoForItem(host, info);
return;
}
LayoutParams glp = (LayoutParams) lp;
int spanGroupIndex = getSpanGroupIndex(recycler, state, glp.getViewLayoutPosition());
if (mOrientation == HORIZONTAL) {
info.setCollectionItemInfo(AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain(
glp.getSpanIndex(), glp.getSpanSize(),
spanGroupIndex, 1,
mSpanCount > 1 && glp.getSpanSize() == mSpanCount, false));
} else { // VERTICAL
info.setCollectionItemInfo(AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain(
spanGroupIndex , 1,
glp.getSpanIndex(), glp.getSpanSize(),
mSpanCount > 1 && glp.getSpanSize() == mSpanCount, false));
}
}
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
if (state.isPreLayout()) {
cachePreLayoutSpanMapping();
}
super.onLayoutChildren(recycler, state);
if (DEBUG) {
validateChildOrder();
}
clearPreLayoutSpanMappingCache();
if (!state.isPreLayout()) {
mPendingSpanCountChange = false;
}
}
private void clearPreLayoutSpanMappingCache() {
mPreLayoutSpanSizeCache.clear();
mPreLayoutSpanIndexCache.clear();
}
private void cachePreLayoutSpanMapping() {
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams();
final int viewPosition = lp.getViewLayoutPosition();
mPreLayoutSpanSizeCache.put(viewPosition, lp.getSpanSize());
mPreLayoutSpanIndexCache.put(viewPosition, lp.getSpanIndex());
}
}
@Override
public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) {
mSpanSizeLookup.invalidateSpanIndexCache();
}
@Override
public void onItemsChanged(RecyclerView recyclerView) {
mSpanSizeLookup.invalidateSpanIndexCache();
}
@Override
public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) {
mSpanSizeLookup.invalidateSpanIndexCache();
}
@Override
public void onItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount) {
mSpanSizeLookup.invalidateSpanIndexCache();
}
@Override
public void onItemsMoved(RecyclerView recyclerView, int from, int to, int itemCount) {
mSpanSizeLookup.invalidateSpanIndexCache();
}
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
}
@Override
public RecyclerView.LayoutParams generateLayoutParams(Context c, AttributeSet attrs) {
return new LayoutParams(c, attrs);
}
@Override
public RecyclerView.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
if (lp instanceof ViewGroup.MarginLayoutParams) {
return new LayoutParams((ViewGroup.MarginLayoutParams) lp);
} else {
return new LayoutParams(lp);
}
}
@Override
public boolean checkLayoutParams(RecyclerView.LayoutParams lp) {
return lp instanceof LayoutParams;
}
/**
* Sets the source to get the number of spans occupied by each item in the adapter.
*
* @param spanSizeLookup {@link SpanSizeLookup} instance to be used to query number of spans
* occupied by each item
*/
public void setSpanSizeLookup(SpanSizeLookup spanSizeLookup) {
mSpanSizeLookup = spanSizeLookup;
}
/**
* Returns the current {@link SpanSizeLookup} used by the GridLayoutManager.
*
* @return The current {@link SpanSizeLookup} used by the GridLayoutManager.
*/
public SpanSizeLookup getSpanSizeLookup() {
return mSpanSizeLookup;
}
private void updateMeasurements() {
int totalSpace;
if (getOrientation() == VERTICAL) {
totalSpace = getWidth() - getPaddingRight() - getPaddingLeft();
} else {
totalSpace = getHeight() - getPaddingBottom() - getPaddingTop();
}
calculateItemBorders(totalSpace);
}
private void calculateItemBorders(int totalSpace) {
if (mCachedBorders == null || mCachedBorders.length != mSpanCount + 1
|| mCachedBorders[mCachedBorders.length - 1] != totalSpace) {
mCachedBorders = new int[mSpanCount + 1];
}
mCachedBorders[0] = 0;
int sizePerSpan = totalSpace / mSpanCount;
int sizePerSpanRemainder = totalSpace % mSpanCount;
int consumedPixels = 0;
int additionalSize = 0;
for (int i = 1; i <= mSpanCount; i++) {
int itemSize = sizePerSpan;
additionalSize += sizePerSpanRemainder;
if (additionalSize > 0 && (mSpanCount - additionalSize) < sizePerSpanRemainder) {
itemSize += 1;
additionalSize -= mSpanCount;
}
consumedPixels += itemSize;
mCachedBorders[i] = consumedPixels;
}
}
@Override
void onAnchorReady(RecyclerView.State state, AnchorInfo anchorInfo) {
super.onAnchorReady(state, anchorInfo);
updateMeasurements();
if (state.getItemCount() > 0 && !state.isPreLayout()) {
ensureAnchorIsInFirstSpan(anchorInfo);
}
if (mSet == null || mSet.length != mSpanCount) {
mSet = new View[mSpanCount];
}
}
private void ensureAnchorIsInFirstSpan(AnchorInfo anchorInfo) {
int span = mSpanSizeLookup.getCachedSpanIndex(anchorInfo.mPosition, mSpanCount);
while (span > 0 && anchorInfo.mPosition > 0) {
anchorInfo.mPosition--;
span = mSpanSizeLookup.getCachedSpanIndex(anchorInfo.mPosition, mSpanCount);
}
}
@Override
View findReferenceChild(int start, int end, int itemCount) {
ensureLayoutState();
View invalidMatch = null;
View outOfBoundsMatch = null;
final int boundsStart = mOrientationHelper.getStartAfterPadding();
final int boundsEnd = mOrientationHelper.getEndAfterPadding();
final int diff = end > start ? 1 : -1;
for (int i = start; i != end; i += diff) {
final View view = getChildAt(i);
final int position = getPosition(view);
if (position >= 0 && position < itemCount) {
final int span = mSpanSizeLookup.getCachedSpanIndex(position, mSpanCount);
if (span != 0) {
continue;
}
if (((RecyclerView.LayoutParams) view.getLayoutParams()).isItemRemoved()) {
if (invalidMatch == null) {
invalidMatch = view; // removed item, least preferred
}
} else if (mOrientationHelper.getDecoratedStart(view) >= boundsEnd ||
mOrientationHelper.getDecoratedEnd(view) < boundsStart) {
if (outOfBoundsMatch == null) {
outOfBoundsMatch = view; // item is not visible, less preferred
}
} else {
return view;
}
}
}
return outOfBoundsMatch != null ? outOfBoundsMatch : invalidMatch;
}
private int getSpanGroupIndex(RecyclerView.Recycler recycler, RecyclerView.State state,
int viewPosition) {
if (!state.isPreLayout()) {
return mSpanSizeLookup.getSpanGroupIndex(viewPosition, mSpanCount);
}
final int adapterPosition = recycler.convertPreLayoutPositionToPostLayout(viewPosition);
if (adapterPosition == -1) {
if (DEBUG) {
throw new RuntimeException("Cannot find span group index for position "
+ viewPosition);
}
Log.w(TAG, "Cannot find span size for pre layout position. " + viewPosition);
return 0;
}
return mSpanSizeLookup.getSpanGroupIndex(adapterPosition, mSpanCount);
}
private int getSpanIndex(RecyclerView.Recycler recycler, RecyclerView.State state, int pos) {
if (!state.isPreLayout()) {
return mSpanSizeLookup.getCachedSpanIndex(pos, mSpanCount);
}
final int cached = mPreLayoutSpanIndexCache.get(pos, -1);
if (cached != -1) {
return cached;
}
final int adapterPosition = recycler.convertPreLayoutPositionToPostLayout(pos);
if (adapterPosition == -1) {
if (DEBUG) {
throw new RuntimeException("Cannot find span index for pre layout position. It is"
+ " not cached, not in the adapter. Pos:" + pos);
}
Log.w(TAG, "Cannot find span size for pre layout position. It is"
+ " not cached, not in the adapter. Pos:" + pos);
return 0;
}
return mSpanSizeLookup.getCachedSpanIndex(adapterPosition, mSpanCount);
}
private int getSpanSize(RecyclerView.Recycler recycler, RecyclerView.State state, int pos) {
if (!state.isPreLayout()) {
return mSpanSizeLookup.getSpanSize(pos);
}
final int cached = mPreLayoutSpanSizeCache.get(pos, -1);
if (cached != -1) {
return cached;
}
final int adapterPosition = recycler.convertPreLayoutPositionToPostLayout(pos);
if (adapterPosition == -1) {
if (DEBUG) {
throw new RuntimeException("Cannot find span size for pre layout position. It is"
+ " not cached, not in the adapter. Pos:" + pos);
}
Log.w(TAG, "Cannot find span size for pre layout position. It is"
+ " not cached, not in the adapter. Pos:" + pos);
return 1;
}
return mSpanSizeLookup.getSpanSize(adapterPosition);
}
@Override
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
final boolean layingOutInPrimaryDirection =
layoutState.mItemDirection == LayoutState.ITEM_DIRECTION_TAIL;
int count = 0;
int consumedSpanCount = 0;
int remainingSpan = mSpanCount;
if (!layingOutInPrimaryDirection) {
int itemSpanIndex = getSpanIndex(recycler, state, layoutState.mCurrentPosition);
int itemSpanSize = getSpanSize(recycler, state, layoutState.mCurrentPosition);
remainingSpan = itemSpanIndex + itemSpanSize;
}
while (count < mSpanCount && layoutState.hasMore(state) && remainingSpan > 0) {
int pos = layoutState.mCurrentPosition;
final int spanSize = getSpanSize(recycler, state, pos);
if (spanSize > mSpanCount) {
throw new IllegalArgumentException("Item at position " + pos + " requires " +
spanSize + " spans but GridLayoutManager has only " + mSpanCount
+ " spans.");
}
remainingSpan -= spanSize;
if (remainingSpan < 0) {
break; // item did not fit into this row or column
}
View view = layoutState.next(recycler);
if (view == null) {
break;
}
consumedSpanCount += spanSize;
mSet[count] = view;
count++;
}
if (count == 0) {
result.mFinished = true;
return;
}
int maxSize = 0;
// we should assign spans before item decor offsets are calculated
assignSpans(recycler, state, count, consumedSpanCount, layingOutInPrimaryDirection);
for (int i = 0; i < count; i++) {
View view = mSet[i];
if (layoutState.mScrapList == null) {
if (layingOutInPrimaryDirection) {
addView(view);
} else {
addView(view, 0);
}
} else {
if (layingOutInPrimaryDirection) {
addDisappearingView(view);
} else {
addDisappearingView(view, 0);
}
}
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
final int spec = View.MeasureSpec.makeMeasureSpec(
mCachedBorders[lp.mSpanIndex + lp.mSpanSize] -
mCachedBorders[lp.mSpanIndex],
View.MeasureSpec.EXACTLY);
if (mOrientation == VERTICAL) {
measureChildWithDecorationsAndMargin(view, spec, getMainDirSpec(lp.height));
} else {
measureChildWithDecorationsAndMargin(view, getMainDirSpec(lp.width), spec);
}
final int size = mOrientationHelper.getDecoratedMeasurement(view);
if (size > maxSize) {
maxSize = size;
}
}
// views that did not measure the maxSize has to be re-measured
final int maxMeasureSpec = getMainDirSpec(maxSize);
for (int i = 0; i < count; i ++) {
final View view = mSet[i];
if (mOrientationHelper.getDecoratedMeasurement(view) != maxSize) {
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
final int spec = View.MeasureSpec.makeMeasureSpec(
mCachedBorders[lp.mSpanIndex + lp.mSpanSize] -
mCachedBorders[lp.mSpanIndex],
View.MeasureSpec.EXACTLY);
if (mOrientation == VERTICAL) {
measureChildWithDecorationsAndMargin(view, spec, maxMeasureSpec);
} else {
measureChildWithDecorationsAndMargin(view, maxMeasureSpec, spec);
}
}
}
result.mConsumed = maxSize;
int left = 0, right = 0, top = 0, bottom = 0;
if (mOrientation == VERTICAL) {
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
bottom = layoutState.mOffset;
top = bottom - maxSize;
} else {
top = layoutState.mOffset;
bottom = top + maxSize;
}
} else {
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
right = layoutState.mOffset;
left = right - maxSize;
} else {
left = layoutState.mOffset;
right = left + maxSize;
}
}
for (int i = 0; i < count; i++) {
View view = mSet[i];
LayoutParams params = (LayoutParams) view.getLayoutParams();
if (mOrientation == VERTICAL) {
left = getPaddingLeft() + mCachedBorders[params.mSpanIndex];
right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
} else {
top = getPaddingTop() + mCachedBorders[params.mSpanIndex];
bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view);
}
// We calculate everything with View's bounding box (which includes decor and margins)
// To calculate correct layout position, we subtract margins.
layoutDecorated(view, left + params.leftMargin, top + params.topMargin,
right - params.rightMargin, bottom - params.bottomMargin);
if (DEBUG) {
Log.d(TAG, "laid out child at position " + getPosition(view) + ", with l:"
+ (left + params.leftMargin) + ", t:" + (top + params.topMargin) + ", r:"
+ (right - params.rightMargin) + ", b:" + (bottom - params.bottomMargin)
+ ", span:" + params.mSpanIndex + ", spanSize:" + params.mSpanSize);
}
// Consume the available space if the view is not removed OR changed
if (params.isItemRemoved() || params.isItemChanged()) {
result.mIgnoreConsumed = true;
}
result.mFocusable |= view.isFocusable();
}
Arrays.fill(mSet, null);
}
private int getMainDirSpec(int dim) {
if (dim < 0) {
return MAIN_DIR_SPEC;
} else {
return View.MeasureSpec.makeMeasureSpec(dim, View.MeasureSpec.EXACTLY);
}
}
private void measureChildWithDecorationsAndMargin(View child, int widthSpec, int heightSpec) {
calculateItemDecorationsForChild(child, mDecorInsets);
RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) child.getLayoutParams();
widthSpec = updateSpecWithExtra(widthSpec, lp.leftMargin + mDecorInsets.left,
lp.rightMargin + mDecorInsets.right);
heightSpec = updateSpecWithExtra(heightSpec, lp.topMargin + mDecorInsets.top,
lp.bottomMargin + mDecorInsets.bottom);
child.measure(widthSpec, heightSpec);
}
private int updateSpecWithExtra(int spec, int startInset, int endInset) {
if (startInset == 0 && endInset == 0) {
return spec;
}
final int mode = View.MeasureSpec.getMode(spec);
if (mode == View.MeasureSpec.AT_MOST || mode == View.MeasureSpec.EXACTLY) {
return View.MeasureSpec.makeMeasureSpec(
View.MeasureSpec.getSize(spec) - startInset - endInset, mode);
}
return spec;
}
private void assignSpans(RecyclerView.Recycler recycler, RecyclerView.State state, int count,
int consumedSpanCount, boolean layingOutInPrimaryDirection) {
int span, spanDiff, start, end, diff;
// make sure we traverse from min position to max position
if (layingOutInPrimaryDirection) {
start = 0;
end = count;
diff = 1;
} else {
start = count - 1;
end = -1;
diff = -1;
}
if (mOrientation == VERTICAL && isLayoutRTL()) { // start from last span
span = mSpanCount - 1;
spanDiff = -1;
} else {
span = 0;
spanDiff = 1;
}
for (int i = start; i != end; i += diff) {
View view = mSet[i];
LayoutParams params = (LayoutParams) view.getLayoutParams();
params.mSpanSize = getSpanSize(recycler, state, getPosition(view));
if (spanDiff == -1 && params.mSpanSize > 1) {
params.mSpanIndex = span - (params.mSpanSize - 1);
} else {
params.mSpanIndex = span;
}
span += spanDiff * params.mSpanSize;
}
}
/**
* Returns the number of spans laid out by this grid.
*
* @return The number of spans
* @see #setSpanCount(int)
*/
public int getSpanCount() {
return mSpanCount;
}
/**
* Sets the number of spans to be laid out.
* <p>
* If {@link #getOrientation()} is {@link #VERTICAL}, this is the number of columns.
* If {@link #getOrientation()} is {@link #HORIZONTAL}, this is the number of rows.
*
* @param spanCount The total number of spans in the grid
* @see #getSpanCount()
*/
public void setSpanCount(int spanCount) {
if (spanCount == mSpanCount) {
return;
}
mPendingSpanCountChange = true;
if (spanCount < 1) {
throw new IllegalArgumentException("Span count should be at least 1. Provided "
+ spanCount);
}
mSpanCount = spanCount;
mSpanSizeLookup.invalidateSpanIndexCache();
}
/**
* A helper class to provide the number of spans each item occupies.
* <p>
* Default implementation sets each item to occupy exactly 1 span.
*
* @see GridLayoutManager#setSpanSizeLookup(SpanSizeLookup)
*/
public static abstract class SpanSizeLookup {
final SparseIntArray mSpanIndexCache = new SparseIntArray();
private boolean mCacheSpanIndices = false;
/**
* Returns the number of span occupied by the item at <code>position</code>.
*
* @param position The adapter position of the item
* @return The number of spans occupied by the item at the provided position
*/
abstract public int getSpanSize(int position);
/**
* Sets whether the results of {@link #getSpanIndex(int, int)} method should be cached or
* not. By default these values are not cached. If you are not overriding
* {@link #getSpanIndex(int, int)}, you should set this to true for better performance.
*
* @param cacheSpanIndices Whether results of getSpanIndex should be cached or not.
*/
public void setSpanIndexCacheEnabled(boolean cacheSpanIndices) {
mCacheSpanIndices = cacheSpanIndices;
}
/**
* Clears the span index cache. GridLayoutManager automatically calls this method when
* adapter changes occur.
*/
public void invalidateSpanIndexCache() {
mSpanIndexCache.clear();
}
/**
* Returns whether results of {@link #getSpanIndex(int, int)} method are cached or not.
*
* @return True if results of {@link #getSpanIndex(int, int)} are cached.
*/
public boolean isSpanIndexCacheEnabled() {
return mCacheSpanIndices;
}
int getCachedSpanIndex(int position, int spanCount) {
if (!mCacheSpanIndices) {
return getSpanIndex(position, spanCount);
}
final int existing = mSpanIndexCache.get(position, -1);
if (existing != -1) {
return existing;
}
final int value = getSpanIndex(position, spanCount);
mSpanIndexCache.put(position, value);
return value;
}
/**
* Returns the final span index of the provided position.
* <p>
* If you have a faster way to calculate span index for your items, you should override
* this method. Otherwise, you should enable span index cache
* ({@link #setSpanIndexCacheEnabled(boolean)}) for better performance. When caching is
* disabled, default implementation traverses all items from 0 to
* <code>position</code>. When caching is enabled, it calculates from the closest cached
* value before the <code>position</code>.
* <p>
* If you override this method, you need to make sure it is consistent with
* {@link #getSpanSize(int)}. GridLayoutManager does not call this method for
* each item. It is called only for the reference item and rest of the items
* are assigned to spans based on the reference item. For example, you cannot assign a
* position to span 2 while span 1 is empty.
* <p>
* Note that span offsets always start with 0 and are not affected by RTL.
*
* @param position The position of the item
* @param spanCount The total number of spans in the grid
* @return The final span position of the item. Should be between 0 (inclusive) and
* <code>spanCount</code>(exclusive)
*/
public int getSpanIndex(int position, int spanCount) {
int positionSpanSize = getSpanSize(position);
if (positionSpanSize == spanCount) {
return 0; // quick return for full-span items
}
int span = 0;
int startPos = 0;
// If caching is enabled, try to jump
if (mCacheSpanIndices && mSpanIndexCache.size() > 0) {
int prevKey = findReferenceIndexFromCache(position);
if (prevKey >= 0) {
span = mSpanIndexCache.get(prevKey) + getSpanSize(prevKey);
startPos = prevKey + 1;
}
}
for (int i = startPos; i < position; i++) {
int size = getSpanSize(i);
span += size;
if (span == spanCount) {
span = 0;
} else if (span > spanCount) {
// did not fit, moving to next row / column
span = size;
}
}
if (span + positionSpanSize <= spanCount) {
return span;
}
return 0;
}
int findReferenceIndexFromCache(int position) {
int lo = 0;
int hi = mSpanIndexCache.size() - 1;
while (lo <= hi) {
final int mid = (lo + hi) >>> 1;
final int midVal = mSpanIndexCache.keyAt(mid);
if (midVal < position) {
lo = mid + 1;
} else {
hi = mid - 1;
}
}
int index = lo - 1;
if (index >= 0 && index < mSpanIndexCache.size()) {
return mSpanIndexCache.keyAt(index);
}
return -1;
}
/**
* Returns the index of the group this position belongs.
* <p>
* For example, if grid has 3 columns and each item occupies 1 span, span group index
* for item 1 will be 0, item 5 will be 1.
*
* @param adapterPosition The position in adapter
* @param spanCount The total number of spans in the grid
* @return The index of the span group including the item at the given adapter position
*/
public int getSpanGroupIndex(int adapterPosition, int spanCount) {
int span = 0;
int group = 0;
int positionSpanSize = getSpanSize(adapterPosition);
for (int i = 0; i < adapterPosition; i++) {
int size = getSpanSize(i);
span += size;
if (span == spanCount) {
span = 0;
group++;
} else if (span > spanCount) {
// did not fit, moving to next row / column
span = size;
group++;
}
}
if (span + positionSpanSize > spanCount) {
group++;
}
return group;
}
}
@Override
public boolean supportsPredictiveItemAnimations() {
return mPendingSavedState == null && !mPendingSpanCountChange;
}
/**
* Default implementation for {@link SpanSizeLookup}. Each item occupies 1 span.
*/
public static final class DefaultSpanSizeLookup extends SpanSizeLookup {
@Override
public int getSpanSize(int position) {
return 1;
}
@Override
public int getSpanIndex(int position, int spanCount) {
return position % spanCount;
}
}
/**
* LayoutParams used by GridLayoutManager.
* <p>
* Note that if the orientation is {@link #VERTICAL}, the width parameter is ignored and if the
* orientation is {@link #HORIZONTAL} the height parameter is ignored because child view is
* expected to fill all of the space given to it.
*/
public static class LayoutParams extends RecyclerView.LayoutParams {
/**
* Span Id for Views that are not laid out yet.
*/
public static final int INVALID_SPAN_ID = -1;
private int mSpanIndex = INVALID_SPAN_ID;
private int mSpanSize = 0;
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
}
public LayoutParams(int width, int height) {
super(width, height);
}
public LayoutParams(ViewGroup.MarginLayoutParams source) {
super(source);
}
public LayoutParams(ViewGroup.LayoutParams source) {
super(source);
}
public LayoutParams(RecyclerView.LayoutParams source) {
super(source);
}
/**
* Returns the current span index of this View. If the View is not laid out yet, the return
* value is <code>undefined</code>.
* <p>
* Note that span index may change by whether the RecyclerView is RTL or not. For
* example, if the number of spans is 3 and layout is RTL, the rightmost item will have
* span index of 2. If the layout changes back to LTR, span index for this view will be 0.
* If the item was occupying 2 spans, span indices would be 1 and 0 respectively.
* <p>
* If the View occupies multiple spans, span with the minimum index is returned.
*
* @return The span index of the View.
*/
public int getSpanIndex() {
return mSpanIndex;
}
/**
* Returns the number of spans occupied by this View. If the View not laid out yet, the
* return value is <code>undefined</code>.
*
* @return The number of spans occupied by this View.
*/
public int getSpanSize() {
return mSpanSize;
}
}
}

View File

@ -0,0 +1,87 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific languag`e governing permissions and
* limitations under the License.
*/
package org.telegram.android.support.widget;
import android.view.View;
/**
* Helper class that keeps temporary state while {LayoutManager} is filling out the empty
* space.
*/
class LayoutState {
final static String TAG = "LayoutState";
final static int LAYOUT_START = -1;
final static int LAYOUT_END = 1;
final static int INVALID_LAYOUT = Integer.MIN_VALUE;
final static int ITEM_DIRECTION_HEAD = -1;
final static int ITEM_DIRECTION_TAIL = 1;
final static int SCOLLING_OFFSET_NaN = Integer.MIN_VALUE;
/**
* Number of pixels that we should fill, in the layout direction.
*/
int mAvailable;
/**
* Current position on the adapter to get the next item.
*/
int mCurrentPosition;
/**
* Defines the direction in which the data adapter is traversed.
* Should be {@link #ITEM_DIRECTION_HEAD} or {@link #ITEM_DIRECTION_TAIL}
*/
int mItemDirection;
/**
* Defines the direction in which the layout is filled.
* Should be {@link #LAYOUT_START} or {@link #LAYOUT_END}
*/
int mLayoutDirection;
/**
* Used if you want to pre-layout items that are not yet visible.
* The difference with {@link #mAvailable} is that, when recycling, distance rendered for
* {@link #mExtra} is not considered not to recycle visible children.
*/
int mExtra = 0;
/**
* @return true if there are more items in the data adapter
*/
boolean hasMore(RecyclerView.State state) {
return mCurrentPosition >= 0 && mCurrentPosition < state.getItemCount();
}
/**
* Gets the view for the next element that we should render.
* Also updates current item index to the next item, based on {@link #mItemDirection}
*
* @return The next element that we should render.
*/
View next(RecyclerView.Recycler recycler) {
final View view = recycler.getViewForPosition(mCurrentPosition);
mCurrentPosition += mItemDirection;
return view;
}
}

View File

@ -0,0 +1,338 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.telegram.android.support.widget;
import android.content.Context;
import android.graphics.PointF;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.View;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.LinearInterpolator;
/**
* {@link RecyclerView.SmoothScroller} implementation which uses
* {@link android.view.animation.LinearInterpolator} until the target position becames a child of
* the RecyclerView and then uses
* {@link android.view.animation.DecelerateInterpolator} to slowly approach to target position.
*/
abstract public class LinearSmoothScroller extends RecyclerView.SmoothScroller {
private static final String TAG = "LinearSmoothScroller";
private static final boolean DEBUG = false;
private static final float MILLISECONDS_PER_INCH = 25f;
private static final int TARGET_SEEK_SCROLL_DISTANCE_PX = 10000;
/**
* Align child view's left or top with parent view's left or top
*
* @see #calculateDtToFit(int, int, int, int, int)
* @see #calculateDxToMakeVisible(android.view.View, int)
* @see #calculateDyToMakeVisible(android.view.View, int)
*/
public static final int SNAP_TO_START = -1;
/**
* Align child view's right or bottom with parent view's right or bottom
*
* @see #calculateDtToFit(int, int, int, int, int)
* @see #calculateDxToMakeVisible(android.view.View, int)
* @see #calculateDyToMakeVisible(android.view.View, int)
*/
public static final int SNAP_TO_END = 1;
/**
* <p>Decides if the child should be snapped from start or end, depending on where it
* currently is in relation to its parent.</p>
* <p>For instance, if the view is virtually on the left of RecyclerView, using
* {@code SNAP_TO_ANY} is the same as using {@code SNAP_TO_START}</p>
*
* @see #calculateDtToFit(int, int, int, int, int)
* @see #calculateDxToMakeVisible(android.view.View, int)
* @see #calculateDyToMakeVisible(android.view.View, int)
*/
public static final int SNAP_TO_ANY = 0;
// Trigger a scroll to a further distance than TARGET_SEEK_SCROLL_DISTANCE_PX so that if target
// view is not laid out until interim target position is reached, we can detect the case before
// scrolling slows down and reschedule another interim target scroll
private static final float TARGET_SEEK_EXTRA_SCROLL_RATIO = 1.2f;
protected final LinearInterpolator mLinearInterpolator = new LinearInterpolator();
protected final DecelerateInterpolator mDecelerateInterpolator = new DecelerateInterpolator();
protected PointF mTargetVector;
private final float MILLISECONDS_PER_PX;
// Temporary variables to keep track of the interim scroll target. These values do not
// point to a real item position, rather point to an estimated location pixels.
protected int mInterimTargetDx = 0, mInterimTargetDy = 0;
public LinearSmoothScroller(Context context) {
MILLISECONDS_PER_PX = calculateSpeedPerPixel(context.getResources().getDisplayMetrics());
}
/**
* {@inheritDoc}
*/
@Override
protected void onStart() {
}
/**
* {@inheritDoc}
*/
@Override
protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
final int dx = calculateDxToMakeVisible(targetView, getHorizontalSnapPreference());
final int dy = calculateDyToMakeVisible(targetView, getVerticalSnapPreference());
final int distance = (int) Math.sqrt(dx * dx + dy * dy);
final int time = calculateTimeForDeceleration(distance);
if (time > 0) {
action.update(-dx, -dy, time, mDecelerateInterpolator);
}
}
/**
* {@inheritDoc}
*/
@Override
protected void onSeekTargetStep(int dx, int dy, RecyclerView.State state, Action action) {
if (getChildCount() == 0) {
stop();
return;
}
if (DEBUG && mTargetVector != null
&& ((mTargetVector.x * dx < 0 || mTargetVector.y * dy < 0))) {
throw new IllegalStateException("Scroll happened in the opposite direction"
+ " of the target. Some calculations are wrong");
}
mInterimTargetDx = clampApplyScroll(mInterimTargetDx, dx);
mInterimTargetDy = clampApplyScroll(mInterimTargetDy, dy);
if (mInterimTargetDx == 0 && mInterimTargetDy == 0) {
updateActionForInterimTarget(action);
} // everything is valid, keep going
}
/**
* {@inheritDoc}
*/
@Override
protected void onStop() {
mInterimTargetDx = mInterimTargetDy = 0;
mTargetVector = null;
}
/**
* Calculates the scroll speed.
*
* @param displayMetrics DisplayMetrics to be used for real dimension calculations
* @return The time (in ms) it should take for each pixel. For instance, if returned value is
* 2 ms, it means scrolling 1000 pixels with LinearInterpolation should take 2 seconds.
*/
protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
}
/**
* <p>Calculates the time for deceleration so that transition from LinearInterpolator to
* DecelerateInterpolator looks smooth.</p>
*
* @param dx Distance to scroll
* @return Time for DecelerateInterpolator to smoothly traverse the distance when transitioning
* from LinearInterpolation
*/
protected int calculateTimeForDeceleration(int dx) {
// we want to cover same area with the linear interpolator for the first 10% of the
// interpolation. After that, deceleration will take control.
// area under curve (1-(1-x)^2) can be calculated as (1 - x/3) * x * x
// which gives 0.100028 when x = .3356
// this is why we divide linear scrolling time with .3356
return (int) Math.ceil(calculateTimeForScrolling(dx) / .3356);
}
/**
* Calculates the time it should take to scroll the given distance (in pixels)
*
* @param dx Distance in pixels that we want to scroll
* @return Time in milliseconds
* @see #calculateSpeedPerPixel(android.util.DisplayMetrics)
*/
protected int calculateTimeForScrolling(int dx) {
// In a case where dx is very small, rounding may return 0 although dx > 0.
// To avoid that issue, ceil the result so that if dx > 0, we'll always return positive
// time.
return (int) Math.ceil(Math.abs(dx) * MILLISECONDS_PER_PX);
}
/**
* When scrolling towards a child view, this method defines whether we should align the left
* or the right edge of the child with the parent RecyclerView.
*
* @return SNAP_TO_START, SNAP_TO_END or SNAP_TO_ANY; depending on the current target vector
* @see #SNAP_TO_START
* @see #SNAP_TO_END
* @see #SNAP_TO_ANY
*/
protected int getHorizontalSnapPreference() {
return mTargetVector == null || mTargetVector.x == 0 ? SNAP_TO_ANY :
mTargetVector.x > 0 ? SNAP_TO_END : SNAP_TO_START;
}
/**
* When scrolling towards a child view, this method defines whether we should align the top
* or the bottom edge of the child with the parent RecyclerView.
*
* @return SNAP_TO_START, SNAP_TO_END or SNAP_TO_ANY; depending on the current target vector
* @see #SNAP_TO_START
* @see #SNAP_TO_END
* @see #SNAP_TO_ANY
*/
protected int getVerticalSnapPreference() {
return mTargetVector == null || mTargetVector.y == 0 ? SNAP_TO_ANY :
mTargetVector.y > 0 ? SNAP_TO_END : SNAP_TO_START;
}
/**
* When the target scroll position is not a child of the RecyclerView, this method calculates
* a direction vector towards that child and triggers a smooth scroll.
*
* @see #computeScrollVectorForPosition(int)
*/
protected void updateActionForInterimTarget(Action action) {
// find an interim target position
PointF scrollVector = computeScrollVectorForPosition(getTargetPosition());
if (scrollVector == null || (scrollVector.x == 0 && scrollVector.y == 0)) {
Log.e(TAG, "To support smooth scrolling, you should override \n"
+ "LayoutManager#computeScrollVectorForPosition.\n"
+ "Falling back to instant scroll");
final int target = getTargetPosition();
stop();
instantScrollToPosition(target);
return;
}
normalize(scrollVector);
mTargetVector = scrollVector;
mInterimTargetDx = (int) (TARGET_SEEK_SCROLL_DISTANCE_PX * scrollVector.x);
mInterimTargetDy = (int) (TARGET_SEEK_SCROLL_DISTANCE_PX * scrollVector.y);
final int time = calculateTimeForScrolling(TARGET_SEEK_SCROLL_DISTANCE_PX);
// To avoid UI hiccups, trigger a smooth scroll to a distance little further than the
// interim target. Since we track the distance travelled in onSeekTargetStep callback, it
// won't actually scroll more than what we need.
action.update((int) (mInterimTargetDx * TARGET_SEEK_EXTRA_SCROLL_RATIO)
, (int) (mInterimTargetDy * TARGET_SEEK_EXTRA_SCROLL_RATIO)
, (int) (time * TARGET_SEEK_EXTRA_SCROLL_RATIO), mLinearInterpolator);
}
private int clampApplyScroll(int tmpDt, int dt) {
final int before = tmpDt;
tmpDt -= dt;
if (before * tmpDt <= 0) { // changed sign, reached 0 or was 0, reset
return 0;
}
return tmpDt;
}
/**
* Helper method for {@link #calculateDxToMakeVisible(android.view.View, int)} and
* {@link #calculateDyToMakeVisible(android.view.View, int)}
*/
public int calculateDtToFit(int viewStart, int viewEnd, int boxStart, int boxEnd, int
snapPreference) {
switch (snapPreference) {
case SNAP_TO_START:
return boxStart - viewStart;
case SNAP_TO_END:
return boxEnd - viewEnd;
case SNAP_TO_ANY:
final int dtStart = boxStart - viewStart;
if (dtStart > 0) {
return dtStart;
}
final int dtEnd = boxEnd - viewEnd;
if (dtEnd < 0) {
return dtEnd;
}
break;
default:
throw new IllegalArgumentException("snap preference should be one of the"
+ " constants defined in SmoothScroller, starting with SNAP_");
}
return 0;
}
/**
* Calculates the vertical scroll amount necessary to make the given view fully visible
* inside the RecyclerView.
*
* @param view The view which we want to make fully visible
* @param snapPreference The edge which the view should snap to when entering the visible
* area. One of {@link #SNAP_TO_START}, {@link #SNAP_TO_END} or
* {@link #SNAP_TO_END}.
* @return The vertical scroll amount necessary to make the view visible with the given
* snap preference.
*/
public int calculateDyToMakeVisible(View view, int snapPreference) {
final RecyclerView.LayoutManager layoutManager = getLayoutManager();
if (!layoutManager.canScrollVertically()) {
return 0;
}
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
final int top = layoutManager.getDecoratedTop(view) - params.topMargin;
final int bottom = layoutManager.getDecoratedBottom(view) + params.bottomMargin;
final int start = layoutManager.getPaddingTop();
final int end = layoutManager.getHeight() - layoutManager.getPaddingBottom();
return calculateDtToFit(top, bottom, start, end, snapPreference);
}
/**
* Calculates the horizontal scroll amount necessary to make the given view fully visible
* inside the RecyclerView.
*
* @param view The view which we want to make fully visible
* @param snapPreference The edge which the view should snap to when entering the visible
* area. One of {@link #SNAP_TO_START}, {@link #SNAP_TO_END} or
* {@link #SNAP_TO_END}
* @return The vertical scroll amount necessary to make the view visible with the given
* snap preference.
*/
public int calculateDxToMakeVisible(View view, int snapPreference) {
final RecyclerView.LayoutManager layoutManager = getLayoutManager();
if (!layoutManager.canScrollHorizontally()) {
return 0;
}
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
final int left = layoutManager.getDecoratedLeft(view) - params.leftMargin;
final int right = layoutManager.getDecoratedRight(view) + params.rightMargin;
final int start = layoutManager.getPaddingLeft();
final int end = layoutManager.getWidth() - layoutManager.getPaddingRight();
return calculateDtToFit(left, right, start, end, snapPreference);
}
abstract public PointF computeScrollVectorForPosition(int targetPosition);
}

View File

@ -0,0 +1,237 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.telegram.android.support.widget;
import java.util.List;
import org.telegram.android.support.widget.AdapterHelper.UpdateOp;
import static org.telegram.android.support.widget.AdapterHelper.UpdateOp.ADD;
import static org.telegram.android.support.widget.AdapterHelper.UpdateOp.MOVE;
import static org.telegram.android.support.widget.AdapterHelper.UpdateOp.REMOVE;
import static org.telegram.android.support.widget.AdapterHelper.UpdateOp.UPDATE;
class OpReorderer {
final Callback mCallback;
public OpReorderer(Callback callback) {
mCallback = callback;
}
void reorderOps(List<UpdateOp> ops) {
// since move operations breaks continuity, their effects on ADD/RM are hard to handle.
// we push them to the end of the list so that they can be handled easily.
int badMove;
while ((badMove = getLastMoveOutOfOrder(ops)) != -1) {
swapMoveOp(ops, badMove, badMove + 1);
}
}
private void swapMoveOp(List<UpdateOp> list, int badMove, int next) {
final UpdateOp moveOp = list.get(badMove);
final UpdateOp nextOp = list.get(next);
switch (nextOp.cmd) {
case REMOVE:
swapMoveRemove(list, badMove, moveOp, next, nextOp);
break;
case ADD:
swapMoveAdd(list, badMove, moveOp, next, nextOp);
break;
case UPDATE:
swapMoveUpdate(list, badMove, moveOp, next, nextOp);
break;
}
}
void swapMoveRemove(List<UpdateOp> list, int movePos, UpdateOp moveOp,
int removePos, UpdateOp removeOp) {
UpdateOp extraRm = null;
// check if move is nulled out by remove
boolean revertedMove = false;
final boolean moveIsBackwards;
if (moveOp.positionStart < moveOp.itemCount) {
moveIsBackwards = false;
if (removeOp.positionStart == moveOp.positionStart
&& removeOp.itemCount == moveOp.itemCount - moveOp.positionStart) {
revertedMove = true;
}
} else {
moveIsBackwards = true;
if (removeOp.positionStart == moveOp.itemCount + 1 &&
removeOp.itemCount == moveOp.positionStart - moveOp.itemCount) {
revertedMove = true;
}
}
// going in reverse, first revert the effect of add
if (moveOp.itemCount < removeOp.positionStart) {
removeOp.positionStart--;
} else if (moveOp.itemCount < removeOp.positionStart + removeOp.itemCount) {
// move is removed.
removeOp.itemCount --;
moveOp.cmd = REMOVE;
moveOp.itemCount = 1;
if (removeOp.itemCount == 0) {
list.remove(removePos);
mCallback.recycleUpdateOp(removeOp);
}
// no need to swap, it is already a remove
return;
}
// now affect of add is consumed. now apply effect of first remove
if (moveOp.positionStart <= removeOp.positionStart) {
removeOp.positionStart++;
} else if (moveOp.positionStart < removeOp.positionStart + removeOp.itemCount) {
final int remaining = removeOp.positionStart + removeOp.itemCount
- moveOp.positionStart;
extraRm = mCallback.obtainUpdateOp(REMOVE, moveOp.positionStart + 1, remaining);
removeOp.itemCount = moveOp.positionStart - removeOp.positionStart;
}
// if effects of move is reverted by remove, we are done.
if (revertedMove) {
list.set(movePos, removeOp);
list.remove(removePos);
mCallback.recycleUpdateOp(moveOp);
return;
}
// now find out the new locations for move actions
if (moveIsBackwards) {
if (extraRm != null) {
if (moveOp.positionStart > extraRm.positionStart) {
moveOp.positionStart -= extraRm.itemCount;
}
if (moveOp.itemCount > extraRm.positionStart) {
moveOp.itemCount -= extraRm.itemCount;
}
}
if (moveOp.positionStart > removeOp.positionStart) {
moveOp.positionStart -= removeOp.itemCount;
}
if (moveOp.itemCount > removeOp.positionStart) {
moveOp.itemCount -= removeOp.itemCount;
}
} else {
if (extraRm != null) {
if (moveOp.positionStart >= extraRm.positionStart) {
moveOp.positionStart -= extraRm.itemCount;
}
if (moveOp.itemCount >= extraRm.positionStart) {
moveOp.itemCount -= extraRm.itemCount;
}
}
if (moveOp.positionStart >= removeOp.positionStart) {
moveOp.positionStart -= removeOp.itemCount;
}
if (moveOp.itemCount >= removeOp.positionStart) {
moveOp.itemCount -= removeOp.itemCount;
}
}
list.set(movePos, removeOp);
if (moveOp.positionStart != moveOp.itemCount) {
list.set(removePos, moveOp);
} else {
list.remove(removePos);
}
if (extraRm != null) {
list.add(movePos, extraRm);
}
}
private void swapMoveAdd(List<UpdateOp> list, int move, UpdateOp moveOp, int add,
UpdateOp addOp) {
int offset = 0;
// going in reverse, first revert the effect of add
if (moveOp.itemCount < addOp.positionStart) {
offset--;
}
if (moveOp.positionStart < addOp.positionStart) {
offset++;
}
if (addOp.positionStart <= moveOp.positionStart) {
moveOp.positionStart += addOp.itemCount;
}
if (addOp.positionStart <= moveOp.itemCount) {
moveOp.itemCount += addOp.itemCount;
}
addOp.positionStart += offset;
list.set(move, addOp);
list.set(add, moveOp);
}
void swapMoveUpdate(List<UpdateOp> list, int move, UpdateOp moveOp, int update,
UpdateOp updateOp) {
UpdateOp extraUp1 = null;
UpdateOp extraUp2 = null;
// going in reverse, first revert the effect of add
if (moveOp.itemCount < updateOp.positionStart) {
updateOp.positionStart--;
} else if (moveOp.itemCount < updateOp.positionStart + updateOp.itemCount) {
// moved item is updated. add an update for it
updateOp.itemCount--;
extraUp1 = mCallback.obtainUpdateOp(UPDATE, moveOp.positionStart, 1);
}
// now affect of add is consumed. now apply effect of first remove
if (moveOp.positionStart <= updateOp.positionStart) {
updateOp.positionStart++;
} else if (moveOp.positionStart < updateOp.positionStart + updateOp.itemCount) {
final int remaining = updateOp.positionStart + updateOp.itemCount
- moveOp.positionStart;
extraUp2 = mCallback.obtainUpdateOp(UPDATE, moveOp.positionStart + 1, remaining);
updateOp.itemCount -= remaining;
}
list.set(update, moveOp);
if (updateOp.itemCount > 0) {
list.set(move, updateOp);
} else {
list.remove(move);
mCallback.recycleUpdateOp(updateOp);
}
if (extraUp1 != null) {
list.add(move, extraUp1);
}
if (extraUp2 != null) {
list.add(move, extraUp2);
}
}
private int getLastMoveOutOfOrder(List<UpdateOp> list) {
boolean foundNonMove = false;
for (int i = list.size() - 1; i >= 0; i--) {
final UpdateOp op1 = list.get(i);
if (op1.cmd == MOVE) {
if (foundNonMove) {
return i;
}
} else {
foundNonMove = true;
}
}
return -1;
}
static interface Callback {
UpdateOp obtainUpdateOp(int cmd, int startPosition, int itemCount);
void recycleUpdateOp(UpdateOp op);
}
}

View File

@ -0,0 +1,338 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.telegram.android.support.widget;
import android.view.View;
import android.widget.LinearLayout;
/**
* Helper class for LayoutManagers to abstract measurements depending on the View's orientation.
* <p>
* It is developed to easily support vertical and horizontal orientations in a LayoutManager but
* can also be used to abstract calls around view bounds and child measurements with margins and
* decorations.
*
* @see #createHorizontalHelper(RecyclerView.LayoutManager)
* @see #createVerticalHelper(RecyclerView.LayoutManager)
*/
public abstract class OrientationHelper {
private static final int INVALID_SIZE = Integer.MIN_VALUE;
protected final RecyclerView.LayoutManager mLayoutManager;
public static final int HORIZONTAL = LinearLayout.HORIZONTAL;
public static final int VERTICAL = LinearLayout.VERTICAL;
private int mLastTotalSpace = INVALID_SIZE;
private OrientationHelper(RecyclerView.LayoutManager layoutManager) {
mLayoutManager = layoutManager;
}
/**
* Call this method after onLayout method is complete if state is NOT pre-layout.
* This method records information like layout bounds that might be useful in the next layout
* calculations.
*/
public void onLayoutComplete() {
mLastTotalSpace = getTotalSpace();
}
/**
* Returns the layout space change between the previous layout pass and current layout pass.
* <p>
* Make sure you call {@link #onLayoutComplete()} at the end of your LayoutManager's
* {@link RecyclerView.LayoutManager#onLayoutChildren(RecyclerView.Recycler,
* RecyclerView.State)} method.
*
* @return The difference between the current total space and previous layout's total space.
* @see #onLayoutComplete()
*/
public int getTotalSpaceChange() {
return INVALID_SIZE == mLastTotalSpace ? 0 : getTotalSpace() - mLastTotalSpace;
}
/**
* Returns the start of the view including its decoration and margin.
* <p>
* For example, for the horizontal helper, if a View's left is at pixel 20, has 2px left
* decoration and 3px left margin, returned value will be 15px.
*
* @param view The view element to check
* @return The first pixel of the element
* @see #getDecoratedEnd(android.view.View)
*/
public abstract int getDecoratedStart(View view);
/**
* Returns the end of the view including its decoration and margin.
* <p>
* For example, for the horizontal helper, if a View's right is at pixel 200, has 2px right
* decoration and 3px right margin, returned value will be 205.
*
* @param view The view element to check
* @return The last pixel of the element
* @see #getDecoratedStart(android.view.View)
*/
public abstract int getDecoratedEnd(View view);
/**
* Returns the space occupied by this View in the current orientation including decorations and
* margins.
*
* @param view The view element to check
* @return Total space occupied by this view
* @see #getDecoratedMeasurementInOther(View)
*/
public abstract int getDecoratedMeasurement(View view);
/**
* Returns the space occupied by this View in the perpendicular orientation including
* decorations and margins.
*
* @param view The view element to check
* @return Total space occupied by this view in the perpendicular orientation to current one
* @see #getDecoratedMeasurement(View)
*/
public abstract int getDecoratedMeasurementInOther(View view);
/**
* Returns the start position of the layout after the start padding is added.
*
* @return The very first pixel we can draw.
*/
public abstract int getStartAfterPadding();
/**
* Returns the end position of the layout after the end padding is removed.
*
* @return The end boundary for this layout.
*/
public abstract int getEndAfterPadding();
/**
* Returns the end position of the layout without taking padding into account.
*
* @return The end boundary for this layout without considering padding.
*/
public abstract int getEnd();
/**
* Offsets all children's positions by the given amount.
*
* @param amount Value to add to each child's layout parameters
*/
public abstract void offsetChildren(int amount);
/**
* Returns the total space to layout. This number is the difference between
* {@link #getEndAfterPadding()} and {@link #getStartAfterPadding()}.
*
* @return Total space to layout children
*/
public abstract int getTotalSpace();
/**
* Offsets the child in this orientation.
*
* @param view View to offset
* @param offset offset amount
*/
public abstract void offsetChild(View view, int offset);
/**
* Returns the padding at the end of the layout. For horizontal helper, this is the right
* padding and for vertical helper, this is the bottom padding. This method does not check
* whether the layout is RTL or not.
*
* @return The padding at the end of the layout.
*/
public abstract int getEndPadding();
/**
* Creates an OrientationHelper for the given LayoutManager and orientation.
*
* @param layoutManager LayoutManager to attach to
* @param orientation Desired orientation. Should be {@link #HORIZONTAL} or {@link #VERTICAL}
* @return A new OrientationHelper
*/
public static OrientationHelper createOrientationHelper(
RecyclerView.LayoutManager layoutManager, int orientation) {
switch (orientation) {
case HORIZONTAL:
return createHorizontalHelper(layoutManager);
case VERTICAL:
return createVerticalHelper(layoutManager);
}
throw new IllegalArgumentException("invalid orientation");
}
/**
* Creates a horizontal OrientationHelper for the given LayoutManager.
*
* @param layoutManager The LayoutManager to attach to.
* @return A new OrientationHelper
*/
public static OrientationHelper createHorizontalHelper(
RecyclerView.LayoutManager layoutManager) {
return new OrientationHelper(layoutManager) {
@Override
public int getEndAfterPadding() {
return mLayoutManager.getWidth() - mLayoutManager.getPaddingRight();
}
@Override
public int getEnd() {
return mLayoutManager.getWidth();
}
@Override
public void offsetChildren(int amount) {
mLayoutManager.offsetChildrenHorizontal(amount);
}
@Override
public int getStartAfterPadding() {
return mLayoutManager.getPaddingLeft();
}
@Override
public int getDecoratedMeasurement(View view) {
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
return mLayoutManager.getDecoratedMeasuredWidth(view) + params.leftMargin
+ params.rightMargin;
}
@Override
public int getDecoratedMeasurementInOther(View view) {
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
return mLayoutManager.getDecoratedMeasuredHeight(view) + params.topMargin
+ params.bottomMargin;
}
@Override
public int getDecoratedEnd(View view) {
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
return mLayoutManager.getDecoratedRight(view) + params.rightMargin;
}
@Override
public int getDecoratedStart(View view) {
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
return mLayoutManager.getDecoratedLeft(view) - params.leftMargin;
}
@Override
public int getTotalSpace() {
return mLayoutManager.getWidth() - mLayoutManager.getPaddingLeft()
- mLayoutManager.getPaddingRight();
}
@Override
public void offsetChild(View view, int offset) {
view.offsetLeftAndRight(offset);
}
@Override
public int getEndPadding() {
return mLayoutManager.getPaddingRight();
}
};
}
/**
* Creates a vertical OrientationHelper for the given LayoutManager.
*
* @param layoutManager The LayoutManager to attach to.
* @return A new OrientationHelper
*/
public static OrientationHelper createVerticalHelper(RecyclerView.LayoutManager layoutManager) {
return new OrientationHelper(layoutManager) {
@Override
public int getEndAfterPadding() {
return mLayoutManager.getHeight() - mLayoutManager.getPaddingBottom();
}
@Override
public int getEnd() {
return mLayoutManager.getHeight();
}
@Override
public void offsetChildren(int amount) {
mLayoutManager.offsetChildrenVertical(amount);
}
@Override
public int getStartAfterPadding() {
return mLayoutManager.getPaddingTop();
}
@Override
public int getDecoratedMeasurement(View view) {
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
return mLayoutManager.getDecoratedMeasuredHeight(view) + params.topMargin
+ params.bottomMargin;
}
@Override
public int getDecoratedMeasurementInOther(View view) {
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
return mLayoutManager.getDecoratedMeasuredWidth(view) + params.leftMargin
+ params.rightMargin;
}
@Override
public int getDecoratedEnd(View view) {
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
return mLayoutManager.getDecoratedBottom(view) + params.bottomMargin;
}
@Override
public int getDecoratedStart(View view) {
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
return mLayoutManager.getDecoratedTop(view) - params.topMargin;
}
@Override
public int getTotalSpace() {
return mLayoutManager.getHeight() - mLayoutManager.getPaddingTop()
- mLayoutManager.getPaddingBottom();
}
@Override
public void offsetChild(View view, int offset) {
view.offsetTopAndBottom(offset);
}
@Override
public int getEndPadding() {
return mLayoutManager.getPaddingBottom();
}
};
}
}

View File

@ -0,0 +1,460 @@
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.telegram.android.support.widget;
import java.util.ArrayList;
/**
* Like a SparseArray, but with the ability to offset key ranges for bulk insertions/deletions.
*/
class PositionMap<E> implements Cloneable {
private static final Object DELETED = new Object();
private boolean mGarbage = false;
private int[] mKeys;
private Object[] mValues;
private int mSize;
/**
* Creates a new SparseArray containing no mappings.
*/
public PositionMap() {
this(10);
}
/**
* Creates a new PositionMap containing no mappings that will not
* require any additional memory allocation to store the specified
* number of mappings. If you supply an initial capacity of 0, the
* sparse array will be initialized with a light-weight representation
* not requiring any additional array allocations.
*/
public PositionMap(int initialCapacity) {
if (initialCapacity == 0) {
mKeys = ContainerHelpers.EMPTY_INTS;
mValues = ContainerHelpers.EMPTY_OBJECTS;
} else {
initialCapacity = idealIntArraySize(initialCapacity);
mKeys = new int[initialCapacity];
mValues = new Object[initialCapacity];
}
mSize = 0;
}
@Override
@SuppressWarnings("unchecked")
public PositionMap<E> clone() {
PositionMap<E> clone = null;
try {
clone = (PositionMap<E>) super.clone();
clone.mKeys = mKeys.clone();
clone.mValues = mValues.clone();
} catch (CloneNotSupportedException cnse) {
/* ignore */
}
return clone;
}
/**
* Gets the Object mapped from the specified key, or <code>null</code>
* if no such mapping has been made.
*/
public E get(int key) {
return get(key, null);
}
/**
* Gets the Object mapped from the specified key, or the specified Object
* if no such mapping has been made.
*/
@SuppressWarnings("unchecked")
public E get(int key, E valueIfKeyNotFound) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
if (i < 0 || mValues[i] == DELETED) {
return valueIfKeyNotFound;
} else {
return (E) mValues[i];
}
}
/**
* Removes the mapping from the specified key, if there was any.
*/
public void delete(int key) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
if (i >= 0) {
if (mValues[i] != DELETED) {
mValues[i] = DELETED;
mGarbage = true;
}
}
}
/**
* Alias for {@link #delete(int)}.
*/
public void remove(int key) {
delete(key);
}
/**
* Removes the mapping at the specified index.
*/
public void removeAt(int index) {
if (mValues[index] != DELETED) {
mValues[index] = DELETED;
mGarbage = true;
}
}
/**
* Remove a range of mappings as a batch.
*
* @param index Index to begin at
* @param size Number of mappings to remove
*/
public void removeAtRange(int index, int size) {
final int end = Math.min(mSize, index + size);
for (int i = index; i < end; i++) {
removeAt(i);
}
}
public void insertKeyRange(int keyStart, int count) {
}
public void removeKeyRange(ArrayList<E> removedItems, int keyStart, int count) {
}
private void gc() {
// Log.e("SparseArray", "gc start with " + mSize);
int n = mSize;
int o = 0;
int[] keys = mKeys;
Object[] values = mValues;
for (int i = 0; i < n; i++) {
Object val = values[i];
if (val != DELETED) {
if (i != o) {
keys[o] = keys[i];
values[o] = val;
values[i] = null;
}
o++;
}
}
mGarbage = false;
mSize = o;
// Log.e("SparseArray", "gc end with " + mSize);
}
/**
* Adds a mapping from the specified key to the specified value,
* replacing the previous mapping from the specified key if there
* was one.
*/
public void put(int key, E value) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
if (i >= 0) {
mValues[i] = value;
} else {
i = ~i;
if (i < mSize && mValues[i] == DELETED) {
mKeys[i] = key;
mValues[i] = value;
return;
}
if (mGarbage && mSize >= mKeys.length) {
gc();
// Search again because indices may have changed.
i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
}
if (mSize >= mKeys.length) {
int n = idealIntArraySize(mSize + 1);
int[] nkeys = new int[n];
Object[] nvalues = new Object[n];
// Log.e("SparseArray", "grow " + mKeys.length + " to " + n);
System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length);
System.arraycopy(mValues, 0, nvalues, 0, mValues.length);
mKeys = nkeys;
mValues = nvalues;
}
if (mSize - i != 0) {
// Log.e("SparseArray", "move " + (mSize - i));
System.arraycopy(mKeys, i, mKeys, i + 1, mSize - i);
System.arraycopy(mValues, i, mValues, i + 1, mSize - i);
}
mKeys[i] = key;
mValues[i] = value;
mSize++;
}
}
/**
* Returns the number of key-value mappings that this SparseArray
* currently stores.
*/
public int size() {
if (mGarbage) {
gc();
}
return mSize;
}
/**
* Given an index in the range <code>0...size()-1</code>, returns
* the key from the <code>index</code>th key-value mapping that this
* SparseArray stores.
*/
public int keyAt(int index) {
if (mGarbage) {
gc();
}
return mKeys[index];
}
/**
* Given an index in the range <code>0...size()-1</code>, returns
* the value from the <code>index</code>th key-value mapping that this
* SparseArray stores.
*/
@SuppressWarnings("unchecked")
public E valueAt(int index) {
if (mGarbage) {
gc();
}
return (E) mValues[index];
}
/**
* Given an index in the range <code>0...size()-1</code>, sets a new
* value for the <code>index</code>th key-value mapping that this
* SparseArray stores.
*/
public void setValueAt(int index, E value) {
if (mGarbage) {
gc();
}
mValues[index] = value;
}
/**
* Returns the index for which {@link #keyAt} would return the
* specified key, or a negative number if the specified
* key is not mapped.
*/
public int indexOfKey(int key) {
if (mGarbage) {
gc();
}
return ContainerHelpers.binarySearch(mKeys, mSize, key);
}
/**
* Returns an index for which {@link #valueAt} would return the
* specified key, or a negative number if no keys map to the
* specified value.
* <p>Beware that this is a linear search, unlike lookups by key,
* and that multiple keys can map to the same value and this will
* find only one of them.
* <p>Note also that unlike most collections' {@code indexOf} methods,
* this method compares values using {@code ==} rather than {@code equals}.
*/
public int indexOfValue(E value) {
if (mGarbage) {
gc();
}
for (int i = 0; i < mSize; i++)
if (mValues[i] == value)
return i;
return -1;
}
/**
* Removes all key-value mappings from this SparseArray.
*/
public void clear() {
int n = mSize;
Object[] values = mValues;
for (int i = 0; i < n; i++) {
values[i] = null;
}
mSize = 0;
mGarbage = false;
}
/**
* Puts a key/value pair into the array, optimizing for the case where
* the key is greater than all existing keys in the array.
*/
public void append(int key, E value) {
if (mSize != 0 && key <= mKeys[mSize - 1]) {
put(key, value);
return;
}
if (mGarbage && mSize >= mKeys.length) {
gc();
}
int pos = mSize;
if (pos >= mKeys.length) {
int n = idealIntArraySize(pos + 1);
int[] nkeys = new int[n];
Object[] nvalues = new Object[n];
// Log.e("SparseArray", "grow " + mKeys.length + " to " + n);
System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length);
System.arraycopy(mValues, 0, nvalues, 0, mValues.length);
mKeys = nkeys;
mValues = nvalues;
}
mKeys[pos] = key;
mValues[pos] = value;
mSize = pos + 1;
}
/**
* {@inheritDoc}
*
* <p>This implementation composes a string by iterating over its mappings. If
* this map contains itself as a value, the string "(this Map)"
* will appear in its place.
*/
@Override
public String toString() {
if (size() <= 0) {
return "{}";
}
StringBuilder buffer = new StringBuilder(mSize * 28);
buffer.append('{');
for (int i=0; i<mSize; i++) {
if (i > 0) {
buffer.append(", ");
}
int key = keyAt(i);
buffer.append(key);
buffer.append('=');
Object value = valueAt(i);
if (value != this) {
buffer.append(value);
} else {
buffer.append("(this Map)");
}
}
buffer.append('}');
return buffer.toString();
}
static int idealByteArraySize(int need) {
for (int i = 4; i < 32; i++)
if (need <= (1 << i) - 12)
return (1 << i) - 12;
return need;
}
static int idealBooleanArraySize(int need) {
return idealByteArraySize(need);
}
static int idealShortArraySize(int need) {
return idealByteArraySize(need * 2) / 2;
}
static int idealCharArraySize(int need) {
return idealByteArraySize(need * 2) / 2;
}
static int idealIntArraySize(int need) {
return idealByteArraySize(need * 4) / 4;
}
static int idealFloatArraySize(int need) {
return idealByteArraySize(need * 4) / 4;
}
static int idealObjectArraySize(int need) {
return idealByteArraySize(need * 4) / 4;
}
static int idealLongArraySize(int need) {
return idealByteArraySize(need * 8) / 8;
}
static class ContainerHelpers {
static final boolean[] EMPTY_BOOLEANS = new boolean[0];
static final int[] EMPTY_INTS = new int[0];
static final long[] EMPTY_LONGS = new long[0];
static final Object[] EMPTY_OBJECTS = new Object[0];
// This is Arrays.binarySearch(), but doesn't do any argument validation.
static int binarySearch(int[] array, int size, int value) {
int lo = 0;
int hi = size - 1;
while (lo <= hi) {
final int mid = (lo + hi) >>> 1;
final int midVal = array[mid];
if (midVal < value) {
lo = mid + 1;
} else if (midVal > value) {
hi = mid - 1;
} else {
return mid; // value found
}
}
return ~lo; // value not present
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,101 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.telegram.android.support.widget;
import android.os.Bundle;
import android.support.v4.view.AccessibilityDelegateCompat;
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
import android.view.View;
import android.view.accessibility.AccessibilityEvent;
/**
* The AccessibilityDelegate used by RecyclerView.
* <p>
* This class handles basic accessibility actions and delegates them to LayoutManager.
*/
public class RecyclerViewAccessibilityDelegate extends AccessibilityDelegateCompat {
final RecyclerView mRecyclerView;
public RecyclerViewAccessibilityDelegate(RecyclerView recyclerView) {
mRecyclerView = recyclerView;
}
private boolean shouldIgnore() {
return mRecyclerView.hasPendingAdapterUpdates();
}
@Override
public boolean performAccessibilityAction(View host, int action, Bundle args) {
if (super.performAccessibilityAction(host, action, args)) {
return true;
}
if (!shouldIgnore() && mRecyclerView.getLayoutManager() != null) {
return mRecyclerView.getLayoutManager().performAccessibilityAction(action, args);
}
return false;
}
@Override
public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
super.onInitializeAccessibilityNodeInfo(host, info);
info.setClassName(RecyclerView.class.getName());
if (!shouldIgnore() && mRecyclerView.getLayoutManager() != null) {
mRecyclerView.getLayoutManager().onInitializeAccessibilityNodeInfo(info);
}
}
@Override
public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) {
super.onInitializeAccessibilityEvent(host, event);
event.setClassName(RecyclerView.class.getName());
if (host instanceof RecyclerView && !shouldIgnore()) {
RecyclerView rv = (RecyclerView) host;
if (rv.getLayoutManager() != null) {
rv.getLayoutManager().onInitializeAccessibilityEvent(event);
}
}
}
AccessibilityDelegateCompat getItemDelegate() {
return mItemDelegate;
}
final AccessibilityDelegateCompat mItemDelegate = new AccessibilityDelegateCompat() {
@Override
public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
super.onInitializeAccessibilityNodeInfo(host, info);
if (!shouldIgnore() && mRecyclerView.getLayoutManager() != null) {
mRecyclerView.getLayoutManager().
onInitializeAccessibilityNodeInfoForItem(host, info);
}
}
@Override
public boolean performAccessibilityAction(View host, int action, Bundle args) {
if (super.performAccessibilityAction(host, action, args)) {
return true;
}
if (!shouldIgnore() && mRecyclerView.getLayoutManager() != null) {
return mRecyclerView.getLayoutManager().
performAccessibilityActionForItem(host, action, args);
}
return false;
}
};
}

Some files were not shown because too many files have changed in this diff Show More