diff --git a/TMessagesProj/build.gradle b/TMessagesProj/build.gradle index 84dc35852..cf9848807 100644 --- a/TMessagesProj/build.gradle +++ b/TMessagesProj/build.gradle @@ -5,11 +5,11 @@ repositories { } dependencies { - compile 'com.android.support:support-v4:23.3.0' + compile 'com.android.support:support-v4:23.4.0' compile "com.google.android.gms:play-services-gcm:8.4.0" compile "com.google.android.gms:play-services-maps:8.4.0" - compile 'net.hockeyapp.android:HockeySDK:4.0.+' - compile 'com.googlecode.mp4parser:isoparser:1.0.+' + compile 'net.hockeyapp.android:HockeySDK:4.0.1' + compile 'com.googlecode.mp4parser:isoparser:1.0.6' } android { @@ -63,7 +63,7 @@ android { } } - defaultConfig.versionCode = 803 + defaultConfig.versionCode = 821 sourceSets.main { jniLibs.srcDir 'libs' @@ -112,9 +112,9 @@ android { } defaultConfig { - minSdkVersion 9 + minSdkVersion 14 targetSdkVersion 23 - versionName "3.9.0" + versionName "3.10.1" } } diff --git a/TMessagesProj/config/debug/AndroidManifest.xml b/TMessagesProj/config/debug/AndroidManifest.xml index 9a53515fe..176edd31f 100644 --- a/TMessagesProj/config/debug/AndroidManifest.xml +++ b/TMessagesProj/config/debug/AndroidManifest.xml @@ -1,5 +1,6 @@ @@ -62,6 +63,22 @@ + + + + + + + + + diff --git a/TMessagesProj/config/release/AndroidManifest.xml b/TMessagesProj/config/release/AndroidManifest.xml index 54502f173..8bb0cb0de 100644 --- a/TMessagesProj/config/release/AndroidManifest.xml +++ b/TMessagesProj/config/release/AndroidManifest.xml @@ -1,5 +1,6 @@ @@ -62,6 +63,21 @@ + + + + + + + + diff --git a/TMessagesProj/google-services.json b/TMessagesProj/google-services.json old mode 100644 new mode 100755 index 0002195c2..9403433ed --- a/TMessagesProj/google-services.json +++ b/TMessagesProj/google-services.json @@ -18,22 +18,9 @@ "oauth_client": [], "api_key": [], "services": { - "analytics_service": { - "status": 1 - }, "cloud_messaging_service": { "status": 2, "apns_config": [] - }, - "appinvite_service": { - "status": 1, - "other_platform_oauth_client": [] - }, - "google_signin_service": { - "status": 1 - }, - "ads_service": { - "status": 1 } } }, @@ -50,22 +37,9 @@ "oauth_client": [], "api_key": [], "services": { - "analytics_service": { - "status": 1 - }, "cloud_messaging_service": { "status": 2, "apns_config": [] - }, - "appinvite_service": { - "status": 1, - "other_platform_oauth_client": [] - }, - "google_signin_service": { - "status": 1 - }, - "ads_service": { - "status": 1 } } } diff --git a/TMessagesProj/jni/Android.mk b/TMessagesProj/jni/Android.mk index d680d15d2..bbcdfab94 100755 --- a/TMessagesProj/jni/Android.mk +++ b/TMessagesProj/jni/Android.mk @@ -265,6 +265,8 @@ else else ifeq ($(TARGET_ARCH_ABI),x86) + LOCAL_CPPFLAGS += -Dx86fix + LOCAL_CFLAGS += -Dx86fix LOCAL_ARM_MODE := arm LOCAL_SRC_FILE += \ ./libyuv/source/row_x86.asm diff --git a/TMessagesProj/jni/libyuv/include/libyuv/convert.h b/TMessagesProj/jni/libyuv/include/libyuv/convert.h index a8d3fa07a..a2cdc5718 100644 --- a/TMessagesProj/jni/libyuv/include/libyuv/convert.h +++ b/TMessagesProj/jni/libyuv/include/libyuv/convert.h @@ -12,10 +12,8 @@ #define INCLUDE_LIBYUV_CONVERT_H_ #include "libyuv/basic_types.h" -// TODO(fbarchard): Remove the following headers includes. -#include "libyuv/convert_from.h" -#include "libyuv/planar_functions.h" -#include "libyuv/rotate.h" + +#include "libyuv/rotate.h" // For enum RotationMode. #ifdef __cplusplus namespace libyuv { diff --git a/TMessagesProj/jni/libyuv/include/libyuv/convert_argb.h b/TMessagesProj/jni/libyuv/include/libyuv/convert_argb.h index ce4e3d075..079d273b1 100644 --- a/TMessagesProj/jni/libyuv/include/libyuv/convert_argb.h +++ b/TMessagesProj/jni/libyuv/include/libyuv/convert_argb.h @@ -12,10 +12,8 @@ #define INCLUDE_LIBYUV_CONVERT_ARGB_H_ #include "libyuv/basic_types.h" -// TODO(fbarchard): Remove the following headers includes -#include "libyuv/convert_from.h" -#include "libyuv/planar_functions.h" -#include "libyuv/rotate.h" + +#include "libyuv/rotate.h" // For enum RotationMode. // TODO(fbarchard): This set of functions should exactly match convert.h // TODO(fbarchard): Add tests. Create random content of right size and convert diff --git a/TMessagesProj/jni/libyuv/include/libyuv/convert_from.h b/TMessagesProj/jni/libyuv/include/libyuv/convert_from.h index 9fd8d4de5..39e1578a0 100644 --- a/TMessagesProj/jni/libyuv/include/libyuv/convert_from.h +++ b/TMessagesProj/jni/libyuv/include/libyuv/convert_from.h @@ -56,8 +56,6 @@ int I400Copy(const uint8* src_y, int src_stride_y, uint8* dst_y, int dst_stride_y, int width, int height); -// TODO(fbarchard): I420ToM420 - LIBYUV_API int I420ToNV12(const uint8* src_y, int src_stride_y, const uint8* src_u, int src_stride_u, diff --git a/TMessagesProj/jni/libyuv/include/libyuv/cpu_id.h b/TMessagesProj/jni/libyuv/include/libyuv/cpu_id.h index 2ccc3e7dd..dfb7445e2 100644 --- a/TMessagesProj/jni/libyuv/include/libyuv/cpu_id.h +++ b/TMessagesProj/jni/libyuv/include/libyuv/cpu_id.h @@ -62,7 +62,7 @@ static __inline int TestCpuFlag(int test_flag) { // For testing, allow CPU flags to be disabled. // ie MaskCpuFlags(~kCpuHasSSSE3) to disable SSSE3. // MaskCpuFlags(-1) to enable all cpu specific optimizations. -// MaskCpuFlags(0) to disable all cpu specific optimizations. +// MaskCpuFlags(1) to disable all cpu specific optimizations. LIBYUV_API void MaskCpuFlags(int enable_flags); diff --git a/TMessagesProj/jni/libyuv/include/libyuv/planar_functions.h b/TMessagesProj/jni/libyuv/include/libyuv/planar_functions.h index 9c19a59df..881b0c5c6 100644 --- a/TMessagesProj/jni/libyuv/include/libyuv/planar_functions.h +++ b/TMessagesProj/jni/libyuv/include/libyuv/planar_functions.h @@ -288,6 +288,12 @@ int ARGBCopyAlpha(const uint8* src_argb, int src_stride_argb, uint8* dst_argb, int dst_stride_argb, int width, int height); +// Extract the alpha channel from ARGB. +LIBYUV_API +int ARGBExtractAlpha(const uint8* src_argb, int src_stride_argb, + uint8* dst_a, int dst_stride_a, + int width, int height); + // Copy Y channel to Alpha of ARGB. LIBYUV_API int ARGBCopyYToAlpha(const uint8* src_y, int src_stride_y, diff --git a/TMessagesProj/jni/libyuv/include/libyuv/row.h b/TMessagesProj/jni/libyuv/include/libyuv/row.h index b5d9aaa17..8dae60166 100644 --- a/TMessagesProj/jni/libyuv/include/libyuv/row.h +++ b/TMessagesProj/jni/libyuv/include/libyuv/row.h @@ -104,6 +104,7 @@ extern "C" { #define HAS_ARGBTOUVROW_SSSE3 #define HAS_ARGBTOYJROW_SSSE3 #define HAS_ARGBTOYROW_SSSE3 +#define HAS_ARGBEXTRACTALPHAROW_SSE2 #define HAS_BGRATOUVROW_SSSE3 #define HAS_BGRATOYROW_SSSE3 #define HAS_COPYROW_ERMS @@ -179,8 +180,7 @@ extern "C" { // The following functions fail on gcc/clang 32 bit with fpic and framepointer. // caveat: clangcl uses row_win.cc which works. -#if defined(NDEBUG) || !(defined(_DEBUG) && defined(__i386__)) || \ - !defined(__i386__) || defined(_MSC_VER) +#if !defined(x86fix) // TODO(fbarchard): fix build error on x86 debug // https://code.google.com/p/libyuv/issues/detail?id=524 #define HAS_I411TOARGBROW_SSSE3 @@ -208,7 +208,7 @@ extern "C" { #define HAS_COPYROW_AVX #define HAS_H422TOARGBROW_AVX2 #define HAS_I400TOARGBROW_AVX2 -#if !(defined(_DEBUG) && defined(__i386__)) +#if !defined(x86fix) // TODO(fbarchard): fix build error on android_full_debug=1 // https://code.google.com/p/libyuv/issues/detail?id=517 #define HAS_I422ALPHATOARGBROW_AVX2 @@ -291,6 +291,7 @@ extern "C" { #define HAS_ARGBTOUVROW_NEON #define HAS_ARGBTOYJROW_NEON #define HAS_ARGBTOYROW_NEON +#define HAS_ARGBEXTRACTALPHAROW_NEON #define HAS_BGRATOUVROW_NEON #define HAS_BGRATOYROW_NEON #define HAS_COPYROW_NEON @@ -877,6 +878,14 @@ void ARGBCopyAlphaRow_Any_SSE2(const uint8* src_argb, uint8* dst_argb, void ARGBCopyAlphaRow_Any_AVX2(const uint8* src_argb, uint8* dst_argb, int width); +void ARGBExtractAlphaRow_C(const uint8* src_argb, uint8* dst_a, int width); +void ARGBExtractAlphaRow_SSE2(const uint8* src_argb, uint8* dst_a, int width); +void ARGBExtractAlphaRow_NEON(const uint8* src_argb, uint8* dst_a, int width); +void ARGBExtractAlphaRow_Any_SSE2(const uint8* src_argb, uint8* dst_a, + int width); +void ARGBExtractAlphaRow_Any_NEON(const uint8* src_argb, uint8* dst_a, + int width); + void ARGBCopyYToAlphaRow_C(const uint8* src_y, uint8* dst_argb, int width); void ARGBCopyYToAlphaRow_SSE2(const uint8* src_y, uint8* dst_argb, int width); void ARGBCopyYToAlphaRow_AVX2(const uint8* src_y, uint8* dst_argb, int width); diff --git a/TMessagesProj/jni/libyuv/include/libyuv/version.h b/TMessagesProj/jni/libyuv/include/libyuv/version.h index f1e6ae2f2..3a6bbe3cb 100644 --- a/TMessagesProj/jni/libyuv/include/libyuv/version.h +++ b/TMessagesProj/jni/libyuv/include/libyuv/version.h @@ -11,6 +11,6 @@ #ifndef INCLUDE_LIBYUV_VERSION_H_ // NOLINT #define INCLUDE_LIBYUV_VERSION_H_ -#define LIBYUV_VERSION 1586 +#define LIBYUV_VERSION 1597 #endif // INCLUDE_LIBYUV_VERSION_H_ NOLINT diff --git a/TMessagesProj/jni/libyuv/source/convert_argb.cc b/TMessagesProj/jni/libyuv/source/convert_argb.cc index e586f7043..fb9582d62 100644 --- a/TMessagesProj/jni/libyuv/source/convert_argb.cc +++ b/TMessagesProj/jni/libyuv/source/convert_argb.cc @@ -14,6 +14,7 @@ #ifdef HAVE_JPEG #include "libyuv/mjpeg_decoder.h" #endif +#include "libyuv/planar_functions.h" // For CopyPlane and ARGBShuffle. #include "libyuv/rotate_argb.h" #include "libyuv/row.h" #include "libyuv/video_common.h" diff --git a/TMessagesProj/jni/libyuv/source/convert_from.cc b/TMessagesProj/jni/libyuv/source/convert_from.cc index 3bc9eb1be..46abdebcd 100644 --- a/TMessagesProj/jni/libyuv/source/convert_from.cc +++ b/TMessagesProj/jni/libyuv/source/convert_from.cc @@ -1077,7 +1077,6 @@ int ConvertFromI420(const uint8* y, int y_stride, // Triplanar formats // TODO(fbarchard): halfstride instead of halfwidth case FOURCC_I420: - case FOURCC_YU12: case FOURCC_YV12: { int halfwidth = (width + 1) / 2; int halfheight = (height + 1) / 2; diff --git a/TMessagesProj/jni/libyuv/source/convert_jpeg.cc b/TMessagesProj/jni/libyuv/source/convert_jpeg.cc index bcb980f7f..90f550a26 100644 --- a/TMessagesProj/jni/libyuv/source/convert_jpeg.cc +++ b/TMessagesProj/jni/libyuv/source/convert_jpeg.cc @@ -9,6 +9,7 @@ */ #include "libyuv/convert.h" +#include "libyuv/convert_argb.h" #ifdef HAVE_JPEG #include "libyuv/mjpeg_decoder.h" diff --git a/TMessagesProj/jni/libyuv/source/convert_to_argb.cc b/TMessagesProj/jni/libyuv/source/convert_to_argb.cc index 7533f5010..aecdc80fd 100644 --- a/TMessagesProj/jni/libyuv/source/convert_to_argb.cc +++ b/TMessagesProj/jni/libyuv/source/convert_to_argb.cc @@ -176,7 +176,6 @@ int ConvertToARGB(const uint8* sample, size_t sample_size, break; // Triplanar formats case FOURCC_I420: - case FOURCC_YU12: case FOURCC_YV12: { const uint8* src_y = sample + (src_width * crop_y + crop_x); const uint8* src_u; diff --git a/TMessagesProj/jni/libyuv/source/convert_to_i420.cc b/TMessagesProj/jni/libyuv/source/convert_to_i420.cc index 5e75369b5..e5f307c44 100644 --- a/TMessagesProj/jni/libyuv/source/convert_to_i420.cc +++ b/TMessagesProj/jni/libyuv/source/convert_to_i420.cc @@ -39,12 +39,13 @@ int ConvertToI420(const uint8* sample, int aligned_src_width = (src_width + 1) & ~1; const uint8* src; const uint8* src_uv; - int abs_src_height = (src_height < 0) ? -src_height : src_height; - int inv_crop_height = (crop_height < 0) ? -crop_height : crop_height; + const int abs_src_height = (src_height < 0) ? -src_height : src_height; + // TODO(nisse): Why allow crop_height < 0? + const int abs_crop_height = (crop_height < 0) ? -crop_height : crop_height; int r = 0; LIBYUV_BOOL need_buf = (rotation && format != FOURCC_I420 && format != FOURCC_NV12 && format != FOURCC_NV21 && - format != FOURCC_YU12 && format != FOURCC_YV12) || y == sample; + format != FOURCC_YV12) || y == sample; uint8* tmp_y = y; uint8* tmp_u = u; uint8* tmp_v = v; @@ -52,16 +53,14 @@ int ConvertToI420(const uint8* sample, int tmp_u_stride = u_stride; int tmp_v_stride = v_stride; uint8* rotate_buffer = NULL; - int abs_crop_height = (crop_height < 0) ? -crop_height : crop_height; + const int inv_crop_height = + (src_height < 0) ? -abs_crop_height : abs_crop_height; if (!y || !u || !v || !sample || src_width <= 0 || crop_width <= 0 || src_height == 0 || crop_height == 0) { return -1; } - if (src_height < 0) { - inv_crop_height = -inv_crop_height; - } // One pass rotation is available for some formats. For the rest, convert // to I420 (with optional vertical flipping) into a temporary I420 buffer, @@ -214,7 +213,6 @@ int ConvertToI420(const uint8* sample, break; // Triplanar formats case FOURCC_I420: - case FOURCC_YU12: case FOURCC_YV12: { const uint8* src_y = sample + (src_width * crop_y + crop_x); const uint8* src_u; diff --git a/TMessagesProj/jni/libyuv/source/planar_functions.cc b/TMessagesProj/jni/libyuv/source/planar_functions.cc index 73fa7d284..237ab6831 100644 --- a/TMessagesProj/jni/libyuv/source/planar_functions.cc +++ b/TMessagesProj/jni/libyuv/source/planar_functions.cc @@ -2374,6 +2374,49 @@ int ARGBCopyAlpha(const uint8* src_argb, int src_stride_argb, return 0; } +// Extract just the alpha channel from ARGB. +LIBYUV_API +int ARGBExtractAlpha(const uint8* src_argb, int src_stride, + uint8* dst_a, int dst_stride, + int width, int height) { + if (!src_argb || !dst_a || width <= 0 || height == 0) { + return -1; + } + // Negative height means invert the image. + if (height < 0) { + height = -height; + src_argb += (height - 1) * src_stride; + src_stride = -src_stride; + } + // Coalesce rows. + if (src_stride == width * 4 && dst_stride == width) { + width *= height; + height = 1; + src_stride = dst_stride = 0; + } + void (*ARGBExtractAlphaRow)(const uint8 *src_argb, uint8 *dst_a, int width) = + ARGBExtractAlphaRow_C; +#if defined(HAS_ARGBEXTRACTALPHAROW_SSE2) + if (TestCpuFlag(kCpuHasSSE2)) { + ARGBExtractAlphaRow = IS_ALIGNED(width, 8) ? ARGBExtractAlphaRow_SSE2 + : ARGBExtractAlphaRow_Any_SSE2; + } +#endif +#if defined(HAS_ARGBEXTRACTALPHAROW_NEON) + if (TestCpuFlag(kCpuHasNEON)) { + ARGBExtractAlphaRow = IS_ALIGNED(width, 16) ? ARGBExtractAlphaRow_NEON + : ARGBExtractAlphaRow_Any_NEON; + } +#endif + + for (int y = 0; y < height; ++y) { + ARGBExtractAlphaRow(src_argb, dst_a, width); + src_argb += src_stride; + dst_a += dst_stride; + } + return 0; +} + // Copy a planar Y channel to the alpha channel of a destination ARGB image. LIBYUV_API int ARGBCopyYToAlpha(const uint8* src_y, int src_stride_y, diff --git a/TMessagesProj/jni/libyuv/source/rotate_mips.cc b/TMessagesProj/jni/libyuv/source/rotate_mips.cc index 23e89fbad..1e8ce2519 100644 --- a/TMessagesProj/jni/libyuv/source/rotate_mips.cc +++ b/TMessagesProj/jni/libyuv/source/rotate_mips.cc @@ -23,7 +23,7 @@ extern "C" { (_MIPS_SIM == _MIPS_SIM_ABI32) void TransposeWx8_DSPR2(const uint8* src, int src_stride, - uint8* dst, int dst_stride, int width) { + uint8* dst, int dst_stride, int width) { __asm__ __volatile__ ( ".set push \n" ".set noreorder \n" @@ -107,7 +107,7 @@ void TransposeWx8_DSPR2(const uint8* src, int src_stride, } void TransposeWx8_Fast_DSPR2(const uint8* src, int src_stride, - uint8* dst, int dst_stride, int width) { + uint8* dst, int dst_stride, int width) { __asm__ __volatile__ ( ".set noat \n" ".set push \n" @@ -309,9 +309,9 @@ void TransposeWx8_Fast_DSPR2(const uint8* src, int src_stride, } void TransposeUVWx8_DSPR2(const uint8* src, int src_stride, - uint8* dst_a, int dst_stride_a, - uint8* dst_b, int dst_stride_b, - int width) { + uint8* dst_a, int dst_stride_a, + uint8* dst_b, int dst_stride_b, + int width) { __asm__ __volatile__ ( ".set push \n" ".set noreorder \n" diff --git a/TMessagesProj/jni/libyuv/source/row_any.cc b/TMessagesProj/jni/libyuv/source/row_any.cc index 29b7a343d..494164fd0 100644 --- a/TMessagesProj/jni/libyuv/source/row_any.cc +++ b/TMessagesProj/jni/libyuv/source/row_any.cc @@ -466,38 +466,15 @@ ANY11(ARGBUnattenuateRow_Any_AVX2, ARGBUnattenuateRow_AVX2, 0, 4, 4, 7) #ifdef HAS_ARGBATTENUATEROW_NEON ANY11(ARGBAttenuateRow_Any_NEON, ARGBAttenuateRow_NEON, 0, 4, 4, 7) #endif +#ifdef HAS_ARGBEXTRACTALPHAROW_SSE2 +ANY11(ARGBExtractAlphaRow_Any_SSE2, ARGBExtractAlphaRow_SSE2, 0, 4, 1, 7) +#endif +#ifdef HAS_ARGBEXTRACTALPHAROW_NEON +ANY11(ARGBExtractAlphaRow_Any_NEON, ARGBExtractAlphaRow_NEON, 0, 4, 1, 15) +#endif #undef ANY11 -// Any 1 to 1 with yuvconstants -#define ANY11C(NAMEANY, ANY_SIMD, UVSHIFT, SBPP, BPP, MASK) \ - void NAMEANY(const uint8* src_ptr, uint8* dst_ptr, \ - const struct YuvConstants* yuvconstants, int width) { \ - SIMD_ALIGNED(uint8 temp[128 * 2]); \ - memset(temp, 0, 128); /* for YUY2 and msan */ \ - int r = width & MASK; \ - int n = width & ~MASK; \ - if (n > 0) { \ - ANY_SIMD(src_ptr, dst_ptr, yuvconstants, n); \ - } \ - memcpy(temp, src_ptr + (n >> UVSHIFT) * SBPP, SS(r, UVSHIFT) * SBPP); \ - ANY_SIMD(temp, temp + 128, yuvconstants, MASK + 1); \ - memcpy(dst_ptr + n * BPP, temp + 128, r * BPP); \ - } -#if defined(HAS_YUY2TOARGBROW_SSSE3) -ANY11C(YUY2ToARGBRow_Any_SSSE3, YUY2ToARGBRow_SSSE3, 1, 4, 4, 15) -ANY11C(UYVYToARGBRow_Any_SSSE3, UYVYToARGBRow_SSSE3, 1, 4, 4, 15) -#endif -#if defined(HAS_YUY2TOARGBROW_AVX2) -ANY11C(YUY2ToARGBRow_Any_AVX2, YUY2ToARGBRow_AVX2, 1, 4, 4, 31) -ANY11C(UYVYToARGBRow_Any_AVX2, UYVYToARGBRow_AVX2, 1, 4, 4, 31) -#endif -#if defined(HAS_YUY2TOARGBROW_NEON) -ANY11C(YUY2ToARGBRow_Any_NEON, YUY2ToARGBRow_NEON, 1, 4, 4, 7) -ANY11C(UYVYToARGBRow_Any_NEON, UYVYToARGBRow_NEON, 1, 4, 4, 7) -#endif -#undef ANY11C - -// Any 1 to 1 blended. +// Any 1 to 1 blended. Destination is read, modify, write. #define ANY11B(NAMEANY, ANY_SIMD, UVSHIFT, SBPP, BPP, MASK) \ void NAMEANY(const uint8* src_ptr, uint8* dst_ptr, int width) { \ SIMD_ALIGNED(uint8 temp[128 * 2]); \ @@ -516,7 +493,7 @@ ANY11C(UYVYToARGBRow_Any_NEON, UYVYToARGBRow_NEON, 1, 4, 4, 7) #ifdef HAS_ARGBCOPYALPHAROW_AVX2 ANY11B(ARGBCopyAlphaRow_Any_AVX2, ARGBCopyAlphaRow_AVX2, 0, 4, 4, 15) #endif -#ifdef HAS_ARGBCOPYYTOALPHAROW_SSE2 +#ifdef HAS_ARGBCOPYALPHAROW_SSE2 ANY11B(ARGBCopyAlphaRow_Any_SSE2, ARGBCopyAlphaRow_SSE2, 0, 4, 4, 7) #endif #ifdef HAS_ARGBCOPYYTOALPHAROW_AVX2 @@ -569,6 +546,35 @@ ANY11P(ARGBShuffleRow_Any_NEON, ARGBShuffleRow_NEON, const uint8*, 4, 4, 3) #endif #undef ANY11P +// Any 1 to 1 with yuvconstants +#define ANY11C(NAMEANY, ANY_SIMD, UVSHIFT, SBPP, BPP, MASK) \ + void NAMEANY(const uint8* src_ptr, uint8* dst_ptr, \ + const struct YuvConstants* yuvconstants, int width) { \ + SIMD_ALIGNED(uint8 temp[128 * 2]); \ + memset(temp, 0, 128); /* for YUY2 and msan */ \ + int r = width & MASK; \ + int n = width & ~MASK; \ + if (n > 0) { \ + ANY_SIMD(src_ptr, dst_ptr, yuvconstants, n); \ + } \ + memcpy(temp, src_ptr + (n >> UVSHIFT) * SBPP, SS(r, UVSHIFT) * SBPP); \ + ANY_SIMD(temp, temp + 128, yuvconstants, MASK + 1); \ + memcpy(dst_ptr + n * BPP, temp + 128, r * BPP); \ + } +#if defined(HAS_YUY2TOARGBROW_SSSE3) +ANY11C(YUY2ToARGBRow_Any_SSSE3, YUY2ToARGBRow_SSSE3, 1, 4, 4, 15) +ANY11C(UYVYToARGBRow_Any_SSSE3, UYVYToARGBRow_SSSE3, 1, 4, 4, 15) +#endif +#if defined(HAS_YUY2TOARGBROW_AVX2) +ANY11C(YUY2ToARGBRow_Any_AVX2, YUY2ToARGBRow_AVX2, 1, 4, 4, 31) +ANY11C(UYVYToARGBRow_Any_AVX2, UYVYToARGBRow_AVX2, 1, 4, 4, 31) +#endif +#if defined(HAS_YUY2TOARGBROW_NEON) +ANY11C(YUY2ToARGBRow_Any_NEON, YUY2ToARGBRow_NEON, 1, 4, 4, 7) +ANY11C(UYVYToARGBRow_Any_NEON, UYVYToARGBRow_NEON, 1, 4, 4, 7) +#endif +#undef ANY11C + // Any 1 to 1 interpolate. Takes 2 rows of source via stride. #define ANY11T(NAMEANY, ANY_SIMD, SBPP, BPP, MASK) \ void NAMEANY(uint8* dst_ptr, const uint8* src_ptr, \ diff --git a/TMessagesProj/jni/libyuv/source/row_common.cc b/TMessagesProj/jni/libyuv/source/row_common.cc index 0c47e1016..32d2f686f 100644 --- a/TMessagesProj/jni/libyuv/source/row_common.cc +++ b/TMessagesProj/jni/libyuv/source/row_common.cc @@ -2381,6 +2381,19 @@ void ARGBCopyAlphaRow_C(const uint8* src, uint8* dst, int width) { } } +void ARGBExtractAlphaRow_C(const uint8* src_argb, uint8* dst_a, int width) { + int i; + for (i = 0; i < width - 1; i += 2) { + dst_a[0] = src_argb[3]; + dst_a[1] = src_argb[7]; + dst_a += 2; + src_argb += 8; + } + if (width & 1) { + dst_a[0] = src_argb[3]; + } +} + void ARGBCopyYToAlphaRow_C(const uint8* src, uint8* dst, int width) { int i; for (i = 0; i < width - 1; i += 2) { diff --git a/TMessagesProj/jni/libyuv/source/row_gcc.cc b/TMessagesProj/jni/libyuv/source/row_gcc.cc index 866bded79..1ac7ef1aa 100644 --- a/TMessagesProj/jni/libyuv/source/row_gcc.cc +++ b/TMessagesProj/jni/libyuv/source/row_gcc.cc @@ -2936,6 +2936,33 @@ void ARGBCopyAlphaRow_AVX2(const uint8* src, uint8* dst, int width) { } #endif // HAS_ARGBCOPYALPHAROW_AVX2 +#ifdef HAS_ARGBEXTRACTALPHAROW_SSE2 +// width in pixels +void ARGBExtractAlphaRow_SSE2(const uint8* src_argb, uint8* dst_a, int width) { + asm volatile ( + LABELALIGN + "1: \n" + "movdqu " MEMACCESS(0) ", %%xmm0 \n" + "movdqu " MEMACCESS2(0x10, 0) ", %%xmm1 \n" + "lea " MEMLEA(0x20, 0) ", %0 \n" + "psrld $0x18, %%xmm0 \n" + "psrld $0x18, %%xmm1 \n" + "packssdw %%xmm1, %%xmm0 \n" + "packuswb %%xmm0, %%xmm0 \n" + "movq %%xmm0," MEMACCESS(1) " \n" + "lea " MEMLEA(0x8, 1) ", %1 \n" + "sub $0x8, %2 \n" + "jg 1b \n" + : "+r"(src_argb), // %0 + "+r"(dst_a), // %1 + "+rm"(width) // %2 + : + : "memory", "cc" + , "xmm0", "xmm1" + ); +} +#endif // HAS_ARGBEXTRACTALPHAROW_SSE2 + #ifdef HAS_ARGBCOPYYTOALPHAROW_SSE2 // width in pixels void ARGBCopyYToAlphaRow_SSE2(const uint8* src, uint8* dst, int width) { @@ -3569,7 +3596,7 @@ void BlendPlaneRow_SSSE3(const uint8* src0, const uint8* src1, "+r"(src1), // %1 "+r"(alpha), // %2 "+r"(dst), // %3 - "+r"(width) // %4 + "+rm"(width) // %4 :: "memory", "cc", "eax", "xmm0", "xmm1", "xmm2", "xmm5", "xmm6", "xmm7" ); } @@ -3626,7 +3653,7 @@ void BlendPlaneRow_AVX2(const uint8* src0, const uint8* src1, "+r"(src1), // %1 "+r"(alpha), // %2 "+r"(dst), // %3 - "+r"(width) // %4 + "+rm"(width) // %4 :: "memory", "cc", "eax", "xmm0", "xmm1", "xmm2", "xmm3", "xmm4", "xmm5", "xmm6", "xmm7" ); @@ -4909,9 +4936,9 @@ void InterpolateRow_SSSE3(uint8* dst_ptr, const uint8* src_ptr, "jg 100b \n" "99: \n" - : "+r"(dst_ptr), // %0 - "+r"(src_ptr), // %1 - "+r"(dst_width), // %2 + : "+r"(dst_ptr), // %0 + "+r"(src_ptr), // %1 + "+rm"(dst_width), // %2 "+r"(source_y_fraction) // %3 : "r"((intptr_t)(src_stride)) // %4 : "memory", "cc", "eax", NACL_R14 @@ -4987,7 +5014,7 @@ void InterpolateRow_AVX2(uint8* dst_ptr, const uint8* src_ptr, "999: \n" : "+D"(dst_ptr), // %0 "+S"(src_ptr), // %1 - "+c"(dst_width), // %2 + "+cm"(dst_width), // %2 "+r"(source_y_fraction) // %3 : "r"((intptr_t)(src_stride)) // %4 : "memory", "cc", "eax", NACL_R14 diff --git a/TMessagesProj/jni/libyuv/source/row_mips.cc b/TMessagesProj/jni/libyuv/source/row_mips.cc index ca6ecce94..285f0b5ad 100644 --- a/TMessagesProj/jni/libyuv/source/row_mips.cc +++ b/TMessagesProj/jni/libyuv/source/row_mips.cc @@ -381,7 +381,7 @@ void CopyRow_MIPS(const uint8* src, uint8* dst, int count) { (_MIPS_SIM == _MIPS_SIM_ABI32) && (__mips_isa_rev < 6) void SplitUVRow_DSPR2(const uint8* src_uv, uint8* dst_u, uint8* dst_v, - int width) { + int width) { __asm__ __volatile__ ( ".set push \n" ".set noreorder \n" @@ -497,7 +497,7 @@ void MirrorRow_DSPR2(const uint8* src, uint8* dst, int width) { } void MirrorUVRow_DSPR2(const uint8* src_uv, uint8* dst_u, uint8* dst_v, - int width) { + int width) { int x; int y; __asm__ __volatile__ ( @@ -654,11 +654,11 @@ void MirrorUVRow_DSPR2(const uint8* src_uv, uint8* dst_u, uint8* dst_v, // TODO(fbarchard): accept yuv conversion constants. void I422ToARGBRow_DSPR2(const uint8* y_buf, - const uint8* u_buf, - const uint8* v_buf, - uint8* rgb_buf, - const struct YuvConstants* yuvconstants, - int width) { + const uint8* u_buf, + const uint8* v_buf, + uint8* rgb_buf, + const struct YuvConstants* yuvconstants, + int width) { __asm__ __volatile__ ( ".set push \n" ".set noreorder \n" @@ -717,8 +717,8 @@ void I422ToARGBRow_DSPR2(const uint8* y_buf, // Bilinear filter 8x2 -> 8x1 void InterpolateRow_DSPR2(uint8* dst_ptr, const uint8* src_ptr, - ptrdiff_t src_stride, int dst_width, - int source_y_fraction) { + ptrdiff_t src_stride, int dst_width, + int source_y_fraction) { int y0_fraction = 256 - source_y_fraction; const uint8* src_ptr1 = src_ptr + src_stride; diff --git a/TMessagesProj/jni/libyuv/source/row_neon.cc b/TMessagesProj/jni/libyuv/source/row_neon.cc index 91d6aa857..909df060c 100644 --- a/TMessagesProj/jni/libyuv/source/row_neon.cc +++ b/TMessagesProj/jni/libyuv/source/row_neon.cc @@ -1298,6 +1298,24 @@ void ARGBToYRow_NEON(const uint8* src_argb, uint8* dst_y, int width) { ); } +void ARGBExtractAlphaRow_NEON(const uint8* src_argb, uint8* dst_a, int width) { + asm volatile ( + "1: \n" + MEMACCESS(0) + "vld4.8 {d0, d2, d4, d6}, [%0]! \n" // load 8 ARGB pixels + "vld4.8 {d1, d3, d5, d7}, [%0]! \n" // load next 8 ARGB pixels + "subs %2, %2, #16 \n" // 16 processed per loop + MEMACCESS(1) + "vst1.8 {q3}, [%1]! \n" // store 16 A's. + "bgt 1b \n" + : "+r"(src_argb), // %0 + "+r"(dst_a), // %1 + "+r"(width) // %2 + : + : "cc", "memory", "q0", "q1", "q2", "q3" // Clobber List + ); +} + void ARGBToYJRow_NEON(const uint8* src_argb, uint8* dst_y, int width) { asm volatile ( "vmov.u8 d24, #15 \n" // B * 0.11400 coefficient @@ -2565,8 +2583,6 @@ void ARGBColorMatrixRow_NEON(const uint8* src_argb, uint8* dst_argb, ); } -// TODO(fbarchard): fix vqshrun in ARGBMultiplyRow_NEON and reenable. -#ifdef HAS_ARGBMULTIPLYROW_NEON // Multiply 2 rows of ARGB pixels together, 8 pixels at a time. void ARGBMultiplyRow_NEON(const uint8* src_argb0, const uint8* src_argb1, uint8* dst_argb, int width) { @@ -2598,7 +2614,6 @@ void ARGBMultiplyRow_NEON(const uint8* src_argb0, const uint8* src_argb1, : "cc", "memory", "q0", "q1", "q2", "q3" ); } -#endif // HAS_ARGBMULTIPLYROW_NEON // Add 2 rows of ARGB pixels together, 8 pixels at a time. void ARGBAddRow_NEON(const uint8* src_argb0, const uint8* src_argb1, diff --git a/TMessagesProj/jni/libyuv/source/row_neon64.cc b/TMessagesProj/jni/libyuv/source/row_neon64.cc index ee42af12e..6375d4f55 100644 --- a/TMessagesProj/jni/libyuv/source/row_neon64.cc +++ b/TMessagesProj/jni/libyuv/source/row_neon64.cc @@ -127,7 +127,6 @@ extern "C" { "sqshrun " #vG ".8b, " #vG ".8h, #6 \n" /* G */ \ "sqshrun " #vR ".8b, " #vR ".8h, #6 \n" /* R */ \ -#ifdef HAS_I444TOARGBROW_NEON void I444ToARGBRow_NEON(const uint8* src_y, const uint8* src_u, const uint8* src_v, @@ -157,9 +156,7 @@ void I444ToARGBRow_NEON(const uint8* src_y, "v21", "v22", "v23", "v24", "v25", "v26", "v27", "v28", "v29", "v30" ); } -#endif // HAS_I444TOARGBROW_NEON -#ifdef HAS_I422TOARGBROW_NEON void I422ToARGBRow_NEON(const uint8* src_y, const uint8* src_u, const uint8* src_v, @@ -189,9 +186,7 @@ void I422ToARGBRow_NEON(const uint8* src_y, "v21", "v22", "v23", "v24", "v25", "v26", "v27", "v28", "v29", "v30" ); } -#endif // HAS_I422TOARGBROW_NEON -#ifdef HAS_I422ALPHATOARGBROW_NEON void I422AlphaToARGBRow_NEON(const uint8* src_y, const uint8* src_u, const uint8* src_v, @@ -224,9 +219,7 @@ void I422AlphaToARGBRow_NEON(const uint8* src_y, "v21", "v22", "v23", "v24", "v25", "v26", "v27", "v28", "v29", "v30" ); } -#endif // HAS_I422ALPHATOARGBROW_NEON -#ifdef HAS_I411TOARGBROW_NEON void I411ToARGBRow_NEON(const uint8* src_y, const uint8* src_u, const uint8* src_v, @@ -256,9 +249,7 @@ void I411ToARGBRow_NEON(const uint8* src_y, "v21", "v22", "v23", "v24", "v25", "v26", "v27", "v28", "v29", "v30" ); } -#endif // HAS_I411TOARGBROW_NEON -#ifdef HAS_I422TORGBAROW_NEON void I422ToRGBARow_NEON(const uint8* src_y, const uint8* src_u, const uint8* src_v, @@ -288,9 +279,7 @@ void I422ToRGBARow_NEON(const uint8* src_y, "v21", "v22", "v23", "v24", "v25", "v26", "v27", "v28", "v29", "v30" ); } -#endif // HAS_I422TORGBAROW_NEON -#ifdef HAS_I422TORGB24ROW_NEON void I422ToRGB24Row_NEON(const uint8* src_y, const uint8* src_u, const uint8* src_v, @@ -319,7 +308,6 @@ void I422ToRGB24Row_NEON(const uint8* src_y, "v21", "v22", "v23", "v24", "v25", "v26", "v27", "v28", "v29", "v30" ); } -#endif // HAS_I422TORGB24ROW_NEON #define ARGBTORGB565 \ "shll v0.8h, v22.8b, #8 \n" /* R */ \ @@ -328,7 +316,6 @@ void I422ToRGB24Row_NEON(const uint8* src_y, "sri v0.8h, v21.8h, #5 \n" /* RG */ \ "sri v0.8h, v20.8h, #11 \n" /* RGB */ -#ifdef HAS_I422TORGB565ROW_NEON void I422ToRGB565Row_NEON(const uint8* src_y, const uint8* src_u, const uint8* src_v, @@ -358,7 +345,6 @@ void I422ToRGB565Row_NEON(const uint8* src_y, "v21", "v22", "v23", "v24", "v25", "v26", "v27", "v28", "v29", "v30" ); } -#endif // HAS_I422TORGB565ROW_NEON #define ARGBTOARGB1555 \ "shll v0.8h, v23.8b, #8 \n" /* A */ \ @@ -369,7 +355,6 @@ void I422ToRGB565Row_NEON(const uint8* src_y, "sri v0.8h, v21.8h, #6 \n" /* ARG */ \ "sri v0.8h, v20.8h, #11 \n" /* ARGB */ -#ifdef HAS_I422TOARGB1555ROW_NEON void I422ToARGB1555Row_NEON(const uint8* src_y, const uint8* src_u, const uint8* src_v, @@ -400,7 +385,6 @@ void I422ToARGB1555Row_NEON(const uint8* src_y, "v21", "v22", "v23", "v24", "v25", "v26", "v27", "v28", "v29", "v30" ); } -#endif // HAS_I422TOARGB1555ROW_NEON #define ARGBTOARGB4444 \ /* Input v20.8b<=B, v21.8b<=G, v22.8b<=R, v23.8b<=A, v4.8b<=0x0f */ \ @@ -412,7 +396,6 @@ void I422ToARGB1555Row_NEON(const uint8* src_y, "orr v1.8b, v22.8b, v23.8b \n" /* RA */ \ "zip1 v0.16b, v0.16b, v1.16b \n" /* BGRA */ -#ifdef HAS_I422TOARGB4444ROW_NEON void I422ToARGB4444Row_NEON(const uint8* src_y, const uint8* src_u, const uint8* src_v, @@ -444,13 +427,10 @@ void I422ToARGB4444Row_NEON(const uint8* src_y, "v21", "v22", "v23", "v24", "v25", "v26", "v27", "v28", "v29", "v30" ); } -#endif // HAS_I422TOARGB4444ROW_NEON -#ifdef HAS_I400TOARGBROW_NEON void I400ToARGBRow_NEON(const uint8* src_y, uint8* dst_argb, int width) { - int64 width64 = (int64)(width); asm volatile ( YUVTORGB_SETUP "movi v23.8b, #255 \n" @@ -463,7 +443,7 @@ void I400ToARGBRow_NEON(const uint8* src_y, "b.gt 1b \n" : "+r"(src_y), // %0 "+r"(dst_argb), // %1 - "+r"(width64) // %2 + "+r"(width) // %2 : [kUVToRB]"r"(&kYuvI601Constants.kUVToRB), [kUVToG]"r"(&kYuvI601Constants.kUVToG), [kUVBiasBGR]"r"(&kYuvI601Constants.kUVBiasBGR), @@ -472,9 +452,7 @@ void I400ToARGBRow_NEON(const uint8* src_y, "v21", "v22", "v23", "v24", "v25", "v26", "v27", "v28", "v29", "v30" ); } -#endif // HAS_I400TOARGBROW_NEON -#ifdef HAS_J400TOARGBROW_NEON void J400ToARGBRow_NEON(const uint8* src_y, uint8* dst_argb, int width) { @@ -496,9 +474,7 @@ void J400ToARGBRow_NEON(const uint8* src_y, : "cc", "memory", "v20", "v21", "v22", "v23" ); } -#endif // HAS_J400TOARGBROW_NEON -#ifdef HAS_NV12TOARGBROW_NEON void NV12ToARGBRow_NEON(const uint8* src_y, const uint8* src_uv, uint8* dst_argb, @@ -526,9 +502,7 @@ void NV12ToARGBRow_NEON(const uint8* src_y, "v21", "v22", "v23", "v24", "v25", "v26", "v27", "v28", "v29", "v30" ); } -#endif // HAS_NV12TOARGBROW_NEON -#ifdef HAS_NV12TOARGBROW_NEON void NV21ToARGBRow_NEON(const uint8* src_y, const uint8* src_vu, uint8* dst_argb, @@ -556,9 +530,7 @@ void NV21ToARGBRow_NEON(const uint8* src_y, "v21", "v22", "v23", "v24", "v25", "v26", "v27", "v28", "v29", "v30" ); } -#endif // HAS_NV12TOARGBROW_NEON -#ifdef HAS_NV12TORGB565ROW_NEON void NV12ToRGB565Row_NEON(const uint8* src_y, const uint8* src_uv, uint8* dst_rgb565, @@ -586,14 +558,11 @@ void NV12ToRGB565Row_NEON(const uint8* src_y, "v21", "v22", "v23", "v24", "v25", "v26", "v27", "v28", "v29", "v30" ); } -#endif // HAS_NV12TORGB565ROW_NEON -#ifdef HAS_YUY2TOARGBROW_NEON void YUY2ToARGBRow_NEON(const uint8* src_yuy2, uint8* dst_argb, const struct YuvConstants* yuvconstants, int width) { - int64 width64 = (int64)(width); asm volatile ( YUVTORGB_SETUP "movi v23.8b, #255 \n" @@ -606,7 +575,7 @@ void YUY2ToARGBRow_NEON(const uint8* src_yuy2, "b.gt 1b \n" : "+r"(src_yuy2), // %0 "+r"(dst_argb), // %1 - "+r"(width64) // %2 + "+r"(width) // %2 : [kUVToRB]"r"(&yuvconstants->kUVToRB), [kUVToG]"r"(&yuvconstants->kUVToG), [kUVBiasBGR]"r"(&yuvconstants->kUVBiasBGR), @@ -615,14 +584,11 @@ void YUY2ToARGBRow_NEON(const uint8* src_yuy2, "v21", "v22", "v23", "v24", "v25", "v26", "v27", "v28", "v29", "v30" ); } -#endif // HAS_YUY2TOARGBROW_NEON -#ifdef HAS_UYVYTOARGBROW_NEON void UYVYToARGBRow_NEON(const uint8* src_uyvy, uint8* dst_argb, const struct YuvConstants* yuvconstants, int width) { - int64 width64 = (int64)(width); asm volatile ( YUVTORGB_SETUP "movi v23.8b, #255 \n" @@ -635,7 +601,7 @@ void UYVYToARGBRow_NEON(const uint8* src_uyvy, "b.gt 1b \n" : "+r"(src_uyvy), // %0 "+r"(dst_argb), // %1 - "+r"(width64) // %2 + "+r"(width) // %2 : [kUVToRB]"r"(&yuvconstants->kUVToRB), [kUVToG]"r"(&yuvconstants->kUVToG), [kUVBiasBGR]"r"(&yuvconstants->kUVBiasBGR), @@ -644,10 +610,8 @@ void UYVYToARGBRow_NEON(const uint8* src_uyvy, "v21", "v22", "v23", "v24", "v25", "v26", "v27", "v28", "v29", "v30" ); } -#endif // HAS_UYVYTOARGBROW_NEON // Reads 16 pairs of UV and write even values to dst_u and odd to dst_v. -#ifdef HAS_SPLITUVROW_NEON void SplitUVRow_NEON(const uint8* src_uv, uint8* dst_u, uint8* dst_v, int width) { asm volatile ( @@ -668,10 +632,8 @@ void SplitUVRow_NEON(const uint8* src_uv, uint8* dst_u, uint8* dst_v, : "cc", "memory", "v0", "v1" // Clobber List ); } -#endif // HAS_SPLITUVROW_NEON // Reads 16 U's and V's and writes out 16 pairs of UV. -#ifdef HAS_MERGEUVROW_NEON void MergeUVRow_NEON(const uint8* src_u, const uint8* src_v, uint8* dst_uv, int width) { asm volatile ( @@ -693,10 +655,8 @@ void MergeUVRow_NEON(const uint8* src_u, const uint8* src_v, uint8* dst_uv, : "cc", "memory", "v0", "v1" // Clobber List ); } -#endif // HAS_MERGEUVROW_NEON // Copy multiple of 32. vld4.8 allow unaligned and is fastest on a15. -#ifdef HAS_COPYROW_NEON void CopyRow_NEON(const uint8* src, uint8* dst, int count) { asm volatile ( "1: \n" @@ -713,17 +673,16 @@ void CopyRow_NEON(const uint8* src, uint8* dst, int count) { : "cc", "memory", "v0", "v1", "v2", "v3" // Clobber List ); } -#endif // HAS_COPYROW_NEON // SetRow writes 'count' bytes using an 8 bit value repeated. void SetRow_NEON(uint8* dst, uint8 v8, int count) { asm volatile ( "dup v0.16b, %w2 \n" // duplicate 16 bytes "1: \n" - "subs %w1, %w1, #16 \n" // 16 bytes per loop + "subs %w1, %w1, #16 \n" // 16 bytes per loop MEMACCESS(0) "st1 {v0.16b}, [%0], #16 \n" // store - "b.gt 1b \n" + "b.gt 1b \n" : "+r"(dst), // %0 "+r"(count) // %1 : "r"(v8) // %2 @@ -735,10 +694,10 @@ void ARGBSetRow_NEON(uint8* dst, uint32 v32, int count) { asm volatile ( "dup v0.4s, %w2 \n" // duplicate 4 ints "1: \n" - "subs %w1, %w1, #4 \n" // 4 ints per loop + "subs %w1, %w1, #4 \n" // 4 ints per loop MEMACCESS(0) "st1 {v0.16b}, [%0], #16 \n" // store - "b.gt 1b \n" + "b.gt 1b \n" : "+r"(dst), // %0 "+r"(count) // %1 : "r"(v32) // %2 @@ -746,18 +705,15 @@ void ARGBSetRow_NEON(uint8* dst, uint32 v32, int count) { ); } -#ifdef HAS_MIRRORROW_NEON void MirrorRow_NEON(const uint8* src, uint8* dst, int width) { - int64 width64 = (int64) width; asm volatile ( // Start at end of source row. - "add %0, %0, %2 \n" + "add %0, %0, %w2, sxtw \n" "sub %0, %0, #16 \n" - "1: \n" MEMACCESS(0) "ld1 {v0.16b}, [%0], %3 \n" // src -= 16 - "subs %2, %2, #16 \n" // 16 pixels per loop. + "subs %w2, %w2, #16 \n" // 16 pixels per loop. "rev64 v0.16b, v0.16b \n" MEMACCESS(1) "st1 {v0.D}[1], [%1], #8 \n" // dst += 16 @@ -766,26 +722,22 @@ void MirrorRow_NEON(const uint8* src, uint8* dst, int width) { "b.gt 1b \n" : "+r"(src), // %0 "+r"(dst), // %1 - "+r"(width64) // %2 + "+r"(width) // %2 : "r"((ptrdiff_t)-16) // %3 : "cc", "memory", "v0" ); } -#endif // HAS_MIRRORROW_NEON -#ifdef HAS_MIRRORUVROW_NEON void MirrorUVRow_NEON(const uint8* src_uv, uint8* dst_u, uint8* dst_v, int width) { - int64 width64 = (int64) width; asm volatile ( // Start at end of source row. - "add %0, %0, %3, lsl #1 \n" + "add %0, %0, %w3, sxtw #1 \n" "sub %0, %0, #16 \n" - "1: \n" MEMACCESS(0) "ld2 {v0.8b, v1.8b}, [%0], %4 \n" // src -= 16 - "subs %3, %3, #8 \n" // 8 pixels per loop. + "subs %w3, %w3, #8 \n" // 8 pixels per loop. "rev64 v0.8b, v0.8b \n" "rev64 v1.8b, v1.8b \n" MEMACCESS(1) @@ -796,25 +748,21 @@ void MirrorUVRow_NEON(const uint8* src_uv, uint8* dst_u, uint8* dst_v, : "+r"(src_uv), // %0 "+r"(dst_u), // %1 "+r"(dst_v), // %2 - "+r"(width64) // %3 + "+r"(width) // %3 : "r"((ptrdiff_t)-16) // %4 : "cc", "memory", "v0", "v1" ); } -#endif // HAS_MIRRORUVROW_NEON -#ifdef HAS_ARGBMIRRORROW_NEON void ARGBMirrorRow_NEON(const uint8* src, uint8* dst, int width) { - int64 width64 = (int64) width; asm volatile ( - // Start at end of source row. - "add %0, %0, %2, lsl #2 \n" + // Start at end of source row. + "add %0, %0, %w2, sxtw #2 \n" "sub %0, %0, #16 \n" - "1: \n" MEMACCESS(0) "ld1 {v0.16b}, [%0], %3 \n" // src -= 16 - "subs %2, %2, #4 \n" // 4 pixels per loop. + "subs %w2, %w2, #4 \n" // 4 pixels per loop. "rev64 v0.4s, v0.4s \n" MEMACCESS(1) "st1 {v0.D}[1], [%1], #8 \n" // dst += 16 @@ -823,14 +771,12 @@ void ARGBMirrorRow_NEON(const uint8* src, uint8* dst, int width) { "b.gt 1b \n" : "+r"(src), // %0 "+r"(dst), // %1 - "+r"(width64) // %2 + "+r"(width) // %2 : "r"((ptrdiff_t)-16) // %3 : "cc", "memory", "v0" ); } -#endif // HAS_ARGBMIRRORROW_NEON -#ifdef HAS_RGB24TOARGBROW_NEON void RGB24ToARGBRow_NEON(const uint8* src_rgb24, uint8* dst_argb, int width) { asm volatile ( "movi v4.8b, #255 \n" // Alpha @@ -843,14 +789,12 @@ void RGB24ToARGBRow_NEON(const uint8* src_rgb24, uint8* dst_argb, int width) { "b.gt 1b \n" : "+r"(src_rgb24), // %0 "+r"(dst_argb), // %1 - "+r"(width) // %2 + "+r"(width) // %2 : : "cc", "memory", "v1", "v2", "v3", "v4" // Clobber List ); } -#endif // HAS_RGB24TOARGBROW_NEON -#ifdef HAS_RAWTOARGBROW_NEON void RAWToARGBRow_NEON(const uint8* src_raw, uint8* dst_argb, int width) { asm volatile ( "movi v5.8b, #255 \n" // Alpha @@ -865,12 +809,11 @@ void RAWToARGBRow_NEON(const uint8* src_raw, uint8* dst_argb, int width) { "b.gt 1b \n" : "+r"(src_raw), // %0 "+r"(dst_argb), // %1 - "+r"(width) // %2 + "+r"(width) // %2 : : "cc", "memory", "v0", "v1", "v2", "v3", "v4", "v5" // Clobber List ); } -#endif // HAS_RAWTOARGBROW_NEON void RAWToRGB24Row_NEON(const uint8* src_raw, uint8* dst_rgb24, int width) { asm volatile ( @@ -904,7 +847,6 @@ void RAWToRGB24Row_NEON(const uint8* src_raw, uint8* dst_rgb24, int width) { "orr v0.16b, v0.16b, v2.16b \n" /* R,B */ \ "dup v2.2D, v0.D[1] \n" /* R */ -#ifdef HAS_RGB565TOARGBROW_NEON void RGB565ToARGBRow_NEON(const uint8* src_rgb565, uint8* dst_argb, int width) { asm volatile ( "movi v3.8b, #255 \n" // Alpha @@ -923,7 +865,6 @@ void RGB565ToARGBRow_NEON(const uint8* src_rgb565, uint8* dst_argb, int width) { : "cc", "memory", "v0", "v1", "v2", "v3", "v4", "v6" // Clobber List ); } -#endif // HAS_RGB565TOARGBROW_NEON #define ARGB1555TOARGB \ "ushr v2.8h, v0.8h, #10 \n" /* R xxxRRRRR */ \ @@ -962,7 +903,6 @@ void RGB565ToARGBRow_NEON(const uint8* src_rgb565, uint8* dst_argb, int width) { "orr v2.16b, v1.16b, v3.16b \n" /* R */ \ "dup v1.2D, v0.D[1] \n" /* G */ \ -#ifdef HAS_ARGB1555TOARGBROW_NEON void ARGB1555ToARGBRow_NEON(const uint8* src_argb1555, uint8* dst_argb, int width) { asm volatile ( @@ -982,7 +922,6 @@ void ARGB1555ToARGBRow_NEON(const uint8* src_argb1555, uint8* dst_argb, : "cc", "memory", "v0", "v1", "v2", "v3" // Clobber List ); } -#endif // HAS_ARGB1555TOARGBROW_NEON #define ARGB4444TOARGB \ "shrn v1.8b, v0.8h, #8 \n" /* v1(l) AR */ \ @@ -996,7 +935,6 @@ void ARGB1555ToARGBRow_NEON(const uint8* src_argb1555, uint8* dst_argb, "dup v0.2D, v2.D[1] \n" \ "dup v1.2D, v3.D[1] \n" -#ifdef HAS_ARGB4444TOARGBROW_NEON void ARGB4444ToARGBRow_NEON(const uint8* src_argb4444, uint8* dst_argb, int width) { asm volatile ( @@ -1015,9 +953,7 @@ void ARGB4444ToARGBRow_NEON(const uint8* src_argb4444, uint8* dst_argb, : "cc", "memory", "v0", "v1", "v2", "v3", "v4" // Clobber List ); } -#endif // HAS_ARGB4444TOARGBROW_NEON -#ifdef HAS_ARGBTORGB24ROW_NEON void ARGBToRGB24Row_NEON(const uint8* src_argb, uint8* dst_rgb24, int width) { asm volatile ( "1: \n" @@ -1034,9 +970,7 @@ void ARGBToRGB24Row_NEON(const uint8* src_argb, uint8* dst_rgb24, int width) { : "cc", "memory", "v1", "v2", "v3", "v4" // Clobber List ); } -#endif // HAS_ARGBTORGB24ROW_NEON -#ifdef HAS_ARGBTORAWROW_NEON void ARGBToRAWRow_NEON(const uint8* src_argb, uint8* dst_raw, int width) { asm volatile ( "1: \n" @@ -1055,9 +989,7 @@ void ARGBToRAWRow_NEON(const uint8* src_argb, uint8* dst_raw, int width) { : "cc", "memory", "v1", "v2", "v3", "v4", "v5" // Clobber List ); } -#endif // HAS_ARGBTORAWROW_NEON -#ifdef HAS_YUY2TOYROW_NEON void YUY2ToYRow_NEON(const uint8* src_yuy2, uint8* dst_y, int width) { asm volatile ( "1: \n" @@ -1074,9 +1006,7 @@ void YUY2ToYRow_NEON(const uint8* src_yuy2, uint8* dst_y, int width) { : "cc", "memory", "v0", "v1" // Clobber List ); } -#endif // HAS_YUY2TOYROW_NEON -#ifdef HAS_UYVYTOYROW_NEON void UYVYToYRow_NEON(const uint8* src_uyvy, uint8* dst_y, int width) { asm volatile ( "1: \n" @@ -1093,9 +1023,7 @@ void UYVYToYRow_NEON(const uint8* src_uyvy, uint8* dst_y, int width) { : "cc", "memory", "v0", "v1" // Clobber List ); } -#endif // HAS_UYVYTOYROW_NEON -#ifdef HAS_YUY2TOUV422ROW_NEON void YUY2ToUV422Row_NEON(const uint8* src_yuy2, uint8* dst_u, uint8* dst_v, int width) { asm volatile ( @@ -1116,9 +1044,7 @@ void YUY2ToUV422Row_NEON(const uint8* src_yuy2, uint8* dst_u, uint8* dst_v, : "cc", "memory", "v0", "v1", "v2", "v3" // Clobber List ); } -#endif // HAS_YUY2TOUV422ROW_NEON -#ifdef HAS_UYVYTOUV422ROW_NEON void UYVYToUV422Row_NEON(const uint8* src_uyvy, uint8* dst_u, uint8* dst_v, int width) { asm volatile ( @@ -1139,9 +1065,7 @@ void UYVYToUV422Row_NEON(const uint8* src_uyvy, uint8* dst_u, uint8* dst_v, : "cc", "memory", "v0", "v1", "v2", "v3" // Clobber List ); } -#endif // HAS_UYVYTOUV422ROW_NEON -#ifdef HAS_YUY2TOUVROW_NEON void YUY2ToUVRow_NEON(const uint8* src_yuy2, int stride_yuy2, uint8* dst_u, uint8* dst_v, int width) { const uint8* src_yuy2b = src_yuy2 + stride_yuy2; @@ -1169,9 +1093,7 @@ void YUY2ToUVRow_NEON(const uint8* src_yuy2, int stride_yuy2, "v5", "v6", "v7" // Clobber List ); } -#endif // HAS_YUY2TOUVROW_NEON -#ifdef HAS_UYVYTOUVROW_NEON void UYVYToUVRow_NEON(const uint8* src_uyvy, int stride_uyvy, uint8* dst_u, uint8* dst_v, int width) { const uint8* src_uyvyb = src_uyvy + stride_uyvy; @@ -1199,10 +1121,8 @@ void UYVYToUVRow_NEON(const uint8* src_uyvy, int stride_uyvy, "v5", "v6", "v7" // Clobber List ); } -#endif // HAS_UYVYTOUVROW_NEON // For BGRAToARGB, ABGRToARGB, RGBAToARGB, and ARGBToRGBA. -#ifdef HAS_ARGBSHUFFLEROW_NEON void ARGBShuffleRow_NEON(const uint8* src_argb, uint8* dst_argb, const uint8* shuffler, int width) { asm volatile ( @@ -1223,9 +1143,7 @@ void ARGBShuffleRow_NEON(const uint8* src_argb, uint8* dst_argb, : "cc", "memory", "v0", "v1", "v2" // Clobber List ); } -#endif // HAS_ARGBSHUFFLEROW_NEON -#ifdef HAS_I422TOYUY2ROW_NEON void I422ToYUY2Row_NEON(const uint8* src_y, const uint8* src_u, const uint8* src_v, @@ -1252,9 +1170,7 @@ void I422ToYUY2Row_NEON(const uint8* src_y, : "cc", "memory", "v0", "v1", "v2", "v3" ); } -#endif // HAS_I422TOYUY2ROW_NEON -#ifdef HAS_I422TOUYVYROW_NEON void I422ToUYVYRow_NEON(const uint8* src_y, const uint8* src_u, const uint8* src_v, @@ -1281,9 +1197,7 @@ void I422ToUYVYRow_NEON(const uint8* src_y, : "cc", "memory", "v0", "v1", "v2", "v3" ); } -#endif // HAS_I422TOUYVYROW_NEON -#ifdef HAS_ARGBTORGB565ROW_NEON void ARGBToRGB565Row_NEON(const uint8* src_argb, uint8* dst_rgb565, int width) { asm volatile ( "1: \n" @@ -1301,9 +1215,7 @@ void ARGBToRGB565Row_NEON(const uint8* src_argb, uint8* dst_rgb565, int width) { : "cc", "memory", "v0", "v20", "v21", "v22", "v23" ); } -#endif // HAS_ARGBTORGB565ROW_NEON -#ifdef HAS_ARGBTORGB565DITHERROW_NEON void ARGBToRGB565DitherRow_NEON(const uint8* src_argb, uint8* dst_rgb, const uint32 dither4, int width) { asm volatile ( @@ -1326,9 +1238,7 @@ void ARGBToRGB565DitherRow_NEON(const uint8* src_argb, uint8* dst_rgb, : "cc", "memory", "v0", "v1", "v20", "v21", "v22", "v23" ); } -#endif // HAS_ARGBTORGB565ROW_NEON -#ifdef HAS_ARGBTOARGB1555ROW_NEON void ARGBToARGB1555Row_NEON(const uint8* src_argb, uint8* dst_argb1555, int width) { asm volatile ( @@ -1347,9 +1257,7 @@ void ARGBToARGB1555Row_NEON(const uint8* src_argb, uint8* dst_argb1555, : "cc", "memory", "v0", "v20", "v21", "v22", "v23" ); } -#endif // HAS_ARGBTOARGB1555ROW_NEON -#ifdef HAS_ARGBTOARGB4444ROW_NEON void ARGBToARGB4444Row_NEON(const uint8* src_argb, uint8* dst_argb4444, int width) { asm volatile ( @@ -1369,9 +1277,7 @@ void ARGBToARGB4444Row_NEON(const uint8* src_argb, uint8* dst_argb4444, : "cc", "memory", "v0", "v1", "v4", "v20", "v21", "v22", "v23" ); } -#endif // HAS_ARGBTOARGB4444ROW_NEON -#ifdef HAS_ARGBTOYROW_NEON void ARGBToYRow_NEON(const uint8* src_argb, uint8* dst_y, int width) { asm volatile ( "movi v4.8b, #13 \n" // B * 0.1016 coefficient @@ -1397,9 +1303,24 @@ void ARGBToYRow_NEON(const uint8* src_argb, uint8* dst_y, int width) { : "cc", "memory", "v0", "v1", "v2", "v3", "v4", "v5", "v6", "v7" ); } -#endif // HAS_ARGBTOYROW_NEON -#ifdef HAS_ARGBTOYJROW_NEON +void ARGBExtractAlphaRow_NEON(const uint8* src_argb, uint8* dst_a, int width) { + asm volatile ( + "1: \n" + MEMACCESS(0) + "ld4 {v0.16b,v1.16b,v2.16b,v3.16b}, [%0], #64 \n" // load row 16 pixels + "subs %w2, %w2, #16 \n" // 16 processed per loop + MEMACCESS(1) + "st1 {v3.16b}, [%1], #16 \n" // store 16 A's. + "b.gt 1b \n" + : "+r"(src_argb), // %0 + "+r"(dst_a), // %1 + "+r"(width) // %2 + : + : "cc", "memory", "v0", "v1", "v2", "v3" // Clobber List + ); +} + void ARGBToYJRow_NEON(const uint8* src_argb, uint8* dst_y, int width) { asm volatile ( "movi v4.8b, #15 \n" // B * 0.11400 coefficient @@ -1423,10 +1344,8 @@ void ARGBToYJRow_NEON(const uint8* src_argb, uint8* dst_y, int width) { : "cc", "memory", "v0", "v1", "v2", "v3", "v4", "v5", "v6" ); } -#endif // HAS_ARGBTOYJROW_NEON // 8x1 pixels. -#ifdef HAS_ARGBTOUV444ROW_NEON void ARGBToUV444Row_NEON(const uint8* src_argb, uint8* dst_u, uint8* dst_v, int width) { asm volatile ( @@ -1467,7 +1386,6 @@ void ARGBToUV444Row_NEON(const uint8* src_argb, uint8* dst_u, uint8* dst_v, "v24", "v25", "v26", "v27", "v28", "v29" ); } -#endif // HAS_ARGBTOUV444ROW_NEON #define RGBTOUV_SETUP_REG \ "movi v20.8h, #56, lsl #0 \n" /* UB/VR coefficient (0.875) / 2 */ \ @@ -1478,7 +1396,6 @@ void ARGBToUV444Row_NEON(const uint8* src_argb, uint8* dst_u, uint8* dst_v, "movi v25.16b, #0x80 \n" /* 128.5 (0x8080 in 16-bit) */ // 32x1 pixels -> 8x1. width is number of argb pixels. e.g. 32. -#ifdef HAS_ARGBTOUV411ROW_NEON void ARGBToUV411Row_NEON(const uint8* src_argb, uint8* dst_u, uint8* dst_v, int width) { asm volatile ( @@ -1528,7 +1445,6 @@ void ARGBToUV411Row_NEON(const uint8* src_argb, uint8* dst_u, uint8* dst_v, "v20", "v21", "v22", "v23", "v24", "v25" ); } -#endif // HAS_ARGBTOUV411ROW_NEON // 16x2 pixels -> 8x1. width is number of argb pixels. e.g. 16. #define RGBTOUV(QB, QG, QR) \ @@ -1546,7 +1462,6 @@ void ARGBToUV411Row_NEON(const uint8* src_argb, uint8* dst_u, uint8* dst_v, // TODO(fbarchard): Consider vhadd vertical, then vpaddl horizontal, avoid shr. // TODO(fbarchard): consider ptrdiff_t for all strides. -#ifdef HAS_ARGBTOUVROW_NEON void ARGBToUVRow_NEON(const uint8* src_argb, int src_stride_argb, uint8* dst_u, uint8* dst_v, int width) { const uint8* src_argb_1 = src_argb + src_stride_argb; @@ -1586,10 +1501,8 @@ void ARGBToUVRow_NEON(const uint8* src_argb, int src_stride_argb, "v20", "v21", "v22", "v23", "v24", "v25" ); } -#endif // HAS_ARGBTOUVROW_NEON // TODO(fbarchard): Subsample match C code. -#ifdef HAS_ARGBTOUVJROW_NEON void ARGBToUVJRow_NEON(const uint8* src_argb, int src_stride_argb, uint8* dst_u, uint8* dst_v, int width) { const uint8* src_argb_1 = src_argb + src_stride_argb; @@ -1633,9 +1546,7 @@ void ARGBToUVJRow_NEON(const uint8* src_argb, int src_stride_argb, "v20", "v21", "v22", "v23", "v24", "v25" ); } -#endif // HAS_ARGBTOUVJROW_NEON -#ifdef HAS_BGRATOUVROW_NEON void BGRAToUVRow_NEON(const uint8* src_bgra, int src_stride_bgra, uint8* dst_u, uint8* dst_v, int width) { const uint8* src_bgra_1 = src_bgra + src_stride_bgra; @@ -1674,9 +1585,7 @@ void BGRAToUVRow_NEON(const uint8* src_bgra, int src_stride_bgra, "v20", "v21", "v22", "v23", "v24", "v25" ); } -#endif // HAS_BGRATOUVROW_NEON -#ifdef HAS_ABGRTOUVROW_NEON void ABGRToUVRow_NEON(const uint8* src_abgr, int src_stride_abgr, uint8* dst_u, uint8* dst_v, int width) { const uint8* src_abgr_1 = src_abgr + src_stride_abgr; @@ -1715,9 +1624,7 @@ void ABGRToUVRow_NEON(const uint8* src_abgr, int src_stride_abgr, "v20", "v21", "v22", "v23", "v24", "v25" ); } -#endif // HAS_ABGRTOUVROW_NEON -#ifdef HAS_RGBATOUVROW_NEON void RGBAToUVRow_NEON(const uint8* src_rgba, int src_stride_rgba, uint8* dst_u, uint8* dst_v, int width) { const uint8* src_rgba_1 = src_rgba + src_stride_rgba; @@ -1756,9 +1663,7 @@ void RGBAToUVRow_NEON(const uint8* src_rgba, int src_stride_rgba, "v20", "v21", "v22", "v23", "v24", "v25" ); } -#endif // HAS_RGBATOUVROW_NEON -#ifdef HAS_RGB24TOUVROW_NEON void RGB24ToUVRow_NEON(const uint8* src_rgb24, int src_stride_rgb24, uint8* dst_u, uint8* dst_v, int width) { const uint8* src_rgb24_1 = src_rgb24 + src_stride_rgb24; @@ -1797,9 +1702,7 @@ void RGB24ToUVRow_NEON(const uint8* src_rgb24, int src_stride_rgb24, "v20", "v21", "v22", "v23", "v24", "v25" ); } -#endif // HAS_RGB24TOUVROW_NEON -#ifdef HAS_RAWTOUVROW_NEON void RAWToUVRow_NEON(const uint8* src_raw, int src_stride_raw, uint8* dst_u, uint8* dst_v, int width) { const uint8* src_raw_1 = src_raw + src_stride_raw; @@ -1838,10 +1741,8 @@ void RAWToUVRow_NEON(const uint8* src_raw, int src_stride_raw, "v20", "v21", "v22", "v23", "v24", "v25" ); } -#endif // HAS_RAWTOUVROW_NEON // 16x2 pixels -> 8x1. width is number of argb pixels. e.g. 16. -#ifdef HAS_RGB565TOUVROW_NEON void RGB565ToUVRow_NEON(const uint8* src_rgb565, int src_stride_rgb565, uint8* dst_u, uint8* dst_v, int width) { const uint8* src_rgb565_1 = src_rgb565 + src_stride_rgb565; @@ -1914,10 +1815,8 @@ void RGB565ToUVRow_NEON(const uint8* src_rgb565, int src_stride_rgb565, "v25", "v26", "v27" ); } -#endif // HAS_RGB565TOUVROW_NEON // 16x2 pixels -> 8x1. width is number of argb pixels. e.g. 16. -#ifdef HAS_ARGB1555TOUVROW_NEON void ARGB1555ToUVRow_NEON(const uint8* src_argb1555, int src_stride_argb1555, uint8* dst_u, uint8* dst_v, int width) { const uint8* src_argb1555_1 = src_argb1555 + src_stride_argb1555; @@ -1985,10 +1884,8 @@ void ARGB1555ToUVRow_NEON(const uint8* src_argb1555, int src_stride_argb1555, "v26", "v27", "v28" ); } -#endif // HAS_ARGB1555TOUVROW_NEON // 16x2 pixels -> 8x1. width is number of argb pixels. e.g. 16. -#ifdef HAS_ARGB4444TOUVROW_NEON void ARGB4444ToUVRow_NEON(const uint8* src_argb4444, int src_stride_argb4444, uint8* dst_u, uint8* dst_v, int width) { const uint8* src_argb4444_1 = src_argb4444 + src_stride_argb4444; @@ -2057,9 +1954,7 @@ void ARGB4444ToUVRow_NEON(const uint8* src_argb4444, int src_stride_argb4444, ); } -#endif // HAS_ARGB4444TOUVROW_NEON -#ifdef HAS_RGB565TOYROW_NEON void RGB565ToYRow_NEON(const uint8* src_rgb565, uint8* dst_y, int width) { asm volatile ( "movi v24.8b, #13 \n" // B * 0.1016 coefficient @@ -2087,9 +1982,7 @@ void RGB565ToYRow_NEON(const uint8* src_rgb565, uint8* dst_y, int width) { "v24", "v25", "v26", "v27" ); } -#endif // HAS_RGB565TOYROW_NEON -#ifdef HAS_ARGB1555TOYROW_NEON void ARGB1555ToYRow_NEON(const uint8* src_argb1555, uint8* dst_y, int width) { asm volatile ( "movi v4.8b, #13 \n" // B * 0.1016 coefficient @@ -2116,9 +2009,7 @@ void ARGB1555ToYRow_NEON(const uint8* src_argb1555, uint8* dst_y, int width) { : "cc", "memory", "v0", "v1", "v2", "v3", "v4", "v5", "v6", "v7" ); } -#endif // HAS_ARGB1555TOYROW_NEON -#ifdef HAS_ARGB4444TOYROW_NEON void ARGB4444ToYRow_NEON(const uint8* src_argb4444, uint8* dst_y, int width) { asm volatile ( "movi v24.8b, #13 \n" // B * 0.1016 coefficient @@ -2145,9 +2036,7 @@ void ARGB4444ToYRow_NEON(const uint8* src_argb4444, uint8* dst_y, int width) { : "cc", "memory", "v0", "v1", "v2", "v3", "v24", "v25", "v26", "v27" ); } -#endif // HAS_ARGB4444TOYROW_NEON -#ifdef HAS_BGRATOYROW_NEON void BGRAToYRow_NEON(const uint8* src_bgra, uint8* dst_y, int width) { asm volatile ( "movi v4.8b, #33 \n" // R * 0.2578 coefficient @@ -2173,9 +2062,7 @@ void BGRAToYRow_NEON(const uint8* src_bgra, uint8* dst_y, int width) { : "cc", "memory", "v0", "v1", "v2", "v3", "v4", "v5", "v6", "v7", "v16" ); } -#endif // HAS_BGRATOYROW_NEON -#ifdef HAS_ABGRTOYROW_NEON void ABGRToYRow_NEON(const uint8* src_abgr, uint8* dst_y, int width) { asm volatile ( "movi v4.8b, #33 \n" // R * 0.2578 coefficient @@ -2201,9 +2088,7 @@ void ABGRToYRow_NEON(const uint8* src_abgr, uint8* dst_y, int width) { : "cc", "memory", "v0", "v1", "v2", "v3", "v4", "v5", "v6", "v7", "v16" ); } -#endif // HAS_ABGRTOYROW_NEON -#ifdef HAS_RGBATOYROW_NEON void RGBAToYRow_NEON(const uint8* src_rgba, uint8* dst_y, int width) { asm volatile ( "movi v4.8b, #13 \n" // B * 0.1016 coefficient @@ -2229,9 +2114,7 @@ void RGBAToYRow_NEON(const uint8* src_rgba, uint8* dst_y, int width) { : "cc", "memory", "v0", "v1", "v2", "v3", "v4", "v5", "v6", "v7", "v16" ); } -#endif // HAS_RGBATOYROW_NEON -#ifdef HAS_RGB24TOYROW_NEON void RGB24ToYRow_NEON(const uint8* src_rgb24, uint8* dst_y, int width) { asm volatile ( "movi v4.8b, #13 \n" // B * 0.1016 coefficient @@ -2257,9 +2140,7 @@ void RGB24ToYRow_NEON(const uint8* src_rgb24, uint8* dst_y, int width) { : "cc", "memory", "v0", "v1", "v2", "v3", "v4", "v5", "v6", "v7", "v16" ); } -#endif // HAS_RGB24TOYROW_NEON -#ifdef HAS_RAWTOYROW_NEON void RAWToYRow_NEON(const uint8* src_raw, uint8* dst_y, int width) { asm volatile ( "movi v4.8b, #33 \n" // R * 0.2578 coefficient @@ -2285,10 +2166,8 @@ void RAWToYRow_NEON(const uint8* src_raw, uint8* dst_y, int width) { : "cc", "memory", "v0", "v1", "v2", "v3", "v4", "v5", "v6", "v7", "v16" ); } -#endif // HAS_RAWTOYROW_NEON // Bilinear filter 16x2 -> 16x1 -#ifdef HAS_INTERPOLATEROW_NEON void InterpolateRow_NEON(uint8* dst_ptr, const uint8* src_ptr, ptrdiff_t src_stride, int dst_width, int source_y_fraction) { @@ -2354,10 +2233,8 @@ void InterpolateRow_NEON(uint8* dst_ptr, : "cc", "memory", "v0", "v1", "v3", "v4", "v5" ); } -#endif // HAS_INTERPOLATEROW_NEON // dr * (256 - sa) / 256 + sr = dr - dr * sa / 256 + sr -#ifdef HAS_ARGBBLENDROW_NEON void ARGBBlendRow_NEON(const uint8* src_argb0, const uint8* src_argb1, uint8* dst_argb, int width) { asm volatile ( @@ -2426,10 +2303,8 @@ void ARGBBlendRow_NEON(const uint8* src_argb0, const uint8* src_argb1, "v16", "v17", "v18" ); } -#endif // HAS_ARGBBLENDROW_NEON // Attenuate 8 pixels at a time. -#ifdef HAS_ARGBATTENUATEROW_NEON void ARGBAttenuateRow_NEON(const uint8* src_argb, uint8* dst_argb, int width) { asm volatile ( // Attenuate 8 pixels. @@ -2453,11 +2328,9 @@ void ARGBAttenuateRow_NEON(const uint8* src_argb, uint8* dst_argb, int width) { : "cc", "memory", "v0", "v1", "v2", "v3", "v4", "v5", "v6" ); } -#endif // HAS_ARGBATTENUATEROW_NEON // Quantize 8 ARGB pixels (32 bytes). // dst = (dst * scale >> 16) * interval_size + interval_offset; -#ifdef HAS_ARGBQUANTIZEROW_NEON void ARGBQuantizeRow_NEON(uint8* dst_argb, int scale, int interval_size, int interval_offset, int width) { asm volatile ( @@ -2497,12 +2370,10 @@ void ARGBQuantizeRow_NEON(uint8* dst_argb, int scale, int interval_size, : "cc", "memory", "v0", "v1", "v2", "v3", "v4", "v5", "v6" ); } -#endif // HAS_ARGBQUANTIZEROW_NEON // Shade 8 pixels at a time by specified value. // NOTE vqrdmulh.s16 q10, q10, d0[0] must use a scaler register from 0 to 8. // Rounding in vqrdmulh does +1 to high if high bit of low s16 is set. -#ifdef HAS_ARGBSHADEROW_NEON void ARGBShadeRow_NEON(const uint8* src_argb, uint8* dst_argb, int width, uint32 value) { asm volatile ( @@ -2537,12 +2408,10 @@ void ARGBShadeRow_NEON(const uint8* src_argb, uint8* dst_argb, int width, : "cc", "memory", "v0", "v4", "v5", "v6", "v7" ); } -#endif // HAS_ARGBSHADEROW_NEON // Convert 8 ARGB pixels (64 bytes) to 8 Gray ARGB pixels // Similar to ARGBToYJ but stores ARGB. // C code is (15 * b + 75 * g + 38 * r + 64) >> 7; -#ifdef HAS_ARGBGRAYROW_NEON void ARGBGrayRow_NEON(const uint8* src_argb, uint8* dst_argb, int width) { asm volatile ( "movi v24.8b, #15 \n" // B * 0.11400 coefficient @@ -2568,14 +2437,12 @@ void ARGBGrayRow_NEON(const uint8* src_argb, uint8* dst_argb, int width) { : "cc", "memory", "v0", "v1", "v2", "v3", "v4", "v24", "v25", "v26" ); } -#endif // HAS_ARGBGRAYROW_NEON // Convert 8 ARGB pixels (32 bytes) to 8 Sepia ARGB pixels. // b = (r * 35 + g * 68 + b * 17) >> 7 // g = (r * 45 + g * 88 + b * 22) >> 7 // r = (r * 50 + g * 98 + b * 24) >> 7 -#ifdef HAS_ARGBSEPIAROW_NEON void ARGBSepiaRow_NEON(uint8* dst_argb, int width) { asm volatile ( "movi v20.8b, #17 \n" // BB coefficient @@ -2613,12 +2480,10 @@ void ARGBSepiaRow_NEON(uint8* dst_argb, int width) { "v20", "v21", "v22", "v24", "v25", "v26", "v28", "v29", "v30" ); } -#endif // HAS_ARGBSEPIAROW_NEON // Tranform 8 ARGB pixels (32 bytes) with color matrix. // TODO(fbarchard): Was same as Sepia except matrix is provided. This function // needs to saturate. Consider doing a non-saturating version. -#ifdef HAS_ARGBCOLORMATRIXROW_NEON void ARGBColorMatrixRow_NEON(const uint8* src_argb, uint8* dst_argb, const int8* matrix_argb, int width) { asm volatile ( @@ -2678,11 +2543,9 @@ void ARGBColorMatrixRow_NEON(const uint8* src_argb, uint8* dst_argb, "v18", "v19", "v22", "v23", "v24", "v25" ); } -#endif // HAS_ARGBCOLORMATRIXROW_NEON // TODO(fbarchard): fix vqshrun in ARGBMultiplyRow_NEON and reenable. // Multiply 2 rows of ARGB pixels together, 8 pixels at a time. -#ifdef HAS_ARGBMULTIPLYROW_NEON void ARGBMultiplyRow_NEON(const uint8* src_argb0, const uint8* src_argb1, uint8* dst_argb, int width) { asm volatile ( @@ -2713,10 +2576,8 @@ void ARGBMultiplyRow_NEON(const uint8* src_argb0, const uint8* src_argb1, : "cc", "memory", "v0", "v1", "v2", "v3", "v4", "v5", "v6", "v7" ); } -#endif // HAS_ARGBMULTIPLYROW_NEON // Add 2 rows of ARGB pixels together, 8 pixels at a time. -#ifdef HAS_ARGBADDROW_NEON void ARGBAddRow_NEON(const uint8* src_argb0, const uint8* src_argb1, uint8* dst_argb, int width) { asm volatile ( @@ -2743,10 +2604,8 @@ void ARGBAddRow_NEON(const uint8* src_argb0, const uint8* src_argb1, : "cc", "memory", "v0", "v1", "v2", "v3", "v4", "v5", "v6", "v7" ); } -#endif // HAS_ARGBADDROW_NEON // Subtract 2 rows of ARGB pixels, 8 pixels at a time. -#ifdef HAS_ARGBSUBTRACTROW_NEON void ARGBSubtractRow_NEON(const uint8* src_argb0, const uint8* src_argb1, uint8* dst_argb, int width) { asm volatile ( @@ -2773,14 +2632,12 @@ void ARGBSubtractRow_NEON(const uint8* src_argb0, const uint8* src_argb1, : "cc", "memory", "v0", "v1", "v2", "v3", "v4", "v5", "v6", "v7" ); } -#endif // HAS_ARGBSUBTRACTROW_NEON // Adds Sobel X and Sobel Y and stores Sobel into ARGB. // A = 255 // R = Sobel // G = Sobel // B = Sobel -#ifdef HAS_SOBELROW_NEON void SobelRow_NEON(const uint8* src_sobelx, const uint8* src_sobely, uint8* dst_argb, int width) { asm volatile ( @@ -2806,10 +2663,8 @@ void SobelRow_NEON(const uint8* src_sobelx, const uint8* src_sobely, : "cc", "memory", "v0", "v1", "v2", "v3" ); } -#endif // HAS_SOBELROW_NEON // Adds Sobel X and Sobel Y and stores Sobel into plane. -#ifdef HAS_SOBELTOPLANEROW_NEON void SobelToPlaneRow_NEON(const uint8* src_sobelx, const uint8* src_sobely, uint8* dst_y, int width) { asm volatile ( @@ -2832,14 +2687,12 @@ void SobelToPlaneRow_NEON(const uint8* src_sobelx, const uint8* src_sobely, : "cc", "memory", "v0", "v1" ); } -#endif // HAS_SOBELTOPLANEROW_NEON // Mixes Sobel X, Sobel Y and Sobel into ARGB. // A = 255 // R = Sobel X // G = Sobel // B = Sobel Y -#ifdef HAS_SOBELXYROW_NEON void SobelXYRow_NEON(const uint8* src_sobelx, const uint8* src_sobely, uint8* dst_argb, int width) { asm volatile ( @@ -2863,13 +2716,11 @@ void SobelXYRow_NEON(const uint8* src_sobelx, const uint8* src_sobely, : "cc", "memory", "v0", "v1", "v2", "v3" ); } -#endif // HAS_SOBELXYROW_NEON // SobelX as a matrix is // -1 0 1 // -2 0 2 // -1 0 1 -#ifdef HAS_SOBELXROW_NEON void SobelXRow_NEON(const uint8* src_y0, const uint8* src_y1, const uint8* src_y2, uint8* dst_sobelx, int width) { asm volatile ( @@ -2908,13 +2759,11 @@ void SobelXRow_NEON(const uint8* src_y0, const uint8* src_y1, : "cc", "memory", "v0", "v1", "v2", "v3" // Clobber List ); } -#endif // HAS_SOBELXROW_NEON // SobelY as a matrix is // -1 -2 -1 // 0 0 0 // 1 2 1 -#ifdef HAS_SOBELYROW_NEON void SobelYRow_NEON(const uint8* src_y0, const uint8* src_y1, uint8* dst_sobely, int width) { asm volatile ( @@ -2952,7 +2801,6 @@ void SobelYRow_NEON(const uint8* src_y0, const uint8* src_y1, : "cc", "memory", "v0", "v1", "v2", "v3" // Clobber List ); } -#endif // HAS_SOBELYROW_NEON #endif // !defined(LIBYUV_DISABLE_NEON) && defined(__aarch64__) #ifdef __cplusplus diff --git a/TMessagesProj/jni/libyuv/source/row_win.cc b/TMessagesProj/jni/libyuv/source/row_win.cc index a8c16c3c1..cdb760603 100644 --- a/TMessagesProj/jni/libyuv/source/row_win.cc +++ b/TMessagesProj/jni/libyuv/source/row_win.cc @@ -3532,6 +3532,33 @@ void ARGBCopyAlphaRow_AVX2(const uint8* src, uint8* dst, int width) { } #endif // HAS_ARGBCOPYALPHAROW_AVX2 +#ifdef HAS_ARGBEXTRACTALPHAROW_SSE2 +// width in pixels +__declspec(naked) +void ARGBExtractAlphaRow_SSE2(const uint8* src_argb, uint8* dst_a, int width) { + __asm { + mov eax, [esp + 4] // src_argb + mov edx, [esp + 8] // dst_a + mov ecx, [esp + 12] // width + + extractloop: + movdqu xmm0, [eax] + movdqu xmm1, [eax + 16] + lea eax, [eax + 32] + psrld xmm0, 24 + psrld xmm1, 24 + packssdw xmm0, xmm1 + packuswb xmm0, xmm0 + movq qword ptr [edx], xmm0 + lea edx, [edx + 8] + sub ecx, 8 + jg extractloop + + ret + } +} +#endif // HAS_ARGBEXTRACTALPHAROW_SSE2 + #ifdef HAS_ARGBCOPYYTOALPHAROW_SSE2 // width in pixels __declspec(naked) @@ -5248,6 +5275,7 @@ void SobelXYRow_SSE2(const uint8* src_sobelx, const uint8* src_sobely, // dst points to pixel to store result to. // count is number of averaged pixels to produce. // Does 4 pixels at a time. +// This function requires alignment on accumulation buffer pointers. void CumulativeSumToAverageRow_SSE2(const int32* topleft, const int32* botleft, int width, int area, uint8* dst, int count) { diff --git a/TMessagesProj/jni/libyuv/source/video_common.cc b/TMessagesProj/jni/libyuv/source/video_common.cc index 379a0669a..00fb71e18 100644 --- a/TMessagesProj/jni/libyuv/source/video_common.cc +++ b/TMessagesProj/jni/libyuv/source/video_common.cc @@ -25,6 +25,7 @@ struct FourCCAliasEntry { static const struct FourCCAliasEntry kFourCCAliases[] = { {FOURCC_IYUV, FOURCC_I420}, + {FOURCC_YU12, FOURCC_I420}, {FOURCC_YU16, FOURCC_I422}, {FOURCC_YU24, FOURCC_I444}, {FOURCC_YUYV, FOURCC_YUY2}, diff --git a/TMessagesProj/src/main/AndroidManifest.xml b/TMessagesProj/src/main/AndroidManifest.xml index e88b0da7b..abf2f25ad 100644 --- a/TMessagesProj/src/main/AndroidManifest.xml +++ b/TMessagesProj/src/main/AndroidManifest.xml @@ -231,6 +231,8 @@ + + diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/AndroidUtilities.java b/TMessagesProj/src/main/java/org/telegram/messenger/AndroidUtilities.java index 2ecda7417..8fa36b6b8 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/AndroidUtilities.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/AndroidUtilities.java @@ -8,6 +8,9 @@ package org.telegram.messenger; +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; import android.annotation.SuppressLint; import android.app.Activity; import android.app.AlertDialog; @@ -48,6 +51,7 @@ import android.view.Surface; import android.view.View; import android.view.WindowManager; import android.view.inputmethod.InputMethodManager; +import android.webkit.MimeTypeMap; import android.widget.AbsListView; import android.widget.EdgeEffect; import android.widget.EditText; @@ -61,10 +65,6 @@ import net.hockeyapp.android.UpdateManager; import org.telegram.tgnet.ConnectionsManager; import org.telegram.tgnet.TLRPC; -import org.telegram.messenger.AnimationCompat.AnimatorListenerAdapterProxy; -import org.telegram.messenger.AnimationCompat.AnimatorSetProxy; -import org.telegram.messenger.AnimationCompat.ObjectAnimatorProxy; -import org.telegram.messenger.AnimationCompat.ViewProxy; import org.telegram.ui.ActionBar.BaseFragment; import org.telegram.ui.Components.ForegroundDetector; import org.telegram.ui.Components.NumberPicker; @@ -156,11 +156,7 @@ public class AndroidUtilities { } } } else if (drawable instanceof ColorDrawable) { - if (Build.VERSION.SDK_INT >= 11) { - bitmapColor = ((ColorDrawable) drawable).getColor(); - } else { - bitmapColor = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE).getInt("selectedColor", 0xff000000); - } + bitmapColor = ((ColorDrawable) drawable).getColor(); } } catch (Exception e) { FileLog.e("tmessages", e); @@ -500,11 +496,7 @@ public class AndroidUtilities { Display display = manager.getDefaultDisplay(); if (display != null) { display.getMetrics(displayMetrics); - if (android.os.Build.VERSION.SDK_INT < 13) { - displaySize.set(display.getWidth(), display.getHeight()); - } else { - display.getSize(displaySize); - } + display.getSize(displaySize); FileLog.e("tmessages", "display size = " + displaySize.x + " " + displaySize.y + " " + displayMetrics.xdpi + "x" + displayMetrics.ydpi); } } @@ -685,7 +677,7 @@ public class AndroidUtilities { } public static void clearCursorDrawable(EditText editText) { - if (editText == null || Build.VERSION.SDK_INT < 12) { + if (editText == null) { return; } try { @@ -883,6 +875,19 @@ public class AndroidUtilities { return size; } + public static CharSequence getTrimmedString(CharSequence src) { + if (src == null || src.length() == 0) { + return src; + } + while (src.length() > 0 && (src.charAt(0) == '\n' || src.charAt(0) == ' ')) { + src = src.subSequence(1, src.length()); + } + while (src.length() > 0 && (src.charAt(src.length() - 1) == '\n' || src.charAt(src.length() - 1) == ' ')) { + src = src.subSequence(0, src.length() - 1); + } + return src; + } + public static void setListViewEdgeEffectColor(AbsListView listView, int color) { if (Build.VERSION.SDK_INT >= 21) { try { @@ -989,14 +994,9 @@ public class AndroidUtilities { } public static boolean needShowPasscode(boolean reset) { - boolean wasInBackground; - if (Build.VERSION.SDK_INT >= 14) { - wasInBackground = ForegroundDetector.getInstance().isWasInBackground(reset); - if (reset) { - ForegroundDetector.getInstance().resetBackgroundVar(); - } - } else { - wasInBackground = UserConfig.lastPauseTime != 0; + boolean wasInBackground = ForegroundDetector.getInstance().isWasInBackground(reset); + if (reset) { + ForegroundDetector.getInstance().resetBackgroundVar(); } return UserConfig.passcodeHash.length() > 0 && wasInBackground && (UserConfig.appLocked || UserConfig.autoLockIn != 0 && UserConfig.lastPauseTime != 0 && !UserConfig.appLocked && (UserConfig.lastPauseTime + UserConfig.autoLockIn) <= ConnectionsManager.getInstance().getCurrentTime()); @@ -1004,24 +1004,21 @@ public class AndroidUtilities { public static void shakeView(final View view, final float x, final int num) { if (num == 6) { - ViewProxy.setTranslationX(view, 0); - view.clearAnimation(); + view.setTranslationX(0); return; } - AnimatorSetProxy animatorSetProxy = new AnimatorSetProxy(); - animatorSetProxy.playTogether(ObjectAnimatorProxy.ofFloat(view, "translationX", AndroidUtilities.dp(x))); - animatorSetProxy.setDuration(50); - animatorSetProxy.addListener(new AnimatorListenerAdapterProxy() { + AnimatorSet animatorSet = new AnimatorSet(); + animatorSet.playTogether(ObjectAnimator.ofFloat(view, "translationX", AndroidUtilities.dp(x))); + animatorSet.setDuration(50); + animatorSet.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { shakeView(view, num == 5 ? 0 : -x, num + 1); } }); - animatorSetProxy.start(); + animatorSet.start(); } - - /*public static String ellipsize(String text, int maxLines, int maxWidth, TextPaint paint) { if (text == null || paint == null) { return null; @@ -1058,7 +1055,7 @@ public class AndroidUtilities { }*/ /*public static void turnOffHardwareAcceleration(Window window) { - if (window == null || Build.MODEL == null || Build.VERSION.SDK_INT < 11) { + if (window == null || Build.MODEL == null) { return; } if (Build.MODEL.contains("GT-S5301") || @@ -1094,14 +1091,9 @@ public class AndroidUtilities { public static void addToClipboard(CharSequence str) { try { - if (Build.VERSION.SDK_INT < 11) { - android.text.ClipboardManager clipboard = (android.text.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); - clipboard.setText(str); - } else { - android.content.ClipboardManager clipboard = (android.content.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); - android.content.ClipData clip = android.content.ClipData.newPlainText("label", str); - clipboard.setPrimaryClip(clip); - } + android.content.ClipboardManager clipboard = (android.content.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); + android.content.ClipData clip = android.content.ClipData.newPlainText("label", str); + clipboard.setPrimaryClip(clip); } catch (Exception e) { FileLog.e("tmessages", e); } @@ -1389,4 +1381,50 @@ public class AndroidUtilities { System.arraycopy(sha1, 0, key_hash, 0, 16); return key_hash; } + + public static void openForView(MessageObject message, Activity activity) throws Exception { + File f = null; + String fileName = message.getFileName(); + if (message.messageOwner.attachPath != null && message.messageOwner.attachPath.length() != 0) { + f = new File(message.messageOwner.attachPath); + } + if (f == null || !f.exists()) { + f = FileLoader.getPathToMessage(message.messageOwner); + } + if (f != null && f.exists()) { + String realMimeType = null; + Intent intent = new Intent(Intent.ACTION_VIEW); + MimeTypeMap myMime = MimeTypeMap.getSingleton(); + int idx = fileName.lastIndexOf('.'); + if (idx != -1) { + String ext = fileName.substring(idx + 1); + realMimeType = myMime.getMimeTypeFromExtension(ext.toLowerCase()); + if (realMimeType == null) { + if (message.type == 9 || message.type == 0) { + realMimeType = message.getDocument().mime_type; + } + if (realMimeType == null || realMimeType.length() == 0) { + realMimeType = null; + } + } + if (realMimeType != null) { + intent.setDataAndType(Uri.fromFile(f), realMimeType); + } else { + intent.setDataAndType(Uri.fromFile(f), "text/plain"); + } + } else { + intent.setDataAndType(Uri.fromFile(f), "text/plain"); + } + if (realMimeType != null) { + try { + activity.startActivityForResult(intent, 500); + } catch (Exception e) { + intent.setDataAndType(Uri.fromFile(f), "text/plain"); + activity.startActivityForResult(intent, 500); + } + } else { + activity.startActivityForResult(intent, 500); + } + } + } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/Animator10.java b/TMessagesProj/src/main/java/org/telegram/messenger/Animation/Animator10.java deleted file mode 100644 index 6defed840..000000000 --- a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/Animator10.java +++ /dev/null @@ -1,191 +0,0 @@ -/* - * Copyright (C) 2010 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.messenger.Animation; - -import android.view.animation.Interpolator; - -import java.util.ArrayList; - -public abstract class Animator10 implements Cloneable { - - ArrayList mListeners = null; - ArrayList mPauseListeners = null; - boolean mPaused = false; - - public abstract long getStartDelay(); - - public abstract void setStartDelay(long startDelay); - - public abstract Animator10 setDuration(long duration); - - public abstract long getDuration(); - - public abstract void setInterpolator(Interpolator value); - - public abstract boolean isRunning(); - - public void start() { - - } - - public void cancel() { - - } - - public void end() { - - } - - @SuppressWarnings("unchecked") - public void pause() { - if (isStarted() && !mPaused) { - mPaused = true; - if (mPauseListeners != null) { - ArrayList tmpListeners = (ArrayList) mPauseListeners.clone(); - int numListeners = tmpListeners.size(); - for (AnimatorPauseListener tmpListener : tmpListeners) { - tmpListener.onAnimationPause(this); - } - } - } - } - - @SuppressWarnings("unchecked") - public void resume() { - if (mPaused) { - mPaused = false; - if (mPauseListeners != null) { - ArrayList tmpListeners = (ArrayList) mPauseListeners.clone(); - int numListeners = tmpListeners.size(); - for (AnimatorPauseListener tmpListener : tmpListeners) { - tmpListener.onAnimationResume(this); - } - } - } - } - - public boolean isPaused() { - return mPaused; - } - - public boolean isStarted() { - return isRunning(); - } - - public Interpolator getInterpolator() { - return null; - } - - public void addListener(AnimatorListener listener) { - if (mListeners == null) { - mListeners = new ArrayList(); - } - mListeners.add(listener); - } - - public void removeListener(AnimatorListener listener) { - if (mListeners == null) { - return; - } - mListeners.remove(listener); - if (mListeners.size() == 0) { - mListeners = null; - } - } - - public ArrayList getListeners() { - return mListeners; - } - - public void addPauseListener(AnimatorPauseListener listener) { - if (mPauseListeners == null) { - mPauseListeners = new ArrayList(); - } - mPauseListeners.add(listener); - } - - public void removePauseListener(AnimatorPauseListener listener) { - if (mPauseListeners == null) { - return; - } - mPauseListeners.remove(listener); - if (mPauseListeners.size() == 0) { - mPauseListeners = null; - } - } - - public void removeAllListeners() { - if (mListeners != null) { - mListeners.clear(); - mListeners = null; - } - if (mPauseListeners != null) { - mPauseListeners.clear(); - mPauseListeners = null; - } - } - - @Override - public Animator10 clone() { - try { - final Animator10 anim = (Animator10) super.clone(); - if (mListeners != null) { - ArrayList oldListeners = mListeners; - anim.mListeners = new ArrayList(); - int numListeners = oldListeners.size(); - for (AnimatorListener oldListener : oldListeners) { - anim.mListeners.add(oldListener); - } - } - if (mPauseListeners != null) { - ArrayList oldListeners = mPauseListeners; - anim.mPauseListeners = new ArrayList(); - int numListeners = oldListeners.size(); - for (AnimatorPauseListener oldListener : oldListeners) { - anim.mPauseListeners.add(oldListener); - } - } - return anim; - } catch (CloneNotSupportedException e) { - throw new AssertionError(); - } - } - - public void setupStartValues() { - - } - - public void setupEndValues() { - - } - - public void setTarget(Object target) { - - } - - public interface AnimatorListener { - void onAnimationStart(Animator10 animation); - void onAnimationEnd(Animator10 animation); - void onAnimationCancel(Animator10 animation); - void onAnimationRepeat(Animator10 animation); - } - - public interface AnimatorPauseListener { - void onAnimationPause(Animator10 animation); - void onAnimationResume(Animator10 animation); - } -} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/AnimatorListenerAdapter10.java b/TMessagesProj/src/main/java/org/telegram/messenger/Animation/AnimatorListenerAdapter10.java deleted file mode 100644 index f5aefe9a8..000000000 --- a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/AnimatorListenerAdapter10.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (C) 2010 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.messenger.Animation; - -public abstract class AnimatorListenerAdapter10 implements Animator10.AnimatorListener, Animator10.AnimatorPauseListener { - - @Override - public void onAnimationCancel(Animator10 animation) { - - } - - @Override - public void onAnimationEnd(Animator10 animation) { - - } - - @Override - public void onAnimationRepeat(Animator10 animation) { - - } - - @Override - public void onAnimationStart(Animator10 animation) { - - } - - @Override - public void onAnimationPause(Animator10 animation) { - - } - - @Override - public void onAnimationResume(Animator10 animation) { - - } -} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/AnimatorSet10.java b/TMessagesProj/src/main/java/org/telegram/messenger/Animation/AnimatorSet10.java deleted file mode 100644 index 2f749f2f0..000000000 --- a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/AnimatorSet10.java +++ /dev/null @@ -1,705 +0,0 @@ -/* - * Copyright (C) 2010 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.messenger.Animation; - -import android.view.animation.Interpolator; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; - -public final class AnimatorSet10 extends Animator10 { - - private ArrayList mPlayingSet = new ArrayList<>(); - private HashMap mNodeMap = new HashMap<>(); - private ArrayList mNodes = new ArrayList<>(); - private ArrayList mSortedNodes = new ArrayList<>(); - private boolean mNeedsSort = true; - private AnimatorSetListener mSetListener = null; - boolean mTerminated = false; - private boolean mStarted = false; - private long mStartDelay = 0; - private ValueAnimator mDelayAnim = null; - private long mDuration = -1; - private Interpolator mInterpolator = null; - - public void playTogether(Animator10... items) { - if (items != null) { - mNeedsSort = true; - Builder builder = play(items[0]); - for (int i = 1; i < items.length; ++i) { - builder.with(items[i]); - } - } - } - - public void playTogether(Collection items) { - if (items != null && items.size() > 0) { - mNeedsSort = true; - Builder builder = null; - for (Animator10 anim : items) { - if (builder == null) { - builder = play(anim); - } else { - builder.with(anim); - } - } - } - } - - public void playSequentially(Animator10... items) { - if (items != null) { - mNeedsSort = true; - if (items.length == 1) { - play(items[0]); - } else { - for (int i = 0; i < items.length - 1; ++i) { - play(items[i]).before(items[i+1]); - } - } - } - } - - public void playSequentially(List items) { - if (items != null && items.size() > 0) { - mNeedsSort = true; - if (items.size() == 1) { - play(items.get(0)); - } else { - for (int i = 0; i < items.size() - 1; ++i) { - play(items.get(i)).before(items.get(i+1)); - } - } - } - } - - public ArrayList getChildAnimations() { - ArrayList childList = new ArrayList<>(); - for (Node node : mNodes) { - childList.add(node.animation); - } - return childList; - } - - @Override - public void setTarget(Object target) { - for (Node node : mNodes) { - Animator10 animation = node.animation; - if (animation instanceof AnimatorSet10) { - animation.setTarget(target); - } else if (animation instanceof ObjectAnimator10) { - animation.setTarget(target); - } - } - } - - @Override - public void setInterpolator(Interpolator interpolator) { - mInterpolator = interpolator; - } - - @Override - public Interpolator getInterpolator() { - return mInterpolator; - } - - public Builder play(Animator10 anim) { - if (anim != null) { - mNeedsSort = true; - return new Builder(anim); - } - return null; - } - - @SuppressWarnings("unchecked") - @Override - public void cancel() { - mTerminated = true; - if (isStarted()) { - ArrayList tmpListeners = null; - if (mListeners != null) { - tmpListeners = (ArrayList) mListeners.clone(); - for (AnimatorListener listener : tmpListeners) { - listener.onAnimationCancel(this); - } - } - if (mDelayAnim != null && mDelayAnim.isRunning()) { - mDelayAnim.cancel(); - } else if (mSortedNodes.size() > 0) { - for (Node node : mSortedNodes) { - node.animation.cancel(); - } - } - if (tmpListeners != null) { - for (AnimatorListener listener : tmpListeners) { - listener.onAnimationEnd(this); - } - } - mStarted = false; - } - } - - @SuppressWarnings("unchecked") - @Override - public void end() { - mTerminated = true; - if (isStarted()) { - if (mSortedNodes.size() != mNodes.size()) { - // hasn't been started yet - sort the nodes now, then end them - sortNodes(); - for (Node node : mSortedNodes) { - if (mSetListener == null) { - mSetListener = new AnimatorSetListener(this); - } - node.animation.addListener(mSetListener); - } - } - if (mDelayAnim != null) { - mDelayAnim.cancel(); - } - if (mSortedNodes.size() > 0) { - for (Node node : mSortedNodes) { - node.animation.end(); - } - } - if (mListeners != null) { - ArrayList tmpListeners = (ArrayList) mListeners.clone(); - for (AnimatorListener listener : tmpListeners) { - listener.onAnimationEnd(this); - } - } - mStarted = false; - } - } - - @Override - public boolean isRunning() { - for (Node node : mNodes) { - if (node.animation.isRunning()) { - return true; - } - } - return false; - } - - @Override - public boolean isStarted() { - return mStarted; - } - - @Override - public long getStartDelay() { - return mStartDelay; - } - - @Override - public void setStartDelay(long startDelay) { - mStartDelay = startDelay; - } - - @Override - public long getDuration() { - return mDuration; - } - - @Override - public AnimatorSet10 setDuration(long duration) { - if (duration < 0) { - throw new IllegalArgumentException("duration must be a value of zero or greater"); - } - mDuration = duration; - return this; - } - - @Override - public void setupStartValues() { - for (Node node : mNodes) { - node.animation.setupStartValues(); - } - } - - @Override - public void setupEndValues() { - for (Node node : mNodes) { - node.animation.setupEndValues(); - } - } - - @Override - public void pause() { - boolean previouslyPaused = mPaused; - super.pause(); - if (!previouslyPaused && mPaused) { - if (mDelayAnim != null) { - mDelayAnim.pause(); - } else { - for (Node node : mNodes) { - node.animation.pause(); - } - } - } - } - - @Override - public void resume() { - boolean previouslyPaused = mPaused; - super.resume(); - if (previouslyPaused && !mPaused) { - if (mDelayAnim != null) { - mDelayAnim.resume(); - } else { - for (Node node : mNodes) { - node.animation.resume(); - } - } - } - } - - @SuppressWarnings("unchecked") - @Override - public void start() { - mTerminated = false; - mStarted = true; - mPaused = false; - - if (mDuration >= 0) { - for (Node node : mNodes) { - node.animation.setDuration(mDuration); - } - } - if (mInterpolator != null) { - for (Node node : mNodes) { - node.animation.setInterpolator(mInterpolator); - } - } - - sortNodes(); - - int numSortedNodes = mSortedNodes.size(); - for (Node node : mSortedNodes) { - ArrayList oldListeners = node.animation.getListeners(); - if (oldListeners != null && oldListeners.size() > 0) { - final ArrayList clonedListeners = new - ArrayList<>(oldListeners); - - for (AnimatorListener listener : clonedListeners) { - if (listener instanceof DependencyListener || - listener instanceof AnimatorSetListener) { - node.animation.removeListener(listener); - } - } - } - } - - final ArrayList nodesToStart = new ArrayList<>(); - for (Node node : mSortedNodes) { - if (mSetListener == null) { - mSetListener = new AnimatorSetListener(this); - } - if (node.dependencies == null || node.dependencies.size() == 0) { - nodesToStart.add(node); - } else { - int numDependencies = node.dependencies.size(); - for (int j = 0; j < numDependencies; ++j) { - Dependency dependency = node.dependencies.get(j); - dependency.node.animation.addListener( - new DependencyListener(this, node, dependency.rule)); - } - node.tmpDependencies = (ArrayList) node.dependencies.clone(); - } - node.animation.addListener(mSetListener); - } - - if (mStartDelay <= 0) { - for (Node node : nodesToStart) { - node.animation.start(); - mPlayingSet.add(node.animation); - } - } else { - mDelayAnim = ValueAnimator.ofFloat(0f, 1f); - mDelayAnim.setDuration(mStartDelay); - mDelayAnim.addListener(new AnimatorListenerAdapter10() { - boolean canceled = false; - public void onAnimationCancel(Animator10 anim) { - canceled = true; - } - public void onAnimationEnd(Animator10 anim) { - if (!canceled) { - int numNodes = nodesToStart.size(); - for (Node node : nodesToStart) { - node.animation.start(); - mPlayingSet.add(node.animation); - } - } - mDelayAnim = null; - } - }); - mDelayAnim.start(); - } - if (mListeners != null) { - ArrayList tmpListeners = - (ArrayList) mListeners.clone(); - int numListeners = tmpListeners.size(); - for (AnimatorListener tmpListener : tmpListeners) { - tmpListener.onAnimationStart(this); - } - } - if (mNodes.size() == 0 && mStartDelay == 0) { - mStarted = false; - if (mListeners != null) { - ArrayList tmpListeners = - (ArrayList) mListeners.clone(); - int numListeners = tmpListeners.size(); - for (AnimatorListener tmpListener : tmpListeners) { - tmpListener.onAnimationEnd(this); - } - } - } - } - - @Override - public AnimatorSet10 clone() { - final AnimatorSet10 anim = (AnimatorSet10) super.clone(); - - anim.mNeedsSort = true; - anim.mTerminated = false; - anim.mStarted = false; - anim.mPlayingSet = new ArrayList<>(); - anim.mNodeMap = new HashMap<>(); - anim.mNodes = new ArrayList<>(); - anim.mSortedNodes = new ArrayList<>(); - - HashMap nodeCloneMap = new HashMap<>(); - for (Node node : mNodes) { - Node nodeClone = node.clone(); - nodeCloneMap.put(node, nodeClone); - anim.mNodes.add(nodeClone); - anim.mNodeMap.put(nodeClone.animation, nodeClone); - nodeClone.dependencies = null; - nodeClone.tmpDependencies = null; - nodeClone.nodeDependents = null; - nodeClone.nodeDependencies = null; - ArrayList cloneListeners = nodeClone.animation.getListeners(); - if (cloneListeners != null) { - ArrayList listenersToRemove = null; - for (AnimatorListener listener : cloneListeners) { - if (listener instanceof AnimatorSetListener) { - if (listenersToRemove == null) { - listenersToRemove = new ArrayList<>(); - } - listenersToRemove.add(listener); - } - } - if (listenersToRemove != null) { - for (AnimatorListener listener : listenersToRemove) { - cloneListeners.remove(listener); - } - } - } - } - for (Node node : mNodes) { - Node nodeClone = nodeCloneMap.get(node); - if (node.dependencies != null) { - for (Dependency dependency : node.dependencies) { - Node clonedDependencyNode = nodeCloneMap.get(dependency.node); - Dependency cloneDependency = new Dependency(clonedDependencyNode, dependency.rule); - nodeClone.addDependency(cloneDependency); - } - } - } - return anim; - } - - private static class DependencyListener implements AnimatorListener { - - private AnimatorSet10 mAnimatorSet; - private Node mNode; - private int mRule; - - public DependencyListener(AnimatorSet10 animatorSet, Node node, int rule) { - this.mAnimatorSet = animatorSet; - this.mNode = node; - this.mRule = rule; - } - - public void onAnimationCancel(Animator10 animation) { - - } - - public void onAnimationEnd(Animator10 animation) { - if (mRule == Dependency.AFTER) { - startIfReady(animation); - } - } - - public void onAnimationRepeat(Animator10 animation) { - - } - - public void onAnimationStart(Animator10 animation) { - if (mRule == Dependency.WITH) { - startIfReady(animation); - } - } - - private void startIfReady(Animator10 dependencyAnimation) { - if (mAnimatorSet.mTerminated) { - return; - } - Dependency dependencyToRemove = null; - int numDependencies = mNode.tmpDependencies.size(); - for (int i = 0; i < numDependencies; ++i) { - Dependency dependency = mNode.tmpDependencies.get(i); - if (dependency.rule == mRule && dependency.node.animation == dependencyAnimation) { - dependencyToRemove = dependency; - dependencyAnimation.removeListener(this); - break; - } - } - mNode.tmpDependencies.remove(dependencyToRemove); - if (mNode.tmpDependencies.size() == 0) { - mNode.animation.start(); - mAnimatorSet.mPlayingSet.add(mNode.animation); - } - } - } - - private class AnimatorSetListener implements AnimatorListener { - - private AnimatorSet10 mAnimatorSet; - - AnimatorSetListener(AnimatorSet10 animatorSet) { - mAnimatorSet = animatorSet; - } - - public void onAnimationCancel(Animator10 animation) { - if (!mTerminated) { - if (mPlayingSet.size() == 0) { - if (mListeners != null) { - int numListeners = mListeners.size(); - for (AnimatorListener mListener : mListeners) { - mListener.onAnimationCancel(mAnimatorSet); - } - } - } - } - } - - @SuppressWarnings("unchecked") - public void onAnimationEnd(Animator10 animation) { - animation.removeListener(this); - mPlayingSet.remove(animation); - Node animNode = mAnimatorSet.mNodeMap.get(animation); - animNode.done = true; - if (!mTerminated) { - ArrayList sortedNodes = mAnimatorSet.mSortedNodes; - boolean allDone = true; - int numSortedNodes = sortedNodes.size(); - for (Node sortedNode : sortedNodes) { - if (!sortedNode.done) { - allDone = false; - break; - } - } - if (allDone) { - if (mListeners != null) { - ArrayList tmpListeners = - (ArrayList) mListeners.clone(); - int numListeners = tmpListeners.size(); - for (AnimatorListener tmpListener : tmpListeners) { - tmpListener.onAnimationEnd(mAnimatorSet); - } - } - mAnimatorSet.mStarted = false; - mAnimatorSet.mPaused = false; - } - } - } - - public void onAnimationRepeat(Animator10 animation) { - - } - - public void onAnimationStart(Animator10 animation) { - - } - } - - private void sortNodes() { - if (mNeedsSort) { - mSortedNodes.clear(); - ArrayList roots = new ArrayList<>(); - int numNodes = mNodes.size(); - for (Node node : mNodes) { - if (node.dependencies == null || node.dependencies.size() == 0) { - roots.add(node); - } - } - ArrayList tmpRoots = new ArrayList<>(); - while (roots.size() > 0) { - int numRoots = roots.size(); - for (Node root : roots) { - mSortedNodes.add(root); - if (root.nodeDependents != null) { - int numDependents = root.nodeDependents.size(); - for (int j = 0; j < numDependents; ++j) { - Node node = root.nodeDependents.get(j); - node.nodeDependencies.remove(root); - if (node.nodeDependencies.size() == 0) { - tmpRoots.add(node); - } - } - } - } - roots.clear(); - roots.addAll(tmpRoots); - tmpRoots.clear(); - } - mNeedsSort = false; - if (mSortedNodes.size() != mNodes.size()) { - throw new IllegalStateException("Circular dependencies cannot exist in AnimatorSet"); - } - } else { - int numNodes = mNodes.size(); - for (Node node : mNodes) { - if (node.dependencies != null && node.dependencies.size() > 0) { - int numDependencies = node.dependencies.size(); - for (int j = 0; j < numDependencies; ++j) { - Dependency dependency = node.dependencies.get(j); - if (node.nodeDependencies == null) { - node.nodeDependencies = new ArrayList<>(); - } - if (!node.nodeDependencies.contains(dependency.node)) { - node.nodeDependencies.add(dependency.node); - } - } - } - node.done = false; - } - } - } - - private static class Dependency { - static final int WITH = 0; - static final int AFTER = 1; - public Node node; - public int rule; - - public Dependency(Node node, int rule) { - this.node = node; - this.rule = rule; - } - } - - private static class Node implements Cloneable { - public Animator10 animation; - public ArrayList dependencies = null; - public ArrayList tmpDependencies = null; - public ArrayList nodeDependencies = null; - public ArrayList nodeDependents = null; - public boolean done = false; - - public Node(Animator10 animation) { - this.animation = animation; - } - - public void addDependency(Dependency dependency) { - if (dependencies == null) { - dependencies = new ArrayList<>(); - nodeDependencies = new ArrayList<>(); - } - dependencies.add(dependency); - if (!nodeDependencies.contains(dependency.node)) { - nodeDependencies.add(dependency.node); - } - Node dependencyNode = dependency.node; - if (dependencyNode.nodeDependents == null) { - dependencyNode.nodeDependents = new ArrayList<>(); - } - dependencyNode.nodeDependents.add(this); - } - - @Override - public Node clone() { - try { - Node node = (Node) super.clone(); - node.animation = animation.clone(); - return node; - } catch (CloneNotSupportedException e) { - throw new AssertionError(); - } - } - } - - public class Builder { - - private Node mCurrentNode; - - Builder(Animator10 anim) { - mCurrentNode = mNodeMap.get(anim); - if (mCurrentNode == null) { - mCurrentNode = new Node(anim); - mNodeMap.put(anim, mCurrentNode); - mNodes.add(mCurrentNode); - } - } - - public Builder with(Animator10 anim) { - Node node = mNodeMap.get(anim); - if (node == null) { - node = new Node(anim); - mNodeMap.put(anim, node); - mNodes.add(node); - } - Dependency dependency = new Dependency(mCurrentNode, Dependency.WITH); - node.addDependency(dependency); - return this; - } - - public Builder before(Animator10 anim) { - Node node = mNodeMap.get(anim); - if (node == null) { - node = new Node(anim); - mNodeMap.put(anim, node); - mNodes.add(node); - } - Dependency dependency = new Dependency(mCurrentNode, Dependency.AFTER); - node.addDependency(dependency); - return this; - } - - public Builder after(Animator10 anim) { - Node node = mNodeMap.get(anim); - if (node == null) { - node = new Node(anim); - mNodeMap.put(anim, node); - mNodes.add(node); - } - Dependency dependency = new Dependency(node, Dependency.AFTER); - mCurrentNode.addDependency(dependency); - return this; - } - - public Builder after(long delay) { - ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f); - anim.setDuration(delay); - after(anim); - return this; - } - } -} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/FloatKeyframeSet.java b/TMessagesProj/src/main/java/org/telegram/messenger/Animation/FloatKeyframeSet.java deleted file mode 100644 index e7865e05c..000000000 --- a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/FloatKeyframeSet.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright (C) 2010 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.messenger.Animation; - -import android.view.animation.Interpolator; - -import org.telegram.messenger.Animation.Keyframe.FloatKeyframe; - -import java.util.ArrayList; - -class FloatKeyframeSet extends KeyframeSet { - private float firstValue; - private float lastValue; - private float deltaValue; - private boolean firstTime = true; - - public FloatKeyframeSet(FloatKeyframe... keyframes) { - super(keyframes); - } - - @Override - public Object getValue(float fraction) { - return getFloatValue(fraction); - } - - @Override - public FloatKeyframeSet clone() { - ArrayList keyframes = mKeyframes; - int numKeyframes = mKeyframes.size(); - FloatKeyframe[] newKeyframes = new FloatKeyframe[numKeyframes]; - for (int i = 0; i < numKeyframes; ++i) { - newKeyframes[i] = (FloatKeyframe) keyframes.get(i).clone(); - } - return new FloatKeyframeSet(newKeyframes); - } - - @SuppressWarnings("unchecked") - public float getFloatValue(float fraction) { - if (mNumKeyframes == 2) { - if (firstTime) { - firstTime = false; - firstValue = ((FloatKeyframe) mKeyframes.get(0)).getFloatValue(); - lastValue = ((FloatKeyframe) mKeyframes.get(1)).getFloatValue(); - deltaValue = lastValue - firstValue; - } - if (mInterpolator != null) { - fraction = mInterpolator.getInterpolation(fraction); - } - if (mEvaluator == null) { - return firstValue + fraction * deltaValue; - } else { - return ((Number)mEvaluator.evaluate(fraction, firstValue, lastValue)).floatValue(); - } - } - if (fraction <= 0f) { - final FloatKeyframe prevKeyframe = (FloatKeyframe) mKeyframes.get(0); - final FloatKeyframe nextKeyframe = (FloatKeyframe) mKeyframes.get(1); - float prevValue = prevKeyframe.getFloatValue(); - float nextValue = nextKeyframe.getFloatValue(); - float prevFraction = prevKeyframe.getFraction(); - float nextFraction = nextKeyframe.getFraction(); - final Interpolator interpolator = nextKeyframe.getInterpolator(); - if (interpolator != null) { - fraction = interpolator.getInterpolation(fraction); - } - float intervalFraction = (fraction - prevFraction) / (nextFraction - prevFraction); - return mEvaluator == null ? prevValue + intervalFraction * (nextValue - prevValue) : ((Number)mEvaluator.evaluate(intervalFraction, prevValue, nextValue)).floatValue(); - } else if (fraction >= 1f) { - final FloatKeyframe prevKeyframe = (FloatKeyframe) mKeyframes.get(mNumKeyframes - 2); - final FloatKeyframe nextKeyframe = (FloatKeyframe) mKeyframes.get(mNumKeyframes - 1); - float prevValue = prevKeyframe.getFloatValue(); - float nextValue = nextKeyframe.getFloatValue(); - float prevFraction = prevKeyframe.getFraction(); - float nextFraction = nextKeyframe.getFraction(); - final Interpolator interpolator = nextKeyframe.getInterpolator(); - if (interpolator != null) { - fraction = interpolator.getInterpolation(fraction); - } - float intervalFraction = (fraction - prevFraction) / (nextFraction - prevFraction); - return mEvaluator == null ? prevValue + intervalFraction * (nextValue - prevValue) : ((Number)mEvaluator.evaluate(intervalFraction, prevValue, nextValue)).floatValue(); - } - FloatKeyframe prevKeyframe = (FloatKeyframe) mKeyframes.get(0); - for (int i = 1; i < mNumKeyframes; ++i) { - FloatKeyframe nextKeyframe = (FloatKeyframe) mKeyframes.get(i); - if (fraction < nextKeyframe.getFraction()) { - final Interpolator interpolator = nextKeyframe.getInterpolator(); - if (interpolator != null) { - fraction = interpolator.getInterpolation(fraction); - } - float intervalFraction = (fraction - prevKeyframe.getFraction()) / - (nextKeyframe.getFraction() - prevKeyframe.getFraction()); - float prevValue = prevKeyframe.getFloatValue(); - float nextValue = nextKeyframe.getFloatValue(); - return mEvaluator == null ? prevValue + intervalFraction * (nextValue - prevValue) : ((Number)mEvaluator.evaluate(intervalFraction, prevValue, nextValue)).floatValue(); - } - prevKeyframe = nextKeyframe; - } - return ((Number)mKeyframes.get(mNumKeyframes - 1).getValue()).floatValue(); - } -} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/IntKeyframeSet.java b/TMessagesProj/src/main/java/org/telegram/messenger/Animation/IntKeyframeSet.java deleted file mode 100644 index 2224f2126..000000000 --- a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/IntKeyframeSet.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright (C) 2010 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.messenger.Animation; - -import android.view.animation.Interpolator; - -import org.telegram.messenger.Animation.Keyframe.IntKeyframe; - -import java.util.ArrayList; - -class IntKeyframeSet extends KeyframeSet { - private int firstValue; - private int lastValue; - private int deltaValue; - private boolean firstTime = true; - - public IntKeyframeSet(IntKeyframe... keyframes) { - super(keyframes); - } - - @Override - public Object getValue(float fraction) { - return getIntValue(fraction); - } - - @Override - public IntKeyframeSet clone() { - ArrayList keyframes = mKeyframes; - int numKeyframes = mKeyframes.size(); - IntKeyframe[] newKeyframes = new IntKeyframe[numKeyframes]; - for (int i = 0; i < numKeyframes; ++i) { - newKeyframes[i] = (IntKeyframe) keyframes.get(i).clone(); - } - return new IntKeyframeSet(newKeyframes); - } - - @SuppressWarnings("unchecked") - public int getIntValue(float fraction) { - if (mNumKeyframes == 2) { - if (firstTime) { - firstTime = false; - firstValue = ((IntKeyframe) mKeyframes.get(0)).getIntValue(); - lastValue = ((IntKeyframe) mKeyframes.get(1)).getIntValue(); - deltaValue = lastValue - firstValue; - } - if (mInterpolator != null) { - fraction = mInterpolator.getInterpolation(fraction); - } - if (mEvaluator == null) { - return firstValue + (int)(fraction * deltaValue); - } else { - return ((Number)mEvaluator.evaluate(fraction, firstValue, lastValue)).intValue(); - } - } - if (fraction <= 0f) { - final IntKeyframe prevKeyframe = (IntKeyframe) mKeyframes.get(0); - final IntKeyframe nextKeyframe = (IntKeyframe) mKeyframes.get(1); - int prevValue = prevKeyframe.getIntValue(); - int nextValue = nextKeyframe.getIntValue(); - float prevFraction = prevKeyframe.getFraction(); - float nextFraction = nextKeyframe.getFraction(); - final Interpolator interpolator = nextKeyframe.getInterpolator(); - if (interpolator != null) { - fraction = interpolator.getInterpolation(fraction); - } - float intervalFraction = (fraction - prevFraction) / (nextFraction - prevFraction); - return mEvaluator == null ? prevValue + (int)(intervalFraction * (nextValue - prevValue)) : ((Number)mEvaluator.evaluate(intervalFraction, prevValue, nextValue)).intValue(); - } else if (fraction >= 1f) { - final IntKeyframe prevKeyframe = (IntKeyframe) mKeyframes.get(mNumKeyframes - 2); - final IntKeyframe nextKeyframe = (IntKeyframe) mKeyframes.get(mNumKeyframes - 1); - int prevValue = prevKeyframe.getIntValue(); - int nextValue = nextKeyframe.getIntValue(); - float prevFraction = prevKeyframe.getFraction(); - float nextFraction = nextKeyframe.getFraction(); - final Interpolator interpolator = nextKeyframe.getInterpolator(); - if (interpolator != null) { - fraction = interpolator.getInterpolation(fraction); - } - float intervalFraction = (fraction - prevFraction) / (nextFraction - prevFraction); - return mEvaluator == null ? prevValue + (int)(intervalFraction * (nextValue - prevValue)) : ((Number)mEvaluator.evaluate(intervalFraction, prevValue, nextValue)).intValue(); - } - IntKeyframe prevKeyframe = (IntKeyframe) mKeyframes.get(0); - for (int i = 1; i < mNumKeyframes; ++i) { - IntKeyframe nextKeyframe = (IntKeyframe) mKeyframes.get(i); - if (fraction < nextKeyframe.getFraction()) { - final Interpolator interpolator = nextKeyframe.getInterpolator(); - if (interpolator != null) { - fraction = interpolator.getInterpolation(fraction); - } - float intervalFraction = (fraction - prevKeyframe.getFraction()) / (nextKeyframe.getFraction() - prevKeyframe.getFraction()); - int prevValue = prevKeyframe.getIntValue(); - int nextValue = nextKeyframe.getIntValue(); - return mEvaluator == null ? prevValue + (int)(intervalFraction * (nextValue - prevValue)) : ((Number)mEvaluator.evaluate(intervalFraction, prevValue, nextValue)).intValue(); - } - prevKeyframe = nextKeyframe; - } - return ((Number)mKeyframes.get(mNumKeyframes - 1).getValue()).intValue(); - } -} - diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/Keyframe.java b/TMessagesProj/src/main/java/org/telegram/messenger/Animation/Keyframe.java deleted file mode 100644 index b65ddaf44..000000000 --- a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/Keyframe.java +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Copyright (C) 2010 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.messenger.Animation; - -import android.view.animation.Interpolator; - -public abstract class Keyframe implements Cloneable { - - float mFraction; - Class mValueType; - private Interpolator mInterpolator = null; - boolean mHasValue = false; - - public static Keyframe ofInt(float fraction, int value) { - return new IntKeyframe(fraction, value); - } - - public static Keyframe ofInt(float fraction) { - return new IntKeyframe(fraction); - } - - public static Keyframe ofFloat(float fraction, float value) { - return new FloatKeyframe(fraction, value); - } - - public static Keyframe ofFloat(float fraction) { - return new FloatKeyframe(fraction); - } - - public static Keyframe ofObject(float fraction, Object value) { - return new ObjectKeyframe(fraction, value); - } - - public static Keyframe ofObject(float fraction) { - return new ObjectKeyframe(fraction, null); - } - - public boolean hasValue() { - return mHasValue; - } - - public abstract Object getValue(); - public abstract void setValue(Object value); - - public float getFraction() { - return mFraction; - } - - public void setFraction(float fraction) { - mFraction = fraction; - } - - public Interpolator getInterpolator() { - return mInterpolator; - } - - public void setInterpolator(Interpolator interpolator) { - mInterpolator = interpolator; - } - - public Class getType() { - return mValueType; - } - - @Override - public abstract Keyframe clone(); - - static class ObjectKeyframe extends Keyframe { - - Object mValue; - - ObjectKeyframe(float fraction, Object value) { - mFraction = fraction; - mValue = value; - mHasValue = (value != null); - mValueType = mHasValue ? value.getClass() : Object.class; - } - - public Object getValue() { - return mValue; - } - - public void setValue(Object value) { - mValue = value; - mHasValue = (value != null); - } - - @Override - public ObjectKeyframe clone() { - ObjectKeyframe kfClone = new ObjectKeyframe(getFraction(), mHasValue ? mValue : null); - kfClone.setInterpolator(getInterpolator()); - return kfClone; - } - } - - static class IntKeyframe extends Keyframe { - - int mValue; - - IntKeyframe(float fraction, int value) { - mFraction = fraction; - mValue = value; - mValueType = int.class; - mHasValue = true; - } - - IntKeyframe(float fraction) { - mFraction = fraction; - mValueType = int.class; - } - - public int getIntValue() { - return mValue; - } - - public Object getValue() { - return mValue; - } - - public void setValue(Object value) { - if (value != null && value.getClass() == Integer.class) { - mValue = (Integer) value; - mHasValue = true; - } - } - - @Override - public IntKeyframe clone() { - IntKeyframe kfClone = mHasValue ? new IntKeyframe(getFraction(), mValue) : new IntKeyframe(getFraction()); - kfClone.setInterpolator(getInterpolator()); - return kfClone; - } - } - - static class FloatKeyframe extends Keyframe { - - float mValue; - - FloatKeyframe(float fraction, float value) { - mFraction = fraction; - mValue = value; - mValueType = float.class; - mHasValue = true; - } - - FloatKeyframe(float fraction) { - mFraction = fraction; - mValueType = float.class; - } - - public float getFloatValue() { - return mValue; - } - - public Object getValue() { - return mValue; - } - - public void setValue(Object value) { - if (value != null && value.getClass() == Float.class) { - mValue = (Float) value; - mHasValue = true; - } - } - - @Override - public FloatKeyframe clone() { - FloatKeyframe kfClone = mHasValue ? new FloatKeyframe(getFraction(), mValue) : new FloatKeyframe(getFraction()); - kfClone.setInterpolator(getInterpolator()); - return kfClone; - } - } -} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/KeyframeSet.java b/TMessagesProj/src/main/java/org/telegram/messenger/Animation/KeyframeSet.java deleted file mode 100644 index 2e86dc876..000000000 --- a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/KeyframeSet.java +++ /dev/null @@ -1,200 +0,0 @@ -/* - * Copyright (C) 2010 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.messenger.Animation; - -import java.util.ArrayList; -import java.util.Arrays; -import android.util.Log; -import android.view.animation.Interpolator; - -import org.telegram.messenger.Animation.Keyframe.IntKeyframe; -import org.telegram.messenger.Animation.Keyframe.FloatKeyframe; -import org.telegram.messenger.Animation.Keyframe.ObjectKeyframe; - -class KeyframeSet { - - int mNumKeyframes; - - Keyframe mFirstKeyframe; - Keyframe mLastKeyframe; - Interpolator mInterpolator; - ArrayList mKeyframes; - TypeEvaluator mEvaluator; - - public KeyframeSet(Keyframe... keyframes) { - mNumKeyframes = keyframes.length; - mKeyframes = new ArrayList(); - mKeyframes.addAll(Arrays.asList(keyframes)); - mFirstKeyframe = mKeyframes.get(0); - mLastKeyframe = mKeyframes.get(mNumKeyframes - 1); - mInterpolator = mLastKeyframe.getInterpolator(); - } - - public static KeyframeSet ofInt(int... values) { - int numKeyframes = values.length; - IntKeyframe keyframes[] = new IntKeyframe[Math.max(numKeyframes,2)]; - if (numKeyframes == 1) { - keyframes[0] = (IntKeyframe) Keyframe.ofInt(0f); - keyframes[1] = (IntKeyframe) Keyframe.ofInt(1f, values[0]); - } else { - keyframes[0] = (IntKeyframe) Keyframe.ofInt(0f, values[0]); - for (int i = 1; i < numKeyframes; ++i) { - keyframes[i] = (IntKeyframe) Keyframe.ofInt((float) i / (numKeyframes - 1), values[i]); - } - } - return new IntKeyframeSet(keyframes); - } - - public static KeyframeSet ofFloat(float... values) { - boolean badValue = false; - int numKeyframes = values.length; - FloatKeyframe keyframes[] = new FloatKeyframe[Math.max(numKeyframes,2)]; - if (numKeyframes == 1) { - keyframes[0] = (FloatKeyframe) Keyframe.ofFloat(0f); - keyframes[1] = (FloatKeyframe) Keyframe.ofFloat(1f, values[0]); - if (Float.isNaN(values[0])) { - badValue = true; - } - } else { - keyframes[0] = (FloatKeyframe) Keyframe.ofFloat(0f, values[0]); - for (int i = 1; i < numKeyframes; ++i) { - keyframes[i] = (FloatKeyframe) Keyframe.ofFloat((float) i / (numKeyframes - 1), values[i]); - if (Float.isNaN(values[i])) { - badValue = true; - } - } - } - if (badValue) { - Log.w("Animator", "Bad value (NaN) in float animator"); - } - return new FloatKeyframeSet(keyframes); - } - - public static KeyframeSet ofKeyframe(Keyframe... keyframes) { - int numKeyframes = keyframes.length; - boolean hasFloat = false; - boolean hasInt = false; - boolean hasOther = false; - for (Keyframe keyframe : keyframes) { - if (keyframe instanceof FloatKeyframe) { - hasFloat = true; - } else if (keyframe instanceof IntKeyframe) { - hasInt = true; - } else { - hasOther = true; - } - } - if (hasFloat && !hasInt && !hasOther) { - FloatKeyframe floatKeyframes[] = new FloatKeyframe[numKeyframes]; - for (int i = 0; i < numKeyframes; ++i) { - floatKeyframes[i] = (FloatKeyframe) keyframes[i]; - } - return new FloatKeyframeSet(floatKeyframes); - } else if (hasInt && !hasFloat && !hasOther) { - IntKeyframe intKeyframes[] = new IntKeyframe[numKeyframes]; - for (int i = 0; i < numKeyframes; ++i) { - intKeyframes[i] = (IntKeyframe) keyframes[i]; - } - return new IntKeyframeSet(intKeyframes); - } else { - return new KeyframeSet(keyframes); - } - } - - public static KeyframeSet ofObject(Object... values) { - int numKeyframes = values.length; - ObjectKeyframe keyframes[] = new ObjectKeyframe[Math.max(numKeyframes,2)]; - if (numKeyframes == 1) { - keyframes[0] = (ObjectKeyframe) Keyframe.ofObject(0f); - keyframes[1] = (ObjectKeyframe) Keyframe.ofObject(1f, values[0]); - } else { - keyframes[0] = (ObjectKeyframe) Keyframe.ofObject(0f, values[0]); - for (int i = 1; i < numKeyframes; ++i) { - keyframes[i] = (ObjectKeyframe) Keyframe.ofObject((float) i / (numKeyframes - 1), values[i]); - } - } - return new KeyframeSet(keyframes); - } - - public void setEvaluator(TypeEvaluator evaluator) { - mEvaluator = evaluator; - } - - @Override - public KeyframeSet clone() { - ArrayList keyframes = mKeyframes; - int numKeyframes = mKeyframes.size(); - Keyframe[] newKeyframes = new Keyframe[numKeyframes]; - for (int i = 0; i < numKeyframes; ++i) { - newKeyframes[i] = keyframes.get(i).clone(); - } - return new KeyframeSet(newKeyframes); - } - - @SuppressWarnings("unchecked") - public Object getValue(float fraction) { - if (mNumKeyframes == 2) { - if (mInterpolator != null) { - fraction = mInterpolator.getInterpolation(fraction); - } - return mEvaluator.evaluate(fraction, mFirstKeyframe.getValue(), mLastKeyframe.getValue()); - } - if (fraction <= 0f) { - final Keyframe nextKeyframe = mKeyframes.get(1); - final Interpolator interpolator = nextKeyframe.getInterpolator(); - if (interpolator != null) { - fraction = interpolator.getInterpolation(fraction); - } - final float prevFraction = mFirstKeyframe.getFraction(); - float intervalFraction = (fraction - prevFraction) / (nextKeyframe.getFraction() - prevFraction); - return mEvaluator.evaluate(intervalFraction, mFirstKeyframe.getValue(), nextKeyframe.getValue()); - } else if (fraction >= 1f) { - final Keyframe prevKeyframe = mKeyframes.get(mNumKeyframes - 2); - final Interpolator interpolator = mLastKeyframe.getInterpolator(); - if (interpolator != null) { - fraction = interpolator.getInterpolation(fraction); - } - final float prevFraction = prevKeyframe.getFraction(); - float intervalFraction = (fraction - prevFraction) / (mLastKeyframe.getFraction() - prevFraction); - return mEvaluator.evaluate(intervalFraction, prevKeyframe.getValue(), mLastKeyframe.getValue()); - } - Keyframe prevKeyframe = mFirstKeyframe; - for (int i = 1; i < mNumKeyframes; ++i) { - Keyframe nextKeyframe = mKeyframes.get(i); - if (fraction < nextKeyframe.getFraction()) { - final Interpolator interpolator = nextKeyframe.getInterpolator(); - if (interpolator != null) { - fraction = interpolator.getInterpolation(fraction); - } - final float prevFraction = prevKeyframe.getFraction(); - float intervalFraction = (fraction - prevFraction) / (nextKeyframe.getFraction() - prevFraction); - return mEvaluator.evaluate(intervalFraction, prevKeyframe.getValue(), nextKeyframe.getValue()); - } - prevKeyframe = nextKeyframe; - } - return mLastKeyframe.getValue(); - } - - @Override - public String toString() { - String returnVal = " "; - for (int i = 0; i < mNumKeyframes; ++i) { - returnVal += mKeyframes.get(i).getValue() + " "; - } - return returnVal; - } -} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/ObjectAnimator10.java b/TMessagesProj/src/main/java/org/telegram/messenger/Animation/ObjectAnimator10.java deleted file mode 100644 index fef5f1851..000000000 --- a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/ObjectAnimator10.java +++ /dev/null @@ -1,488 +0,0 @@ -/* - * Copyright (C) 2010 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.messenger.Animation; - -import android.view.View; - -import java.util.HashMap; - -public final class ObjectAnimator10 extends ValueAnimator { - - private static final HashMap PROXY_PROPERTIES = new HashMap(); - - static { - Property ALPHA = new FloatProperty10("alpha") { - @Override - public void setValue(View object, float value) { - View10.wrap(object).setAlpha(value); - } - - @Override - public Float get(View object) { - return View10.wrap(object).getAlpha(); - } - }; - - Property PIVOT_X = new FloatProperty10("pivotX") { - @Override - public void setValue(View object, float value) { - View10.wrap(object).setPivotX(value); - } - - @Override - public Float get(View object) { - return View10.wrap(object).getPivotX(); - } - }; - - Property PIVOT_Y = new FloatProperty10("pivotY") { - @Override - public void setValue(View object, float value) { - View10.wrap(object).setPivotY(value); - } - - @Override - public Float get(View object) { - return View10.wrap(object).getPivotY(); - } - }; - - Property TRANSLATION_X = new FloatProperty10("translationX") { - @Override - public void setValue(View object, float value) { - View10.wrap(object).setTranslationX(value); - } - - @Override - public Float get(View object) { - return View10.wrap(object).getTranslationX(); - } - }; - - Property TRANSLATION_Y = new FloatProperty10("translationY") { - @Override - public void setValue(View object, float value) { - View10.wrap(object).setTranslationY(value); - } - - @Override - public Float get(View object) { - return View10.wrap(object).getTranslationY(); - } - }; - - Property ROTATION = new FloatProperty10("rotation") { - @Override - public void setValue(View object, float value) { - View10.wrap(object).setRotation(value); - } - - @Override - public Float get(View object) { - return View10.wrap(object).getRotation(); - } - }; - - Property ROTATION_X = new FloatProperty10("rotationX") { - @Override - public void setValue(View object, float value) { - View10.wrap(object).setRotationX(value); - } - - @Override - public Float get(View object) { - return View10.wrap(object).getRotationX(); - } - }; - - Property ROTATION_Y = new FloatProperty10("rotationY") { - @Override - public void setValue(View object, float value) { - View10.wrap(object).setRotationY(value); - } - - @Override - public Float get(View object) { - return View10.wrap(object).getRotationY(); - } - }; - - Property SCALE_X = new FloatProperty10("scaleX") { - @Override - public void setValue(View object, float value) { - View10.wrap(object).setScaleX(value); - } - - @Override - public Float get(View object) { - return View10.wrap(object).getScaleX(); - } - }; - - Property SCALE_Y = new FloatProperty10("scaleY") { - @Override - public void setValue(View object, float value) { - View10.wrap(object).setScaleY(value); - } - - @Override - public Float get(View object) { - return View10.wrap(object).getScaleY(); - } - }; - - Property SCROLL_X = new IntProperty("scrollX") { - @Override - public void setValue(View object, int value) { - View10.wrap(object).setScrollX(value); - } - - @Override - public Integer get(View object) { - return View10.wrap(object).getScrollX(); - } - }; - - Property SCROLL_Y = new IntProperty("scrollY") { - @Override - public void setValue(View object, int value) { - View10.wrap(object).setScrollY(value); - } - - @Override - public Integer get(View object) { - return View10.wrap(object).getScrollY(); - } - }; - - Property X = new FloatProperty10("x") { - @Override - public void setValue(View object, float value) { - View10.wrap(object).setX(value); - } - - @Override - public Float get(View object) { - return View10.wrap(object).getX(); - } - }; - - Property Y = new FloatProperty10("y") { - @Override - public void setValue(View object, float value) { - View10.wrap(object).setY(value); - } - - @Override - public Float get(View object) { - return View10.wrap(object).getY(); - } - }; - - PROXY_PROPERTIES.put("alpha", ALPHA); - PROXY_PROPERTIES.put("pivotX", PIVOT_X); - PROXY_PROPERTIES.put("pivotY", PIVOT_Y); - PROXY_PROPERTIES.put("translationX", TRANSLATION_X); - PROXY_PROPERTIES.put("translationY", TRANSLATION_Y); - PROXY_PROPERTIES.put("rotation", ROTATION); - PROXY_PROPERTIES.put("rotationX", ROTATION_X); - PROXY_PROPERTIES.put("rotationY", ROTATION_Y); - PROXY_PROPERTIES.put("scaleX", SCALE_X); - PROXY_PROPERTIES.put("scaleY", SCALE_Y); - PROXY_PROPERTIES.put("scrollX", SCROLL_X); - PROXY_PROPERTIES.put("scrollY", SCROLL_Y); - PROXY_PROPERTIES.put("x", X); - PROXY_PROPERTIES.put("y", Y); - } - - private Object mTarget; - private String mPropertyName; - private Property mProperty; - private boolean mAutoCancel = false; - - public void setPropertyName(String propertyName) { - if (mValues != null) { - PropertyValuesHolder valuesHolder = mValues[0]; - String oldName = valuesHolder.getPropertyName(); - valuesHolder.setPropertyName(propertyName); - mValuesMap.remove(oldName); - mValuesMap.put(propertyName, valuesHolder); - } - mPropertyName = propertyName; - mInitialized = false; - } - - public void setProperty(Property property) { - if (mValues != null) { - PropertyValuesHolder valuesHolder = mValues[0]; - String oldName = valuesHolder.getPropertyName(); - valuesHolder.setProperty(property); - mValuesMap.remove(oldName); - mValuesMap.put(mPropertyName, valuesHolder); - } - if (mProperty != null) { - mPropertyName = property.getName(); - } - mProperty = property; - mInitialized = false; - } - - public String getPropertyName() { - String propertyName = null; - if (mPropertyName != null) { - propertyName = mPropertyName; - } else if (mProperty != null) { - propertyName = mProperty.getName(); - } else if (mValues != null && mValues.length > 0) { - for (int i = 0; i < mValues.length; ++i) { - if (i == 0) { - propertyName = ""; - } else { - propertyName += ","; - } - propertyName += mValues[i].getPropertyName(); - } - } - return propertyName; - } - - public ObjectAnimator10() { - - } - - private ObjectAnimator10(Object target, String propertyName) { - mTarget = target; - setPropertyName(propertyName); - } - - private ObjectAnimator10(T target, Property property) { - mTarget = target; - setProperty(property); - } - - public static ObjectAnimator10 ofInt(Object target, String propertyName, int... values) { - ObjectAnimator10 anim = new ObjectAnimator10(target, propertyName); - anim.setIntValues(values); - return anim; - } - - public static ObjectAnimator10 ofInt(T target, Property property, int... values) { - ObjectAnimator10 anim = new ObjectAnimator10(target, property); - anim.setIntValues(values); - return anim; - } - - public static ObjectAnimator10 ofFloat(Object target, String propertyName, float... values) { - ObjectAnimator10 anim = new ObjectAnimator10(target, propertyName); - anim.setFloatValues(values); - return anim; - } - - public static ObjectAnimator10 ofFloat(T target, Property property, float... values) { - ObjectAnimator10 anim = new ObjectAnimator10(target, property); - anim.setFloatValues(values); - return anim; - } - - public static ObjectAnimator10 ofObject(Object target, String propertyName, TypeEvaluator evaluator, Object... values) { - ObjectAnimator10 anim = new ObjectAnimator10(target, propertyName); - anim.setObjectValues(values); - anim.setEvaluator(evaluator); - return anim; - } - - public static ObjectAnimator10 ofObject(T target, Property property, TypeEvaluator evaluator, V... values) { - ObjectAnimator10 anim = new ObjectAnimator10(target, property); - anim.setObjectValues(values); - anim.setEvaluator(evaluator); - return anim; - } - - public static ObjectAnimator10 ofPropertyValuesHolder(Object target, PropertyValuesHolder... values) { - ObjectAnimator10 anim = new ObjectAnimator10(); - anim.mTarget = target; - anim.setValues(values); - return anim; - } - - @SuppressWarnings("unchecked") - @Override - public void setIntValues(int... values) { - if (mValues == null || mValues.length == 0) { - if (mProperty != null) { - setValues(PropertyValuesHolder.ofInt(mProperty, values)); - } else { - setValues(PropertyValuesHolder.ofInt(mPropertyName, values)); - } - } else { - super.setIntValues(values); - } - } - - @SuppressWarnings("unchecked") - @Override - public void setFloatValues(float... values) { - if (mValues == null || mValues.length == 0) { - if (mProperty != null) { - setValues(PropertyValuesHolder.ofFloat(mProperty, values)); - } else { - setValues(PropertyValuesHolder.ofFloat(mPropertyName, values)); - } - } else { - super.setFloatValues(values); - } - } - - @Override - public void setObjectValues(Object... values) { - if (mValues == null || mValues.length == 0) { - if (mProperty != null) { - setValues(PropertyValuesHolder.ofObject(mProperty, null, values)); - } else { - setValues(PropertyValuesHolder.ofObject(mPropertyName, null, values)); - } - } else { - super.setObjectValues(values); - } - } - - public void setAutoCancel(boolean cancel) { - mAutoCancel = cancel; - } - - private boolean hasSameTargetAndProperties(Animator10 anim) { - if (anim instanceof ObjectAnimator10) { - PropertyValuesHolder[] theirValues = ((ObjectAnimator10) anim).getValues(); - if (((ObjectAnimator10) anim).getTarget() == mTarget && - mValues.length == theirValues.length) { - for (int i = 0; i < mValues.length; ++i) { - PropertyValuesHolder pvhMine = mValues[i]; - PropertyValuesHolder pvhTheirs = theirValues[i]; - if (pvhMine.getPropertyName() == null || - !pvhMine.getPropertyName().equals(pvhTheirs.getPropertyName())) { - return false; - } - } - return true; - } - } - return false; - } - - @Override - public void start() { - AnimationHandler handler = sAnimationHandler.get(); - if (handler != null) { - int numAnims = handler.mAnimations.size(); - for (int i = numAnims - 1; i >= 0; i--) { - if (handler.mAnimations.get(i) instanceof ObjectAnimator10) { - ObjectAnimator10 anim = (ObjectAnimator10) handler.mAnimations.get(i); - if (anim.mAutoCancel && hasSameTargetAndProperties(anim)) { - anim.cancel(); - } - } - } - numAnims = handler.mPendingAnimations.size(); - for (int i = numAnims - 1; i >= 0; i--) { - if (handler.mPendingAnimations.get(i) instanceof ObjectAnimator10) { - ObjectAnimator10 anim = (ObjectAnimator10) handler.mPendingAnimations.get(i); - if (anim.mAutoCancel && hasSameTargetAndProperties(anim)) { - anim.cancel(); - } - } - } - numAnims = handler.mDelayedAnims.size(); - for (int i = numAnims - 1; i >= 0; i--) { - if (handler.mDelayedAnims.get(i) instanceof ObjectAnimator10) { - ObjectAnimator10 anim = (ObjectAnimator10) handler.mDelayedAnims.get(i); - if (anim.mAutoCancel && hasSameTargetAndProperties(anim)) { - anim.cancel(); - } - } - } - } - super.start(); - } - - @Override - void initAnimation() { - if (!mInitialized) { - if ((mProperty == null) && (mTarget instanceof View) && PROXY_PROPERTIES.containsKey(mPropertyName)) { - setProperty(PROXY_PROPERTIES.get(mPropertyName)); - } - int numValues = mValues.length; - for (PropertyValuesHolder mValue : mValues) { - mValue.setupSetterAndGetter(mTarget); - } - super.initAnimation(); - } - } - - @Override - public ObjectAnimator10 setDuration(long duration) { - super.setDuration(duration); - return this; - } - - public Object getTarget() { - return mTarget; - } - - @Override - public void setTarget(Object target) { - if (mTarget != target) { - final Object oldTarget = mTarget; - mTarget = target; - if (oldTarget != null && target != null && oldTarget.getClass() == target.getClass()) { - return; - } - mInitialized = false; - } - } - - @Override - public void setupStartValues() { - initAnimation(); - int numValues = mValues.length; - for (PropertyValuesHolder mValue : mValues) { - mValue.setupStartValue(mTarget); - } - } - - @Override - public void setupEndValues() { - initAnimation(); - int numValues = mValues.length; - for (PropertyValuesHolder mValue : mValues) { - mValue.setupEndValue(mTarget); - } - } - - @Override - void animateValue(float fraction) { - super.animateValue(fraction); - int numValues = mValues.length; - for (PropertyValuesHolder mValue : mValues) { - mValue.setAnimatedValue(mTarget); - } - } - - @Override - public ObjectAnimator10 clone() { - return (ObjectAnimator10) super.clone(); - } -} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/Property.java b/TMessagesProj/src/main/java/org/telegram/messenger/Animation/Property.java deleted file mode 100755 index 98983e162..000000000 --- a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/Property.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (C) 2011 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.messenger.Animation; - -public abstract class Property { - - private final String mName; - private final Class mType; - - public static Property of(Class hostType, Class valueType, String name) { - return new ReflectiveProperty(hostType, valueType, name); - } - - public Property(Class type, String name) { - mName = name; - mType = type; - } - - public boolean isReadOnly() { - return false; - } - - public void set(T object, V value) { - throw new UnsupportedOperationException("Property " + getName() +" is read-only"); - } - - public abstract V get(T object); - - public String getName() { - return mName; - } - - public Class getType() { - return mType; - } -} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/PropertyValuesHolder.java b/TMessagesProj/src/main/java/org/telegram/messenger/Animation/PropertyValuesHolder.java deleted file mode 100644 index c4ab83b4c..000000000 --- a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/PropertyValuesHolder.java +++ /dev/null @@ -1,545 +0,0 @@ -/* - * Copyright (C) 2010 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.messenger.Animation; - -import java.lang.reflect.Method; -import java.util.HashMap; -import java.util.concurrent.locks.ReentrantReadWriteLock; - -public class PropertyValuesHolder implements Cloneable { - - String mPropertyName; - protected Property mProperty; - Method mSetter = null; - private Method mGetter = null; - Class mValueType; - KeyframeSet mKeyframeSet = null; - - private static final TypeEvaluator sIntEvaluator = new IntEvaluator(); - private static final TypeEvaluator sFloatEvaluator = new FloatEvaluator(); - - private static Class[] FLOAT_VARIANTS = {float.class, Float.class, double.class, int.class, Double.class, Integer.class}; - private static Class[] INTEGER_VARIANTS = {int.class, Integer.class, float.class, double.class, Float.class, Double.class}; - private static Class[] DOUBLE_VARIANTS = {double.class, Double.class, float.class, int.class, Float.class, Integer.class}; - - private static final HashMap> sSetterPropertyMap = new HashMap>(); - private static final HashMap> sGetterPropertyMap = new HashMap>(); - - final ReentrantReadWriteLock mPropertyMapLock = new ReentrantReadWriteLock(); - final Object[] mTmpValueArray = new Object[1]; - - private TypeEvaluator mEvaluator; - - private Object mAnimatedValue; - - private PropertyValuesHolder(String propertyName) { - mPropertyName = propertyName; - } - - private PropertyValuesHolder(Property property) { - mProperty = property; - if (property != null) { - mPropertyName = property.getName(); - } - } - - public static PropertyValuesHolder ofInt(String propertyName, int... values) { - return new IntPropertyValuesHolder(propertyName, values); - } - - public static PropertyValuesHolder ofInt(Property property, int... values) { - return new IntPropertyValuesHolder(property, values); - } - - public static PropertyValuesHolder ofFloat(String propertyName, float... values) { - return new FloatPropertyValuesHolder(propertyName, values); - } - - public static PropertyValuesHolder ofFloat(Property property, float... values) { - return new FloatPropertyValuesHolder(property, values); - } - - public static PropertyValuesHolder ofObject(String propertyName, TypeEvaluator evaluator, - Object... values) { - PropertyValuesHolder pvh = new PropertyValuesHolder(propertyName); - pvh.setObjectValues(values); - pvh.setEvaluator(evaluator); - return pvh; - } - - public static PropertyValuesHolder ofObject(Property property, - TypeEvaluator evaluator, V... values) { - PropertyValuesHolder pvh = new PropertyValuesHolder(property); - pvh.setObjectValues(values); - pvh.setEvaluator(evaluator); - return pvh; - } - - public static PropertyValuesHolder ofKeyframe(String propertyName, Keyframe... values) { - KeyframeSet keyframeSet = KeyframeSet.ofKeyframe(values); - if (keyframeSet instanceof IntKeyframeSet) { - return new IntPropertyValuesHolder(propertyName, (IntKeyframeSet) keyframeSet); - } else if (keyframeSet instanceof FloatKeyframeSet) { - return new FloatPropertyValuesHolder(propertyName, (FloatKeyframeSet) keyframeSet); - } else { - PropertyValuesHolder pvh = new PropertyValuesHolder(propertyName); - pvh.mKeyframeSet = keyframeSet; - pvh.mValueType = values[0].getType(); - return pvh; - } - } - - public static PropertyValuesHolder ofKeyframe(Property property, Keyframe... values) { - KeyframeSet keyframeSet = KeyframeSet.ofKeyframe(values); - if (keyframeSet instanceof IntKeyframeSet) { - return new IntPropertyValuesHolder(property, (IntKeyframeSet) keyframeSet); - } else if (keyframeSet instanceof FloatKeyframeSet) { - return new FloatPropertyValuesHolder(property, (FloatKeyframeSet) keyframeSet); - } else { - PropertyValuesHolder pvh = new PropertyValuesHolder(property); - pvh.mKeyframeSet = keyframeSet; - pvh.mValueType = values[0].getType(); - return pvh; - } - } - - public void setIntValues(int... values) { - mValueType = int.class; - mKeyframeSet = KeyframeSet.ofInt(values); - } - - public void setFloatValues(float... values) { - mValueType = float.class; - mKeyframeSet = KeyframeSet.ofFloat(values); - } - - public void setKeyframes(Keyframe... values) { - int numKeyframes = values.length; - Keyframe keyframes[] = new Keyframe[Math.max(numKeyframes, 2)]; - mValueType = values[0].getType(); - System.arraycopy(values, 0, keyframes, 0, numKeyframes); - mKeyframeSet = new KeyframeSet(keyframes); - } - - public void setObjectValues(Object... values) { - mValueType = values[0].getClass(); - mKeyframeSet = KeyframeSet.ofObject(values); - } - - @SuppressWarnings("unchecked") - private Method getPropertyFunction(Class targetClass, String prefix, Class valueType) { - Method returnVal = null; - String methodName = getMethodName(prefix, mPropertyName); - Class args[] = null; - if (valueType == null) { - try { - returnVal = targetClass.getMethod(methodName); - } catch (Throwable e) { - try { - returnVal = targetClass.getDeclaredMethod(methodName); - returnVal.setAccessible(true); - } catch (Throwable e2) { - e2.printStackTrace(); - } - } - } else { - args = new Class[1]; - Class typeVariants[]; - if (mValueType.equals(Float.class)) { - typeVariants = FLOAT_VARIANTS; - } else if (mValueType.equals(Integer.class)) { - typeVariants = INTEGER_VARIANTS; - } else if (mValueType.equals(Double.class)) { - typeVariants = DOUBLE_VARIANTS; - } else { - typeVariants = new Class[1]; - typeVariants[0] = mValueType; - } - for (Class typeVariant : typeVariants) { - args[0] = typeVariant; - try { - returnVal = targetClass.getMethod(methodName, args); - mValueType = typeVariant; - return returnVal; - } catch (Throwable e) { - try { - returnVal = targetClass.getDeclaredMethod(methodName, args); - returnVal.setAccessible(true); - mValueType = typeVariant; - return returnVal; - } catch (Throwable e2) { - // Swallow the error and keep trying other variants - } - } - } - } - - return returnVal; - } - - private Method setupSetterOrGetter(Class targetClass, HashMap> propertyMapMap, String prefix, Class valueType) { - Method setterOrGetter = null; - try { - mPropertyMapLock.writeLock().lock(); - HashMap propertyMap = propertyMapMap.get(targetClass); - if (propertyMap != null) { - setterOrGetter = propertyMap.get(mPropertyName); - } - if (setterOrGetter == null) { - setterOrGetter = getPropertyFunction(targetClass, prefix, valueType); - if (propertyMap == null) { - propertyMap = new HashMap(); - propertyMapMap.put(targetClass, propertyMap); - } - propertyMap.put(mPropertyName, setterOrGetter); - } - } finally { - mPropertyMapLock.writeLock().unlock(); - } - return setterOrGetter; - } - - void setupSetter(Class targetClass) { - mSetter = setupSetterOrGetter(targetClass, sSetterPropertyMap, "set", mValueType); - } - - private void setupGetter(Class targetClass) { - mGetter = setupSetterOrGetter(targetClass, sGetterPropertyMap, "get", null); - } - - @SuppressWarnings("unchecked") - void setupSetterAndGetter(Object target) { - if (mProperty != null) { - try { - Object testValue = mProperty.get(target); - for (Keyframe kf : mKeyframeSet.mKeyframes) { - if (!kf.hasValue()) { - kf.setValue(mProperty.get(target)); - } - } - return; - } catch (Throwable e) { - mProperty = null; - } - } - Class targetClass = target.getClass(); - if (mSetter == null) { - setupSetter(targetClass); - } - for (Keyframe kf : mKeyframeSet.mKeyframes) { - if (!kf.hasValue()) { - if (mGetter == null) { - setupGetter(targetClass); - if (mGetter == null) { - return; - } - } - try { - kf.setValue(mGetter.invoke(target)); - } catch (Throwable e) { - e.printStackTrace(); - } - } - } - } - - @SuppressWarnings("unchecked") - private void setupValue(Object target, Keyframe kf) { - if (mProperty != null) { - kf.setValue(mProperty.get(target)); - } - try { - if (mGetter == null) { - Class targetClass = target.getClass(); - setupGetter(targetClass); - if (mGetter == null) { - return; - } - } - kf.setValue(mGetter.invoke(target)); - } catch (Throwable e) { - e.printStackTrace(); - } - } - - void setupStartValue(Object target) { - setupValue(target, mKeyframeSet.mKeyframes.get(0)); - } - - void setupEndValue(Object target) { - setupValue(target, mKeyframeSet.mKeyframes.get(mKeyframeSet.mKeyframes.size() - 1)); - } - - @Override - public PropertyValuesHolder clone() { - try { - PropertyValuesHolder newPVH = (PropertyValuesHolder) super.clone(); - newPVH.mPropertyName = mPropertyName; - newPVH.mProperty = mProperty; - newPVH.mKeyframeSet = mKeyframeSet.clone(); - newPVH.mEvaluator = mEvaluator; - return newPVH; - } catch (CloneNotSupportedException e) { - return null; - } - } - - @SuppressWarnings("unchecked") - void setAnimatedValue(Object target) { - if (mProperty != null) { - mProperty.set(target, getAnimatedValue()); - } - if (mSetter != null) { - try { - mTmpValueArray[0] = getAnimatedValue(); - mSetter.invoke(target, mTmpValueArray); - } catch (Throwable e) { - e.printStackTrace(); - } - } - } - - void init() { - if (mEvaluator == null) { - mEvaluator = (mValueType == Integer.class) ? sIntEvaluator : (mValueType == Float.class) ? sFloatEvaluator : null; - } - if (mEvaluator != null) { - mKeyframeSet.setEvaluator(mEvaluator); - } - } - - public void setEvaluator(TypeEvaluator evaluator) { - mEvaluator = evaluator; - mKeyframeSet.setEvaluator(evaluator); - } - - void calculateValue(float fraction) { - mAnimatedValue = mKeyframeSet.getValue(fraction); - } - - public void setPropertyName(String propertyName) { - mPropertyName = propertyName; - } - - public void setProperty(Property property) { - mProperty = property; - } - - public String getPropertyName() { - return mPropertyName; - } - - Object getAnimatedValue() { - return mAnimatedValue; - } - - @Override - public String toString() { - return mPropertyName + ": " + mKeyframeSet.toString(); - } - - static String getMethodName(String prefix, String propertyName) { - if (propertyName == null || propertyName.length() == 0) { - return prefix; - } - char firstLetter = Character.toUpperCase(propertyName.charAt(0)); - String theRest = propertyName.substring(1); - return prefix + firstLetter + theRest; - } - - static class IntPropertyValuesHolder extends PropertyValuesHolder { - private static final HashMap> sJNISetterPropertyMap = new HashMap>(); - private IntProperty mIntProperty; - - IntKeyframeSet mIntKeyframeSet; - int mIntAnimatedValue; - - public IntPropertyValuesHolder(String propertyName, IntKeyframeSet keyframeSet) { - super(propertyName); - mValueType = int.class; - mKeyframeSet = keyframeSet; - mIntKeyframeSet = (IntKeyframeSet) mKeyframeSet; - } - - public IntPropertyValuesHolder(Property property, IntKeyframeSet keyframeSet) { - super(property); - mValueType = int.class; - mKeyframeSet = keyframeSet; - mIntKeyframeSet = (IntKeyframeSet) mKeyframeSet; - if (property instanceof IntProperty) { - mIntProperty = (IntProperty) mProperty; - } - } - - public IntPropertyValuesHolder(String propertyName, int... values) { - super(propertyName); - setIntValues(values); - } - - public IntPropertyValuesHolder(Property property, int... values) { - super(property); - setIntValues(values); - if (property instanceof IntProperty) { - mIntProperty = (IntProperty) mProperty; - } - } - - @Override - public void setIntValues(int... values) { - super.setIntValues(values); - mIntKeyframeSet = (IntKeyframeSet) mKeyframeSet; - } - - @Override - void calculateValue(float fraction) { - mIntAnimatedValue = mIntKeyframeSet.getIntValue(fraction); - } - - @Override - Object getAnimatedValue() { - return mIntAnimatedValue; - } - - @Override - public IntPropertyValuesHolder clone() { - IntPropertyValuesHolder newPVH = (IntPropertyValuesHolder) super.clone(); - newPVH.mIntKeyframeSet = (IntKeyframeSet) newPVH.mKeyframeSet; - return newPVH; - } - - @SuppressWarnings("unchecked") - @Override - void setAnimatedValue(Object target) { - if (mIntProperty != null) { - mIntProperty.setValue(target, mIntAnimatedValue); - return; - } - if (mProperty != null) { - mProperty.set(target, mIntAnimatedValue); - return; - } - if (mSetter != null) { - try { - mTmpValueArray[0] = mIntAnimatedValue; - mSetter.invoke(target, mTmpValueArray); - } catch (Throwable e) { - e.printStackTrace(); - } - } - } - - @Override - void setupSetter(Class targetClass) { - if (mProperty != null) { - return; - } - - super.setupSetter(targetClass); - } - } - - static class FloatPropertyValuesHolder extends PropertyValuesHolder { - - private static final HashMap> sJNISetterPropertyMap = new HashMap>(); - private FloatProperty10 mFloatProperty; - - FloatKeyframeSet mFloatKeyframeSet; - float mFloatAnimatedValue; - - public FloatPropertyValuesHolder(String propertyName, FloatKeyframeSet keyframeSet) { - super(propertyName); - mValueType = float.class; - mKeyframeSet = keyframeSet; - mFloatKeyframeSet = (FloatKeyframeSet) mKeyframeSet; - } - - public FloatPropertyValuesHolder(Property property, FloatKeyframeSet keyframeSet) { - super(property); - mValueType = float.class; - mKeyframeSet = keyframeSet; - mFloatKeyframeSet = (FloatKeyframeSet) mKeyframeSet; - if (property instanceof FloatProperty10) { - mFloatProperty = (FloatProperty10) mProperty; - } - } - - public FloatPropertyValuesHolder(String propertyName, float... values) { - super(propertyName); - setFloatValues(values); - } - - public FloatPropertyValuesHolder(Property property, float... values) { - super(property); - setFloatValues(values); - if (property instanceof FloatProperty10) { - mFloatProperty = (FloatProperty10) mProperty; - } - } - - @Override - public void setFloatValues(float... values) { - super.setFloatValues(values); - mFloatKeyframeSet = (FloatKeyframeSet) mKeyframeSet; - } - - @Override - void calculateValue(float fraction) { - mFloatAnimatedValue = mFloatKeyframeSet.getFloatValue(fraction); - } - - @Override - Object getAnimatedValue() { - return mFloatAnimatedValue; - } - - @Override - public FloatPropertyValuesHolder clone() { - FloatPropertyValuesHolder newPVH = (FloatPropertyValuesHolder) super.clone(); - newPVH.mFloatKeyframeSet = (FloatKeyframeSet) newPVH.mKeyframeSet; - return newPVH; - } - - @SuppressWarnings("unchecked") - @Override - void setAnimatedValue(Object target) { - if (mFloatProperty != null) { - mFloatProperty.setValue(target, mFloatAnimatedValue); - return; - } - if (mProperty != null) { - mProperty.set(target, mFloatAnimatedValue); - return; - } - if (mSetter != null) { - try { - mTmpValueArray[0] = mFloatAnimatedValue; - mSetter.invoke(target, mTmpValueArray); - } catch (Throwable e) { - e.printStackTrace(); - } - } - } - - @Override - void setupSetter(Class targetClass) { - if (mProperty != null) { - return; - } - super.setupSetter(targetClass); - } - } -} \ No newline at end of file diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/ReflectiveProperty.java b/TMessagesProj/src/main/java/org/telegram/messenger/Animation/ReflectiveProperty.java deleted file mode 100755 index 47b65aac9..000000000 --- a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/ReflectiveProperty.java +++ /dev/null @@ -1,182 +0,0 @@ -/* - * Copyright (C) 2011 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.messenger.Animation; - -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; - -/** - * Internal class to automatically generate a Property for a given class/name pair, given the - * specification of {@link Property#of(java.lang.Class, java.lang.Class, java.lang.String)} - */ -class ReflectiveProperty extends Property { - - private static final String PREFIX_GET = "get"; - private static final String PREFIX_IS = "is"; - private static final String PREFIX_SET = "set"; - private Method mSetter; - private Method mGetter; - private Field mField; - - /** - * For given property name 'name', look for getName/isName method or 'name' field. - * Also look for setName method (optional - could be readonly). Failing method getters and - * field results in throwing NoSuchPropertyException. - * - * @param propertyHolder The class on which the methods or field are found - * @param name The name of the property, where this name is capitalized and appended to - * "get" and "is to search for the appropriate methods. If the get/is methods are not found, - * the constructor will search for a field with that exact name. - */ - public ReflectiveProperty(Class propertyHolder, Class valueType, String name) { - // TODO: cache reflection info for each new class/name pair - super(valueType, name); - char firstLetter = Character.toUpperCase(name.charAt(0)); - String theRest = name.substring(1); - String capitalizedName = firstLetter + theRest; - String getterName = PREFIX_GET + capitalizedName; - try { - mGetter = propertyHolder.getMethod(getterName, (Class[]) null); - } catch (NoSuchMethodException e) { - try { - /* The native implementation uses JNI to do reflection, which allows access to private methods. - * getDeclaredMethod(..) does not find superclass methods, so it's implemented as a fallback. - */ - mGetter = propertyHolder.getDeclaredMethod(getterName, (Class[]) null); - mGetter.setAccessible(true); - } catch (NoSuchMethodException e2) { - // getName() not available - try isName() instead - getterName = PREFIX_IS + capitalizedName; - try { - mGetter = propertyHolder.getMethod(getterName, (Class[]) null); - } catch (NoSuchMethodException e3) { - try { - /* The native implementation uses JNI to do reflection, which allows access to private methods. - * getDeclaredMethod(..) does not find superclass methods, so it's implemented as a fallback. - */ - mGetter = propertyHolder.getDeclaredMethod(getterName, (Class[]) null); - mGetter.setAccessible(true); - } catch (NoSuchMethodException e4) { - // Try public field instead - try { - mField = propertyHolder.getField(name); - Class fieldType = mField.getType(); - if (!typesMatch(valueType, fieldType)) { - throw new NoSuchPropertyException("Underlying type (" + fieldType + ") " + - "does not match Property type (" + valueType + ")"); - } - return; - } catch (NoSuchFieldException e5) { - // no way to access property - throw appropriate exception - throw new NoSuchPropertyException("No accessor method or field found for" - + " property with name " + name); - } - } - } - } - } - Class getterType = mGetter.getReturnType(); - // Check to make sure our getter type matches our valueType - if (!typesMatch(valueType, getterType)) { - throw new NoSuchPropertyException("Underlying type (" + getterType + ") " + - "does not match Property type (" + valueType + ")"); - } - String setterName = PREFIX_SET + capitalizedName; - try { - // mSetter = propertyHolder.getMethod(setterName, getterType); - // The native implementation uses JNI to do reflection, which allows access to private methods. - mSetter = propertyHolder.getDeclaredMethod(setterName, getterType); - mSetter.setAccessible(true); - } catch (NoSuchMethodException ignored) { - // Okay to not have a setter - just a readonly property - } - } - - /** - * Utility method to check whether the type of the underlying field/method on the target - * object matches the type of the Property. The extra checks for primitive types are because - * generics will force the Property type to be a class, whereas the type of the underlying - * method/field will probably be a primitive type instead. Accept float as matching Float, - * etc. - */ - private boolean typesMatch(Class valueType, Class getterType) { - if (getterType != valueType) { - if (getterType.isPrimitive()) { - return (getterType == float.class && valueType == Float.class) || - (getterType == int.class && valueType == Integer.class) || - (getterType == boolean.class && valueType == Boolean.class) || - (getterType == long.class && valueType == Long.class) || - (getterType == double.class && valueType == Double.class) || - (getterType == short.class && valueType == Short.class) || - (getterType == byte.class && valueType == Byte.class) || - (getterType == char.class && valueType == Character.class); - } - return false; - } - return true; - } - - @Override - public void set(T object, V value) { - if (mSetter != null) { - try { - mSetter.invoke(object, value); - } catch (IllegalAccessException e) { - throw new AssertionError(); - } catch (InvocationTargetException e) { - throw new RuntimeException(e.getCause()); - } - } else if (mField != null) { - try { - mField.set(object, value); - } catch (IllegalAccessException e) { - throw new AssertionError(); - } - } else { - throw new UnsupportedOperationException("Property " + getName() +" is read-only"); - } - } - - @Override - public V get(T object) { - if (mGetter != null) { - try { - return (V) mGetter.invoke(object, (Object[])null); - } catch (IllegalAccessException e) { - throw new AssertionError(); - } catch (InvocationTargetException e) { - throw new RuntimeException(e.getCause()); - } - } else if (mField != null) { - try { - return (V) mField.get(object); - } catch (IllegalAccessException e) { - throw new AssertionError(); - } - } - // Should not get here: there should always be a non-null getter or field - throw new AssertionError(); - } - - /** - * Returns false if there is no setter or public field underlying this Property. - */ - @Override - public boolean isReadOnly() { - return (mSetter == null && mField == null); - } -} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/ValueAnimator.java b/TMessagesProj/src/main/java/org/telegram/messenger/Animation/ValueAnimator.java deleted file mode 100644 index ec4648883..000000000 --- a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/ValueAnimator.java +++ /dev/null @@ -1,675 +0,0 @@ -/* - * Copyright (C) 2010 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.messenger.Animation; - -import android.os.Looper; -import android.util.AndroidRuntimeException; -import android.view.animation.AccelerateDecelerateInterpolator; -import android.view.animation.AnimationUtils; -import android.view.animation.Interpolator; -import android.view.animation.LinearInterpolator; - -import org.telegram.messenger.AndroidUtilities; - -import java.util.ArrayList; -import java.util.HashMap; - -public class ValueAnimator extends Animator10 { - - private static float sDurationScale = 1.0f; - static final int STOPPED = 0; - static final int RUNNING = 1; - static final int SEEKED = 2; - - long mStartTime; - long mSeekTime = -1; - private long mPauseTime; - private boolean mResumed = false; - protected static ThreadLocal sAnimationHandler = new ThreadLocal(); - private static final Interpolator sDefaultInterpolator = new AccelerateDecelerateInterpolator(); - private boolean mPlayingBackwards = false; - private int mCurrentIteration = 0; - private float mCurrentFraction = 0f; - private boolean mStartedDelay = false; - private long mDelayStartTime; - int mPlayingState = STOPPED; - private boolean mRunning = false; - private boolean mStarted = false; - private boolean mStartListenersCalled = false; - boolean mInitialized = false; - - private long mDuration = (long)(300 * sDurationScale); - private long mUnscaledDuration = 300; - private long mStartDelay = 0; - private long mUnscaledStartDelay = 0; - private int mRepeatCount = 0; - private int mRepeatMode = RESTART; - private Interpolator mInterpolator = sDefaultInterpolator; - private ArrayList mUpdateListeners = null; - PropertyValuesHolder[] mValues; - HashMap mValuesMap; - - public static final int RESTART = 1; - public static final int REVERSE = 2; - public static final int INFINITE = -1; - - public static void setDurationScale(float durationScale) { - sDurationScale = durationScale; - } - - public static float getDurationScale() { - return sDurationScale; - } - - public ValueAnimator() { - - } - - public static ValueAnimator ofInt(int... values) { - ValueAnimator anim = new ValueAnimator(); - anim.setIntValues(values); - return anim; - } - - public static ValueAnimator ofFloat(float... values) { - ValueAnimator anim = new ValueAnimator(); - anim.setFloatValues(values); - return anim; - } - - public static ValueAnimator ofPropertyValuesHolder(PropertyValuesHolder... values) { - ValueAnimator anim = new ValueAnimator(); - anim.setValues(values); - return anim; - } - - public static ValueAnimator ofObject(TypeEvaluator evaluator, Object... values) { - ValueAnimator anim = new ValueAnimator(); - anim.setObjectValues(values); - anim.setEvaluator(evaluator); - return anim; - } - - public void setIntValues(int... values) { - if (values == null || values.length == 0) { - return; - } - if (mValues == null || mValues.length == 0) { - setValues(PropertyValuesHolder.ofInt("", values)); - } else { - PropertyValuesHolder valuesHolder = mValues[0]; - valuesHolder.setIntValues(values); - } - mInitialized = false; - } - - public void setFloatValues(float... values) { - if (values == null || values.length == 0) { - return; - } - if (mValues == null || mValues.length == 0) { - setValues(PropertyValuesHolder.ofFloat("", values)); - } else { - PropertyValuesHolder valuesHolder = mValues[0]; - valuesHolder.setFloatValues(values); - } - mInitialized = false; - } - - public void setObjectValues(Object... values) { - if (values == null || values.length == 0) { - return; - } - if (mValues == null || mValues.length == 0) { - setValues(PropertyValuesHolder.ofObject("", null, values)); - } else { - PropertyValuesHolder valuesHolder = mValues[0]; - valuesHolder.setObjectValues(values); - } - mInitialized = false; - } - - public void setValues(PropertyValuesHolder... values) { - int numValues = values.length; - mValues = values; - mValuesMap = new HashMap(numValues); - for (PropertyValuesHolder valuesHolder : values) { - mValuesMap.put(valuesHolder.getPropertyName(), valuesHolder); - } - mInitialized = false; - } - - public PropertyValuesHolder[] getValues() { - return mValues; - } - - void initAnimation() { - if (!mInitialized) { - int numValues = mValues.length; - for (PropertyValuesHolder mValue : mValues) { - mValue.init(); - } - mInitialized = true; - } - } - - public ValueAnimator setDuration(long duration) { - if (duration < 0) { - throw new IllegalArgumentException("Animators cannot have negative duration: " + duration); - } - mUnscaledDuration = duration; - mDuration = (long)(duration * sDurationScale); - return this; - } - - public long getDuration() { - return mUnscaledDuration; - } - - public void setCurrentPlayTime(long playTime) { - initAnimation(); - long currentTime = AnimationUtils.currentAnimationTimeMillis(); - if (mPlayingState != RUNNING) { - mSeekTime = playTime; - mPlayingState = SEEKED; - } - mStartTime = currentTime - playTime; - doAnimationFrame(currentTime); - } - - public long getCurrentPlayTime() { - if (!mInitialized || mPlayingState == STOPPED) { - return 0; - } - return AnimationUtils.currentAnimationTimeMillis() - mStartTime; - } - - @SuppressWarnings("unchecked") - protected static class AnimationHandler implements Runnable { - - protected final ArrayList mAnimations = new ArrayList(); - private final ArrayList mTmpAnimations = new ArrayList(); - protected final ArrayList mPendingAnimations = new ArrayList(); - protected final ArrayList mDelayedAnims = new ArrayList(); - private final ArrayList mEndingAnims = new ArrayList(); - private final ArrayList mReadyAnims = new ArrayList(); - - private boolean mAnimationScheduled; - - public void start() { - scheduleAnimation(); - } - - private void doAnimationFrame(long frameTime) { - while (mPendingAnimations.size() > 0) { - ArrayList pendingCopy = (ArrayList) mPendingAnimations.clone(); - mPendingAnimations.clear(); - int count = pendingCopy.size(); - for (ValueAnimator anim : pendingCopy) { - if (anim.mStartDelay == 0) { - anim.startAnimation(this); - } else { - mDelayedAnims.add(anim); - } - } - } - - int numDelayedAnims = mDelayedAnims.size(); - for (ValueAnimator anim : mDelayedAnims) { - if (anim.delayedAnimationFrame(frameTime)) { - mReadyAnims.add(anim); - } - } - int numReadyAnims = mReadyAnims.size(); - if (numReadyAnims > 0) { - for (ValueAnimator anim : mReadyAnims) { - anim.startAnimation(this); - anim.mRunning = true; - mDelayedAnims.remove(anim); - } - mReadyAnims.clear(); - } - - int numAnims = mAnimations.size(); - for (ValueAnimator mAnimation : mAnimations) { - mTmpAnimations.add(mAnimation); - } - for (int i = 0; i < numAnims; ++i) { - ValueAnimator anim = mTmpAnimations.get(i); - if (mAnimations.contains(anim) && anim.doAnimationFrame(frameTime)) { - mEndingAnims.add(anim); - } - } - mTmpAnimations.clear(); - if (mEndingAnims.size() > 0) { - for (ValueAnimator mEndingAnim : mEndingAnims) { - mEndingAnim.endAnimation(this); - } - mEndingAnims.clear(); - } - - if (!mAnimations.isEmpty() || !mDelayedAnims.isEmpty()) { - scheduleAnimation(); - } - } - - @Override - public void run() { - mAnimationScheduled = false; - doAnimationFrame(System.nanoTime() / 1000000); - } - - private void scheduleAnimation() { - if (!mAnimationScheduled) { - AndroidUtilities.runOnUIThread(this); - mAnimationScheduled = true; - } - } - } - - public long getStartDelay() { - return mUnscaledStartDelay; - } - - public void setStartDelay(long startDelay) { - this.mStartDelay = (long)(startDelay * sDurationScale); - mUnscaledStartDelay = startDelay; - } - - public Object getAnimatedValue() { - if (mValues != null && mValues.length > 0) { - return mValues[0].getAnimatedValue(); - } - return null; - } - - public Object getAnimatedValue(String propertyName) { - PropertyValuesHolder valuesHolder = mValuesMap.get(propertyName); - if (valuesHolder != null) { - return valuesHolder.getAnimatedValue(); - } else { - return null; - } - } - - public void setRepeatCount(int value) { - mRepeatCount = value; - } - - public int getRepeatCount() { - return mRepeatCount; - } - - public void setRepeatMode(int value) { - mRepeatMode = value; - } - - public int getRepeatMode() { - return mRepeatMode; - } - - public void addUpdateListener(AnimatorUpdateListener listener) { - if (mUpdateListeners == null) { - mUpdateListeners = new ArrayList(); - } - mUpdateListeners.add(listener); - } - - public void removeAllUpdateListeners() { - if (mUpdateListeners == null) { - return; - } - mUpdateListeners.clear(); - mUpdateListeners = null; - } - - public void removeUpdateListener(AnimatorUpdateListener listener) { - if (mUpdateListeners == null) { - return; - } - mUpdateListeners.remove(listener); - if (mUpdateListeners.size() == 0) { - mUpdateListeners = null; - } - } - - @Override - public void setInterpolator(Interpolator value) { - if (value != null) { - mInterpolator = value; - } else { - mInterpolator = new LinearInterpolator(); - } - } - - @Override - public Interpolator getInterpolator() { - return mInterpolator; - } - - public void setEvaluator(TypeEvaluator value) { - if (value != null && mValues != null && mValues.length > 0) { - mValues[0].setEvaluator(value); - } - } - - @SuppressWarnings("unchecked") - private void notifyStartListeners() { - if (mListeners != null && !mStartListenersCalled) { - ArrayList tmpListeners = (ArrayList) mListeners.clone(); - int numListeners = tmpListeners.size(); - for (AnimatorListener tmpListener : tmpListeners) { - tmpListener.onAnimationStart(this); - } - } - mStartListenersCalled = true; - } - - private void start(boolean playBackwards) { - if (Looper.myLooper() == null) { - throw new AndroidRuntimeException("Animators may only be run on Looper threads"); - } - mPlayingBackwards = playBackwards; - mCurrentIteration = 0; - mPlayingState = STOPPED; - mStarted = true; - mStartedDelay = false; - mPaused = false; - AnimationHandler animationHandler = getOrCreateAnimationHandler(); - animationHandler.mPendingAnimations.add(this); - if (mStartDelay == 0) { - setCurrentPlayTime(0); - mPlayingState = STOPPED; - mRunning = true; - notifyStartListeners(); - } - animationHandler.start(); - } - - @Override - public void start() { - start(false); - } - - @SuppressWarnings("unchecked") - @Override - public void cancel() { - AnimationHandler handler = getOrCreateAnimationHandler(); - if (mPlayingState != STOPPED || handler.mPendingAnimations.contains(this) || handler.mDelayedAnims.contains(this)) { - if ((mStarted || mRunning) && mListeners != null) { - if (!mRunning) { - notifyStartListeners(); - } - ArrayList tmpListeners = (ArrayList) mListeners.clone(); - for (AnimatorListener listener : tmpListeners) { - listener.onAnimationCancel(this); - } - } - endAnimation(handler); - } - } - - @Override - public void end() { - AnimationHandler handler = getOrCreateAnimationHandler(); - if (!handler.mAnimations.contains(this) && !handler.mPendingAnimations.contains(this)) { - mStartedDelay = false; - startAnimation(handler); - mStarted = true; - } else if (!mInitialized) { - initAnimation(); - } - animateValue(mPlayingBackwards ? 0f : 1f); - endAnimation(handler); - } - - @Override - public void resume() { - if (mPaused) { - mResumed = true; - } - super.resume(); - } - - @Override - public void pause() { - boolean previouslyPaused = mPaused; - super.pause(); - if (!previouslyPaused && mPaused) { - mPauseTime = -1; - mResumed = false; - } - } - - @Override - public boolean isRunning() { - return (mPlayingState == RUNNING || mRunning); - } - - @Override - public boolean isStarted() { - return mStarted; - } - - public void reverse() { - mPlayingBackwards = !mPlayingBackwards; - if (mPlayingState == RUNNING) { - long currentTime = AnimationUtils.currentAnimationTimeMillis(); - long currentPlayTime = currentTime - mStartTime; - long timeLeft = mDuration - currentPlayTime; - mStartTime = currentTime - timeLeft; - } else if (mStarted) { - end(); - } else { - start(true); - } - } - - @SuppressWarnings("unchecked") - private void endAnimation(AnimationHandler handler) { - handler.mAnimations.remove(this); - handler.mPendingAnimations.remove(this); - handler.mDelayedAnims.remove(this); - mPlayingState = STOPPED; - mPaused = false; - if ((mStarted || mRunning) && mListeners != null) { - if (!mRunning) { - notifyStartListeners(); - } - ArrayList tmpListeners = (ArrayList) mListeners.clone(); - int numListeners = tmpListeners.size(); - for (AnimatorListener tmpListener : tmpListeners) { - tmpListener.onAnimationEnd(this); - } - } - mRunning = false; - mStarted = false; - mStartListenersCalled = false; - mPlayingBackwards = false; - } - - private void startAnimation(AnimationHandler handler) { - initAnimation(); - handler.mAnimations.add(this); - if (mStartDelay > 0 && mListeners != null) { - notifyStartListeners(); - } - } - - private boolean delayedAnimationFrame(long currentTime) { - if (!mStartedDelay) { - mStartedDelay = true; - mDelayStartTime = currentTime; - } else { - if (mPaused) { - if (mPauseTime < 0) { - mPauseTime = currentTime; - } - return false; - } else if (mResumed) { - mResumed = false; - if (mPauseTime > 0) { - mDelayStartTime += (currentTime - mPauseTime); - } - } - long deltaTime = currentTime - mDelayStartTime; - if (deltaTime > mStartDelay) { - mStartTime = currentTime - (deltaTime - mStartDelay); - mPlayingState = RUNNING; - return true; - } - } - return false; - } - - boolean animationFrame(long currentTime) { - boolean done = false; - switch (mPlayingState) { - case RUNNING: - case SEEKED: - float fraction = mDuration > 0 ? (float)(currentTime - mStartTime) / mDuration : 1f; - if (fraction >= 1f) { - if (mCurrentIteration < mRepeatCount || mRepeatCount == INFINITE) { - if (mListeners != null) { - int numListeners = mListeners.size(); - for (AnimatorListener mListener : mListeners) { - mListener.onAnimationRepeat(this); - } - } - if (mRepeatMode == REVERSE) { - mPlayingBackwards = !mPlayingBackwards; - } - mCurrentIteration += (int)fraction; - fraction = fraction % 1f; - mStartTime += mDuration; - } else { - done = true; - fraction = Math.min(fraction, 1.0f); - } - } - if (mPlayingBackwards) { - fraction = 1f - fraction; - } - animateValue(fraction); - break; - } - - return done; - } - - final boolean doAnimationFrame(long frameTime) { - if (mPlayingState == STOPPED) { - mPlayingState = RUNNING; - if (mSeekTime < 0) { - mStartTime = frameTime; - } else { - mStartTime = frameTime - mSeekTime; - mSeekTime = -1; - } - } - if (mPaused) { - if (mPauseTime < 0) { - mPauseTime = frameTime; - } - return false; - } else if (mResumed) { - mResumed = false; - if (mPauseTime > 0) { - mStartTime += (frameTime - mPauseTime); - } - } - final long currentTime = Math.max(frameTime, mStartTime); - return animationFrame(currentTime); - } - - public float getAnimatedFraction() { - return mCurrentFraction; - } - - void animateValue(float fraction) { - fraction = mInterpolator.getInterpolation(fraction); - mCurrentFraction = fraction; - int numValues = mValues.length; - for (PropertyValuesHolder mValue : mValues) { - mValue.calculateValue(fraction); - } - if (mUpdateListeners != null) { - int numListeners = mUpdateListeners.size(); - for (AnimatorUpdateListener mUpdateListener : mUpdateListeners) { - mUpdateListener.onAnimationUpdate(this); - } - } - } - - @Override - public ValueAnimator clone() { - final ValueAnimator anim = (ValueAnimator) super.clone(); - if (mUpdateListeners != null) { - ArrayList oldListeners = mUpdateListeners; - anim.mUpdateListeners = new ArrayList(); - int numListeners = oldListeners.size(); - for (AnimatorUpdateListener oldListener : oldListeners) { - anim.mUpdateListeners.add(oldListener); - } - } - anim.mSeekTime = -1; - anim.mPlayingBackwards = false; - anim.mCurrentIteration = 0; - anim.mInitialized = false; - anim.mPlayingState = STOPPED; - anim.mStartedDelay = false; - PropertyValuesHolder[] oldValues = mValues; - if (oldValues != null) { - int numValues = oldValues.length; - anim.mValues = new PropertyValuesHolder[numValues]; - anim.mValuesMap = new HashMap(numValues); - for (int i = 0; i < numValues; ++i) { - PropertyValuesHolder newValuesHolder = oldValues[i].clone(); - anim.mValues[i] = newValuesHolder; - anim.mValuesMap.put(newValuesHolder.getPropertyName(), newValuesHolder); - } - } - return anim; - } - - public interface AnimatorUpdateListener { - void onAnimationUpdate(ValueAnimator animation); - } - - public static int getCurrentAnimationsCount() { - AnimationHandler handler = sAnimationHandler.get(); - return handler != null ? handler.mAnimations.size() : 0; - } - - public static void clearAllAnimations() { - AnimationHandler handler = sAnimationHandler.get(); - if (handler != null) { - handler.mAnimations.clear(); - handler.mPendingAnimations.clear(); - handler.mDelayedAnims.clear(); - } - } - - private static AnimationHandler getOrCreateAnimationHandler() { - AnimationHandler handler = sAnimationHandler.get(); - if (handler == null) { - handler = new AnimationHandler(); - sAnimationHandler.set(handler); - } - return handler; - } -} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/View10.java b/TMessagesProj/src/main/java/org/telegram/messenger/Animation/View10.java deleted file mode 100644 index 540b61a95..000000000 --- a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/View10.java +++ /dev/null @@ -1,349 +0,0 @@ -/* - Copyright 2012 Jake Wharton - -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.messenger.Animation; - -import android.graphics.Camera; -import android.graphics.Matrix; -import android.graphics.RectF; -import android.os.Build; -import android.view.View; -import android.view.animation.Animation; -import android.view.animation.Transformation; - -import java.lang.ref.WeakReference; -import java.util.WeakHashMap; - -public class View10 extends Animation { - - public static boolean NEED_PROXY = Build.VERSION.SDK_INT < 11; - - private static final WeakHashMap PROXIES = new WeakHashMap<>(); - - public static View10 wrap(View view) { - View10 proxy = PROXIES.get(view); - Animation animation = view.getAnimation(); - if (proxy == null || proxy != animation && animation != null) { - proxy = new View10(view); - PROXIES.put(view, proxy); - } else if (animation == null) { - view.setAnimation(proxy); - } - return proxy; - } - - private final WeakReference mView; - private final Camera mCamera = new Camera(); - private boolean mHasPivot; - - private float mAlpha = 1; - private float mPivotX; - private float mPivotY; - private float mRotationX; - private float mRotationY; - private float mRotationZ; - private float mScaleX = 1; - private float mScaleY = 1; - private float mTranslationX; - private float mTranslationY; - - private final RectF mBefore = new RectF(); - private final RectF mAfter = new RectF(); - private final Matrix mTempMatrix = new Matrix(); - - private View10(View view) { - setDuration(0); - setFillAfter(true); - view.setAnimation(this); - mView = new WeakReference<>(view); - } - - public float getAlpha() { - return mAlpha; - } - - public void setAlpha(float alpha) { - if (mAlpha != alpha) { - mAlpha = alpha; - View view = mView.get(); - if (view != null) { - view.invalidate(); - } - } - } - - public float getPivotX() { - return mPivotX; - } - - public void setPivotX(float pivotX) { - if (!mHasPivot || mPivotX != pivotX) { - prepareForUpdate(); - mHasPivot = true; - mPivotX = pivotX; - invalidateAfterUpdate(); - } - } - - public float getPivotY() { - return mPivotY; - } - - public void setPivotY(float pivotY) { - if (!mHasPivot || mPivotY != pivotY) { - prepareForUpdate(); - mHasPivot = true; - mPivotY = pivotY; - invalidateAfterUpdate(); - } - } - - public float getRotation() { - return mRotationZ; - } - - public void setRotation(float rotation) { - if (mRotationZ != rotation) { - prepareForUpdate(); - mRotationZ = rotation; - invalidateAfterUpdate(); - } - } - - public float getRotationX() { - return mRotationX; - } - - public void setRotationX(float rotationX) { - if (mRotationX != rotationX) { - prepareForUpdate(); - mRotationX = rotationX; - invalidateAfterUpdate(); - } - } - - public float getRotationY() { - return mRotationY; - } - - public void setRotationY(float rotationY) { - if (mRotationY != rotationY) { - prepareForUpdate(); - mRotationY = rotationY; - invalidateAfterUpdate(); - } - } - - public float getScaleX() { - return mScaleX; - } - - public void setScaleX(float scaleX) { - if (mScaleX != scaleX) { - prepareForUpdate(); - mScaleX = scaleX; - invalidateAfterUpdate(); - } - } - - public float getScaleY() { - return mScaleY; - } - - public void setScaleY(float scaleY) { - if (mScaleY != scaleY) { - prepareForUpdate(); - mScaleY = scaleY; - invalidateAfterUpdate(); - } - } - - public int getScrollX() { - View view = mView.get(); - if (view == null) { - return 0; - } - return view.getScrollX(); - } - - public void setScrollX(int value) { - View view = mView.get(); - if (view != null) { - view.scrollTo(value, view.getScrollY()); - } - } - - public int getScrollY() { - View view = mView.get(); - if (view == null) { - return 0; - } - return view.getScrollY(); - } - - public void setScrollY(int value) { - View view = mView.get(); - if (view != null) { - view.scrollTo(view.getScrollX(), value); - } - } - - public float getTranslationX() { - return mTranslationX; - } - - public void setTranslationX(float translationX) { - if (mTranslationX != translationX) { - prepareForUpdate(); - mTranslationX = translationX; - invalidateAfterUpdate(); - } - } - - public float getTranslationY() { - return mTranslationY; - } - - public void setTranslationY(float translationY) { - if (mTranslationY != translationY) { - prepareForUpdate(); - mTranslationY = translationY; - invalidateAfterUpdate(); - } - } - - public float getX() { - View view = mView.get(); - if (view == null) { - return 0; - } - return view.getLeft() + mTranslationX; - } - - public void setX(float x) { - View view = mView.get(); - if (view != null) { - setTranslationX(x - view.getLeft()); - } - } - - public float getY() { - View view = mView.get(); - if (view == null) { - return 0; - } - return view.getTop() + mTranslationY; - } - - public void setY(float y) { - View view = mView.get(); - if (view != null) { - setTranslationY(y - view.getTop()); - } - } - - private void prepareForUpdate() { - View view = mView.get(); - if (view != null) { - computeRect(mBefore, view); - } - } - - private void invalidateAfterUpdate() { - View view = mView.get(); - if (view == null || view.getParent() == null) { - return; - } - - final RectF after = mAfter; - computeRect(after, view); - after.union(mBefore); - - ((View) view.getParent()).invalidate( - (int) Math.floor(after.left), - (int) Math.floor(after.top), - (int) Math.ceil(after.right), - (int) Math.ceil(after.bottom)); - } - - private void computeRect(final RectF r, View view) { - final float w = view.getWidth(); - final float h = view.getHeight(); - - r.set(0, 0, w, h); - - final Matrix m = mTempMatrix; - m.reset(); - transformMatrix(m, view); - mTempMatrix.mapRect(r); - - r.offset(view.getLeft(), view.getTop()); - - if (r.right < r.left) { - final float f = r.right; - r.right = r.left; - r.left = f; - } - if (r.bottom < r.top) { - final float f = r.top; - r.top = r.bottom; - r.bottom = f; - } - } - - private void transformMatrix(Matrix m, View view) { - final float w = view.getWidth(); - final float h = view.getHeight(); - final boolean hasPivot = mHasPivot; - final float pX = hasPivot ? mPivotX : w / 2f; - final float pY = hasPivot ? mPivotY : h / 2f; - - final float rX = mRotationX; - final float rY = mRotationY; - final float rZ = mRotationZ; - if ((rX != 0) || (rY != 0) || (rZ != 0)) { - final Camera camera = mCamera; - camera.save(); - camera.rotateX(rX); - camera.rotateY(rY); - camera.rotateZ(-rZ); - camera.getMatrix(m); - camera.restore(); - m.preTranslate(-pX, -pY); - m.postTranslate(pX, pY); - } - - final float sX = mScaleX; - final float sY = mScaleY; - if ((sX != 1.0f) || (sY != 1.0f)) { - m.postScale(sX, sY); - final float sPX = -(pX / w) * ((sX * w) - w); - final float sPY = -(pY / h) * ((sY * h) - h); - m.postTranslate(sPX, sPY); - } - - m.postTranslate(mTranslationX, mTranslationY); - } - - @Override - protected void applyTransformation(float interpolatedTime, Transformation t) { - View view = mView.get(); - if (view != null) { - t.setAlpha(mAlpha); - transformMatrix(t.getMatrix(), view); - } - } -} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/AnimationCompat/AnimatorListenerAdapterProxy.java b/TMessagesProj/src/main/java/org/telegram/messenger/AnimationCompat/AnimatorListenerAdapterProxy.java deleted file mode 100644 index 4a5c55a17..000000000 --- a/TMessagesProj/src/main/java/org/telegram/messenger/AnimationCompat/AnimatorListenerAdapterProxy.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * This is the source code of Telegram for Android v. 3.x.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-2016. - */ - -package org.telegram.messenger.AnimationCompat; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; - -import org.telegram.messenger.Animation.Animator10; -import org.telegram.messenger.Animation.AnimatorListenerAdapter10; -import org.telegram.messenger.Animation.View10; - -public class AnimatorListenerAdapterProxy { - protected Object animatorListenerAdapter; - - public AnimatorListenerAdapterProxy() { - if (View10.NEED_PROXY) { - animatorListenerAdapter = new AnimatorListenerAdapter10() { - @Override - public void onAnimationCancel(Animator10 animation) { - AnimatorListenerAdapterProxy.this.onAnimationCancel(animation); - } - - @Override - public void onAnimationEnd(Animator10 animation) { - AnimatorListenerAdapterProxy.this.onAnimationEnd(animation); - } - - @Override - public void onAnimationRepeat(Animator10 animation) { - AnimatorListenerAdapterProxy.this.onAnimationRepeat(animation); - } - - @Override - public void onAnimationStart(Animator10 animation) { - AnimatorListenerAdapterProxy.this.onAnimationStart(animation); - } - - @Override - public void onAnimationPause(Animator10 animation) { - AnimatorListenerAdapterProxy.this.onAnimationPause(animation); - } - - @Override - public void onAnimationResume(Animator10 animation) { - AnimatorListenerAdapterProxy.this.onAnimationResume(animation); - } - }; - } else { - animatorListenerAdapter = new AnimatorListenerAdapter() { - @Override - public void onAnimationCancel(Animator animation) { - AnimatorListenerAdapterProxy.this.onAnimationCancel(animation); - } - - @Override - public void onAnimationEnd(Animator animation) { - AnimatorListenerAdapterProxy.this.onAnimationEnd(animation); - } - - @Override - public void onAnimationRepeat(Animator animation) { - AnimatorListenerAdapterProxy.this.onAnimationRepeat(animation); - } - - @Override - public void onAnimationStart(Animator animation) { - AnimatorListenerAdapterProxy.this.onAnimationStart(animation); - } - - @Override - public void onAnimationPause(Animator animation) { - AnimatorListenerAdapterProxy.this.onAnimationPause(animation); - } - - @Override - public void onAnimationResume(Animator animation) { - AnimatorListenerAdapterProxy.this.onAnimationResume(animation); - } - }; - } - } - - public void onAnimationCancel(Object animation) { - - } - - public void onAnimationEnd(Object animation) { - - } - - public void onAnimationRepeat(Object animation) { - - } - - public void onAnimationStart(Object animation) { - - } - - public void onAnimationPause(Object animation) { - - } - - public void onAnimationResume(Object animation) { - - } -} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/AnimationCompat/AnimatorSetProxy.java b/TMessagesProj/src/main/java/org/telegram/messenger/AnimationCompat/AnimatorSetProxy.java deleted file mode 100644 index e6cccb0a2..000000000 --- a/TMessagesProj/src/main/java/org/telegram/messenger/AnimationCompat/AnimatorSetProxy.java +++ /dev/null @@ -1,137 +0,0 @@ -/* - * This is the source code of Telegram for Android v. 3.x.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-2016. - */ - -package org.telegram.messenger.AnimationCompat; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.AnimatorSet; -import android.view.animation.Interpolator; - -import org.telegram.messenger.Animation.Animator10; -import org.telegram.messenger.Animation.AnimatorListenerAdapter10; -import org.telegram.messenger.Animation.AnimatorSet10; -import org.telegram.messenger.Animation.View10; - -import java.lang.reflect.Array; -import java.util.ArrayList; - -public class AnimatorSetProxy { - - private Object animatorSet; - - public static T[] copyOf(U[] original, int newLength, Class newType) { - return copyOfRange(original, 0, newLength, newType); - } - - @SuppressWarnings("unchecked") - public static T[] copyOfRange(U[] original, int start, int end, Class newType) { - if (start > end) { - throw new IllegalArgumentException(); - } - int originalLength = original.length; - if (start < 0 || start > originalLength) { - throw new ArrayIndexOutOfBoundsException(); - } - int resultLength = end - start; - int copyLength = Math.min(resultLength, originalLength - start); - T[] result = (T[]) Array.newInstance(newType.getComponentType(), resultLength); - System.arraycopy(original, start, result, 0, copyLength); - return result; - } - - public AnimatorSetProxy() { - if (View10.NEED_PROXY) { - animatorSet = new AnimatorSet10(); - } else { - animatorSet = new AnimatorSet(); - } - } - - @SuppressWarnings("unchecked") - public void playTogether(Object... items) { - if (View10.NEED_PROXY) { - Animator10[] animators = copyOf(items, items.length, Animator10[].class); - ((AnimatorSet10) animatorSet).playTogether(animators); - } else { - Animator[] animators = copyOf(items, items.length, Animator[].class); - ((AnimatorSet) animatorSet).playTogether(animators); - } - } - - public void playTogether(ArrayList items) { - if (View10.NEED_PROXY) { - ArrayList animators = new ArrayList<>(); - for (Object obj : items) { - animators.add((Animator10)obj); - } - ((AnimatorSet10) animatorSet).playTogether(animators); - } else { - ArrayList animators = new ArrayList<>(); - for (Object obj : items) { - animators.add((Animator)obj); - } - ((AnimatorSet) animatorSet).playTogether(animators); - } - } - - public AnimatorSetProxy setDuration(long duration) { - if (View10.NEED_PROXY) { - ((AnimatorSet10) animatorSet).setDuration(duration); - } else { - ((AnimatorSet) animatorSet).setDuration(duration); - } - return this; - } - - public AnimatorSetProxy setStartDelay(long delay) { - if (View10.NEED_PROXY) { - ((AnimatorSet10) animatorSet).setStartDelay(delay); - } else { - ((AnimatorSet) animatorSet).setStartDelay(delay); - } - return this; - } - - public void start() { - if (View10.NEED_PROXY) { - ((AnimatorSet10) animatorSet).start(); - } else { - ((AnimatorSet) animatorSet).start(); - } - } - - public void cancel() { - if (View10.NEED_PROXY) { - ((AnimatorSet10) animatorSet).cancel(); - } else { - ((AnimatorSet) animatorSet).cancel(); - } - } - - public void addListener(AnimatorListenerAdapterProxy listener) { - if (View10.NEED_PROXY) { - ((AnimatorSet10) animatorSet).addListener((AnimatorListenerAdapter10) listener.animatorListenerAdapter); - } else { - ((AnimatorSet) animatorSet).addListener((AnimatorListenerAdapter) listener.animatorListenerAdapter); - } - } - - public void setInterpolator(Interpolator interpolator) { - if (View10.NEED_PROXY) { - ((AnimatorSet10) animatorSet).setInterpolator(interpolator); - } else { - ((AnimatorSet) animatorSet).setInterpolator(interpolator); - } - } - - @Override - public boolean equals(Object o) { - return animatorSet == o; - } -} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/AnimationCompat/ObjectAnimatorProxy.java b/TMessagesProj/src/main/java/org/telegram/messenger/AnimationCompat/ObjectAnimatorProxy.java deleted file mode 100644 index 8bf69db6a..000000000 --- a/TMessagesProj/src/main/java/org/telegram/messenger/AnimationCompat/ObjectAnimatorProxy.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * This is the source code of Telegram for Android v. 3.x.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-2016. - */ - -package org.telegram.messenger.AnimationCompat; - -import android.animation.AnimatorListenerAdapter; -import android.animation.ObjectAnimator; -import android.view.animation.Interpolator; - -import org.telegram.messenger.Animation.AnimatorListenerAdapter10; -import org.telegram.messenger.Animation.ObjectAnimator10; -import org.telegram.messenger.Animation.View10; - -public class ObjectAnimatorProxy { - - private Object objectAnimator; - - public ObjectAnimatorProxy(Object animator) { - objectAnimator = animator; - } - - public static Object ofFloat(Object target, String propertyName, float... values) { - if (View10.NEED_PROXY) { - return ObjectAnimator10.ofFloat(target, propertyName, values); - } else { - return ObjectAnimator.ofFloat(target, propertyName, values); - } - } - - public static Object ofInt(Object target, String propertyName, int... values) { - if (View10.NEED_PROXY) { - return ObjectAnimator10.ofInt(target, propertyName, values); - } else { - return ObjectAnimator.ofInt(target, propertyName, values); - } - } - - public static ObjectAnimatorProxy ofFloatProxy(Object target, String propertyName, float... values) { - if (View10.NEED_PROXY) { - return new ObjectAnimatorProxy(ObjectAnimator10.ofFloat(target, propertyName, values)); - } else { - return new ObjectAnimatorProxy(ObjectAnimator.ofFloat(target, propertyName, values)); - } - } - - public static ObjectAnimatorProxy ofIntProxy(Object target, String propertyName, int... values) { - if (View10.NEED_PROXY) { - return new ObjectAnimatorProxy(ObjectAnimator10.ofInt(target, propertyName, values)); - } else { - return new ObjectAnimatorProxy(ObjectAnimator.ofInt(target, propertyName, values)); - } - } - - public ObjectAnimatorProxy setDuration(long duration) { - if (View10.NEED_PROXY) { - ((ObjectAnimator10) objectAnimator).setDuration(duration); - } else { - ((ObjectAnimator) objectAnimator).setDuration(duration); - } - return this; - } - - public void setInterpolator(Interpolator value) { - if (View10.NEED_PROXY) { - ((ObjectAnimator10) objectAnimator).setInterpolator(value); - } else { - ((ObjectAnimator) objectAnimator).setInterpolator(value); - } - } - - public ObjectAnimatorProxy start() { - if (View10.NEED_PROXY) { - ((ObjectAnimator10) objectAnimator).start(); - } else { - ((ObjectAnimator) objectAnimator).start(); - } - return this; - } - - public void setAutoCancel(boolean cancel) { - if (View10.NEED_PROXY) { - ((ObjectAnimator10) objectAnimator).setAutoCancel(cancel); - } else { - ((ObjectAnimator) objectAnimator).setAutoCancel(cancel); - } - } - - public boolean isRunning() { - if (View10.NEED_PROXY) { - return ((ObjectAnimator10) objectAnimator).isRunning(); - } else { - return ((ObjectAnimator) objectAnimator).isRunning(); - } - } - - public void end() { - if (View10.NEED_PROXY) { - ((ObjectAnimator10) objectAnimator).end(); - } else { - ((ObjectAnimator) objectAnimator).end(); - } - } - - public void cancel() { - if (View10.NEED_PROXY) { - ((ObjectAnimator10) objectAnimator).cancel(); - } else { - ((ObjectAnimator) objectAnimator).cancel(); - } - } - - public ObjectAnimatorProxy addListener(AnimatorListenerAdapterProxy listener) { - if (View10.NEED_PROXY) { - ((ObjectAnimator10) objectAnimator).addListener((AnimatorListenerAdapter10) listener.animatorListenerAdapter); - } else { - ((ObjectAnimator) objectAnimator).addListener((AnimatorListenerAdapter) listener.animatorListenerAdapter); - } - return this; - } - - @Override - public boolean equals(Object o) { - return objectAnimator == o; - } -} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/AnimationCompat/ViewProxy.java b/TMessagesProj/src/main/java/org/telegram/messenger/AnimationCompat/ViewProxy.java deleted file mode 100644 index 50a2690fa..000000000 --- a/TMessagesProj/src/main/java/org/telegram/messenger/AnimationCompat/ViewProxy.java +++ /dev/null @@ -1,248 +0,0 @@ -/* - * This is the source code of Telegram for Android v. 3.x.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-2016. - */ - -package org.telegram.messenger.AnimationCompat; - -import android.view.View; - -import org.telegram.messenger.Animation.View10; - -public class ViewProxy { - - public static float getAlpha(View view) { - if (View10.NEED_PROXY) { - return View10.wrap(view).getAlpha(); - } else { - return view.getAlpha(); - } - } - - public static void setAlpha(View view, float alpha) { - if (View10.NEED_PROXY) { - View10.wrap(view).setAlpha(alpha); - } else { - view.setAlpha(alpha); - } - } - - public static float getPivotX(View view) { - if (View10.NEED_PROXY) { - return View10.wrap(view).getPivotX(); - } else { - return view.getPivotX(); - } - } - - public static void setPivotX(View view, float pivotX) { - if (View10.NEED_PROXY) { - View10.wrap(view).setPivotX(pivotX); - } else { - view.setPivotX(pivotX); - } - } - - public static float getPivotY(View view) { - if (View10.NEED_PROXY) { - return View10.wrap(view).getPivotY(); - } else { - return view.getPivotY(); - } - } - - public static void setPivotY(View view, float pivotY) { - if (View10.NEED_PROXY) { - View10.wrap(view).setPivotY(pivotY); - } else { - view.setPivotY(pivotY); - } - } - - public static float getRotation(View view) { - if (View10.NEED_PROXY) { - return View10.wrap(view).getRotation(); - } else { - return view.getRotation(); - } - } - - public static void setRotation(View view, float rotation) { - if (View10.NEED_PROXY) { - View10.wrap(view).setRotation(rotation); - } else { - view.setRotation(rotation); - } - } - - public static float getRotationX(View view) { - if (View10.NEED_PROXY) { - return View10.wrap(view).getRotationX(); - } else { - return view.getRotationX(); - } - } - - public void setRotationX(View view, float rotationX) { - if (View10.NEED_PROXY) { - View10.wrap(view).setRotationX(rotationX); - } else { - view.setRotationX(rotationX); - } - } - - public static float getRotationY(View view) { - if (View10.NEED_PROXY) { - return View10.wrap(view).getRotationY(); - } else { - return view.getRotationY(); - } - } - - public void setRotationY(View view, float rotationY) { - if (View10.NEED_PROXY) { - View10.wrap(view).setRotationY(rotationY); - } else { - view.setRotationY(rotationY); - } - } - - public static float getScaleX(View view) { - if (View10.NEED_PROXY) { - return View10.wrap(view).getScaleX(); - } else { - return view.getScaleX(); - } - } - - public static void setScaleX(View view, float scaleX) { - if (View10.NEED_PROXY) { - View10.wrap(view).setScaleX(scaleX); - } else { - view.setScaleX(scaleX); - } - } - - public static float getScaleY(View view) { - if (View10.NEED_PROXY) { - return View10.wrap(view).getScaleY(); - } else { - return view.getScaleY(); - } - } - - public static void setScaleY(View view, float scaleY) { - if (View10.NEED_PROXY) { - View10.wrap(view).setScaleY(scaleY); - } else { - view.setScaleY(scaleY); - } - } - - public static int getScrollX(View view) { - if (View10.NEED_PROXY) { - return View10.wrap(view).getScrollX(); - } else { - return view.getScrollX(); - } - } - - public static void setScrollX(View view, int value) { - if (View10.NEED_PROXY) { - View10.wrap(view).setScrollX(value); - } else { - view.setScrollX(value); - } - } - - public static int getScrollY(View view) { - if (View10.NEED_PROXY) { - return View10.wrap(view).getScrollY(); - } else { - return view.getScrollY(); - } - } - - public static void setScrollY(View view, int value) { - if (View10.NEED_PROXY) { - View10.wrap(view).setScrollY(value); - } else { - view.setScrollY(value); - } - } - - public static float getTranslationX(View view) { - if (View10.NEED_PROXY) { - return View10.wrap(view).getTranslationX(); - } else { - return view.getTranslationX(); - } - } - - public static void setTranslationX(View view, float translationX) { - if (View10.NEED_PROXY) { - View10.wrap(view).setTranslationX(translationX); - } else { - view.setTranslationX(translationX); - } - } - - public static float getTranslationY(View view) { - if (View10.NEED_PROXY) { - return View10.wrap(view).getTranslationY(); - } else { - return view.getTranslationY(); - } - } - - public static void setTranslationY(View view, float translationY) { - if (View10.NEED_PROXY) { - View10.wrap(view).setTranslationY(translationY); - } else { - view.setTranslationY(translationY); - } - } - - public static float getX(View view) { - if (View10.NEED_PROXY) { - return View10.wrap(view).getX(); - } else { - return view.getX(); - } - } - - public static void setX(View view, float x) { - if (View10.NEED_PROXY) { - View10.wrap(view).setX(x); - } else { - view.setX(x); - } - } - - public static float getY(View view) { - if (View10.NEED_PROXY) { - return View10.wrap(view).getY(); - } else { - return view.getY(); - } - } - - public static void setY(View view, float y) { - if (View10.NEED_PROXY) { - View10.wrap(view).setY(y); - } else { - view.setY(y); - } - } - - public static Object wrap(View view) { - if (View10.NEED_PROXY) { - return View10.wrap(view); - } else { - return view; - } - } -} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/AnimatorListenerAdapterProxy.java b/TMessagesProj/src/main/java/org/telegram/messenger/AnimatorListenerAdapterProxy.java new file mode 100644 index 000000000..855df9b85 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/AnimatorListenerAdapterProxy.java @@ -0,0 +1,45 @@ +/* + * This is the source code of Telegram for Android v. 3.x.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-2016. + */ + +package org.telegram.messenger; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; + +public class AnimatorListenerAdapterProxy extends AnimatorListenerAdapter { + + @Override + public void onAnimationCancel(Animator animator) { + + } + + @Override + public void onAnimationEnd(Animator animator) { + + } + + @Override + public void onAnimationRepeat(Animator animator) { + + } + + @Override + public void onAnimationStart(Animator animator) { + + } + + @Override + public void onAnimationPause(Animator animator) { + + } + + @Override + public void onAnimationResume(Animator animator) { + + } +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/ApplicationLoader.java b/TMessagesProj/src/main/java/org/telegram/messenger/ApplicationLoader.java index ffa92478b..b98ccbf85 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/ApplicationLoader.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/ApplicationLoader.java @@ -287,40 +287,40 @@ public class ApplicationLoader extends Application { public void onCreate() { super.onCreate(); - if (Build.VERSION.SDK_INT < 11) { - java.lang.System.setProperty("java.net.preferIPv4Stack", "true"); - java.lang.System.setProperty("java.net.preferIPv6Addresses", "false"); - } - applicationContext = getApplicationContext(); NativeLoader.initNativeLibs(ApplicationLoader.applicationContext); ConnectionsManager.native_setJava(Build.VERSION.SDK_INT == 14 || Build.VERSION.SDK_INT == 15); - - if (Build.VERSION.SDK_INT >= 14) { - new ForegroundDetector(this); - } + new ForegroundDetector(this); applicationHandler = new Handler(applicationContext.getMainLooper()); startPushService(); } + /*public static void sendRegIdToBackend(final String token) { + Utilities.stageQueue.postRunnable(new Runnable() { + @Override + public void run() { + UserConfig.pushString = token; + UserConfig.registeredForPush = false; + UserConfig.saveConfig(false); + if (UserConfig.getClientUserId() != 0) { + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + MessagesController.getInstance().registerForPush(token); + } + }); + } + } + }); + }*/ + public static void startPushService() { SharedPreferences preferences = applicationContext.getSharedPreferences("Notifications", MODE_PRIVATE); if (preferences.getBoolean("pushService", true)) { applicationContext.startService(new Intent(applicationContext, NotificationsService.class)); - - if (android.os.Build.VERSION.SDK_INT >= 19) { -// Calendar cal = Calendar.getInstance(); -// PendingIntent pintent = PendingIntent.getService(applicationContext, 0, new Intent(applicationContext, NotificationsService.class), 0); -// AlarmManager alarm = (AlarmManager) applicationContext.getSystemService(Context.ALARM_SERVICE); -// alarm.setRepeating(AlarmManager.RTC_WAKEUP, cal.getTimeInMillis(), 30000, pintent); - - PendingIntent pintent = PendingIntent.getService(applicationContext, 0, new Intent(applicationContext, NotificationsService.class), 0); - AlarmManager alarm = (AlarmManager)applicationContext.getSystemService(Context.ALARM_SERVICE); - alarm.cancel(pintent); - } } else { stopPushService(); } @@ -357,8 +357,8 @@ public class ApplicationLoader extends Application { } //if (UserConfig.pushString == null || UserConfig.pushString.length() == 0) { - Intent intent = new Intent(applicationContext, GcmRegistrationIntentService.class); - startService(intent); + Intent intent = new Intent(applicationContext, GcmRegistrationIntentService.class); + startService(intent); //} else { // FileLog.d("tmessages", "GCM regId = " + UserConfig.pushString); //} @@ -369,6 +369,33 @@ public class ApplicationLoader extends Application { }, 1000); } + /*private void initPlayServices() { + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + if (checkPlayServices()) { + if (UserConfig.pushString != null && UserConfig.pushString.length() != 0) { + FileLog.d("tmessages", "GCM regId = " + UserConfig.pushString); + } else { + FileLog.d("tmessages", "GCM Registration not found."); + } + try { + if (!FirebaseApp.getApps(ApplicationLoader.applicationContext).isEmpty()) { + String token = FirebaseInstanceId.getInstance().getToken(); + if (token != null) { + sendRegIdToBackend(token); + } + } + } catch (Throwable e) { + FileLog.e("tmessages", e); + } + } else { + FileLog.d("tmessages", "No valid Google Play Services APK found."); + } + } + }, 2000); + }*/ + private boolean checkPlayServices() { try { int resultCode = GooglePlayServicesUtil.isGooglePlayServicesAvailable(this); diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/AutoMessageReplyReceiver.java b/TMessagesProj/src/main/java/org/telegram/messenger/AutoMessageReplyReceiver.java index 1635bd37b..1f45a7273 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/AutoMessageReplyReceiver.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/AutoMessageReplyReceiver.java @@ -32,7 +32,7 @@ public class AutoMessageReplyReceiver extends BroadcastReceiver { if (dialog_id == 0 || max_id == 0) { return; } - SendMessagesHelper.getInstance().sendMessage(text.toString(), dialog_id, null, null, true, false, null, null, null); + SendMessagesHelper.getInstance().sendMessage(text.toString(), dialog_id, null, null, true, null, null, null); MessagesController.getInstance().markDialogAsRead(dialog_id, max_id, max_id, 0, true, false); } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/Bitmaps.java b/TMessagesProj/src/main/java/org/telegram/messenger/Bitmaps.java index fe3724626..77778f8d2 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/Bitmaps.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/Bitmaps.java @@ -110,7 +110,7 @@ public class Bitmaps { public static Bitmap createBitmap(int width, int height, Bitmap.Config config) { Bitmap bitmap; - if (Build.VERSION.SDK_INT >= 14 && Build.VERSION.SDK_INT < 21) { + if (Build.VERSION.SDK_INT < 21) { BitmapFactory.Options options = new BitmapFactory.Options(); options.inDither = true; options.inPreferredConfig = config; @@ -212,9 +212,7 @@ public class Bitmaps { } } bitmap.setDensity(source.getDensity()); - if (Build.VERSION.SDK_INT >= 12) { - bitmap.setHasAlpha(source.hasAlpha()); - } + bitmap.setHasAlpha(source.hasAlpha()); if (Build.VERSION.SDK_INT >= 19) { bitmap.setPremultiplied(source.isPremultiplied()); } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/BuildVars.java b/TMessagesProj/src/main/java/org/telegram/messenger/BuildVars.java index ac30c6286..f872d49bd 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/BuildVars.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/BuildVars.java @@ -10,8 +10,8 @@ package org.telegram.messenger; public class BuildVars { public static boolean DEBUG_VERSION = false; - public static int BUILD_VERSION = 803; - public static String BUILD_VERSION_STRING = "3.9"; + public static int BUILD_VERSION = 821; + public static String BUILD_VERSION_STRING = "3.10"; public static int APP_ID = 0; //obtain your own APP_ID at https://core.telegram.org/api/obtaining_api_id public static String APP_HASH = ""; //obtain your own APP_HASH at https://core.telegram.org/api/obtaining_api_id public static String HOCKEY_APP_HASH = "your-hockeyapp-api-key-here"; diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/ContactsController.java b/TMessagesProj/src/main/java/org/telegram/messenger/ContactsController.java index 376ba2ede..3e8b51257 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/ContactsController.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/ContactsController.java @@ -71,17 +71,17 @@ public class ContactsController { } private String[] projectionPhones = { - ContactsContract.CommonDataKinds.Phone.CONTACT_ID, - ContactsContract.CommonDataKinds.Phone.NUMBER, - ContactsContract.CommonDataKinds.Phone.TYPE, - ContactsContract.CommonDataKinds.Phone.LABEL + ContactsContract.CommonDataKinds.Phone.CONTACT_ID, + ContactsContract.CommonDataKinds.Phone.NUMBER, + ContactsContract.CommonDataKinds.Phone.TYPE, + ContactsContract.CommonDataKinds.Phone.LABEL }; private String[] projectionNames = { - ContactsContract.CommonDataKinds.StructuredName.CONTACT_ID, - ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME, - ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME, - ContactsContract.Data.DISPLAY_NAME, - ContactsContract.CommonDataKinds.StructuredName.MIDDLE_NAME + ContactsContract.CommonDataKinds.StructuredName.CONTACT_ID, + ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME, + ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME, + ContactsContract.Data.DISPLAY_NAME, + ContactsContract.CommonDataKinds.StructuredName.MIDDLE_NAME }; public HashMap contactsBook = new HashMap<>(); @@ -98,7 +98,10 @@ public class ContactsController { public HashMap contactsByPhone = new HashMap<>(); + private int completedRequestsCount; + private static volatile ContactsController Instance = null; + public static ContactsController getInstance() { ContactsController localInstance = Instance; if (localInstance == null) { @@ -163,6 +166,12 @@ public class ContactsController { deleteAccountTTL = 0; loadingLastSeenInfo = 0; loadingGroupInfo = 0; + Utilities.globalQueue.postRunnable(new Runnable() { + @Override + public void run() { + completedRequestsCount = 0; + } + }); privacyRules = null; } @@ -170,13 +179,9 @@ public class ContactsController { SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE); inviteText = preferences.getString("invitetext", null); int time = preferences.getInt("invitetexttime", 0); - if (!updatingInviteText && (inviteText == null || time + 86400 < (int)(System.currentTimeMillis() / 1000))) { + 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.getLocaleStringIso639(); - if (req.lang_code.length() == 0) { - req.lang_code = "en"; - } ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { @Override public void run(TLObject response, TLRPC.TL_error error) { @@ -275,12 +280,21 @@ public class ContactsController { public void run() { if (checkContactsInternal()) { FileLog.e("tmessages", "detected contacts change"); - ContactsController.getInstance().performSyncPhoneBook(ContactsController.getInstance().getContactsCopy(ContactsController.getInstance().contactsBook), true, false, true); + ContactsController.getInstance().performSyncPhoneBook(ContactsController.getInstance().getContactsCopy(ContactsController.getInstance().contactsBook), true, false, true, false); } } }); } + public void forceImportContacts() { + Utilities.globalQueue.postRunnable(new Runnable() { + @Override + public void run() { + ContactsController.getInstance().performSyncPhoneBook(new HashMap(), true, true, true, true); + } + }); + } + private boolean checkContactsInternal() { boolean reload = false; try { @@ -444,7 +458,7 @@ public class ContactsController { } try { - pCur = cr.query(ContactsContract.RawContacts.CONTENT_URI, new String[] { "display_name", ContactsContract.RawContacts.SYNC1, ContactsContract.RawContacts.CONTACT_ID }, ContactsContract.RawContacts.ACCOUNT_TYPE + " = " + "'com.whatsapp'", null, null); + pCur = cr.query(ContactsContract.RawContacts.CONTENT_URI, new String[]{"display_name", ContactsContract.RawContacts.SYNC1, ContactsContract.RawContacts.CONTACT_ID}, ContactsContract.RawContacts.ACCOUNT_TYPE + " = " + "'com.whatsapp'", null, null); if (pCur != null) { while ((pCur.moveToNext())) { String phone = pCur.getString(1); @@ -466,8 +480,8 @@ public class ContactsController { } String name = pCur.getString(0); - if (name == null || name.length() == 0) { - name = PhoneFormat.getInstance().format(phone); + if (TextUtils.isEmpty(name)) { + continue; } Contact contact = new Contact(); @@ -528,7 +542,7 @@ public class ContactsController { return ret; } - public void performSyncPhoneBook(final HashMap contactHashMap, final boolean requ, final boolean first, final boolean schedule) { + protected void performSyncPhoneBook(final HashMap contactHashMap, final boolean request, final boolean first, final boolean schedule, final boolean force) { if (!first && !contactsBookLoaded) { return; } @@ -560,21 +574,11 @@ public class ContactsController { } }*/ - boolean request = requ; - if (request && first) { - if (UserConfig.importHash != null && UserConfig.importHash.length() != 0 || UserConfig.contactsVersion != 1) { - UserConfig.importHash = ""; - UserConfig.contactsVersion = 1; - UserConfig.saveConfig(false); - request = false; - } - } - HashMap contactShortHashMap = new HashMap<>(); for (HashMap.Entry entry : contactHashMap.entrySet()) { Contact c = entry.getValue(); - for (String sphone : c.shortPhones) { - contactShortHashMap.put(sphone, c); + for (int a = 0; a < c.shortPhones.size(); a++) { + contactShortHashMap.put(c.shortPhones.get(a), c); } } @@ -593,8 +597,8 @@ public class ContactsController { Contact value = pair.getValue(); Contact existing = contactHashMap.get(id); if (existing == null) { - for (String s : value.shortPhones) { - Contact c = contactShortHashMap.get(s); + for (int a = 0; a < value.shortPhones.size(); a++) { + Contact c = contactShortHashMap.get(value.shortPhones.get(a)); if (c != null) { existing = c; id = existing.id; @@ -603,7 +607,7 @@ public class ContactsController { } } - boolean nameChanged = existing != null && (value.first_name != null && value.first_name.length() != 0 && !existing.first_name.equals(value.first_name) || value.last_name != null && existing.last_name != null && !existing.last_name.equals(value.last_name)); + boolean nameChanged = existing != null && (TextUtils.isEmpty(value.first_name) && !existing.first_name.equals(value.first_name) || !TextUtils.isEmpty(value.last_name) && !existing.last_name.equals(value.last_name)); if (existing == null || nameChanged) { for (int a = 0; a < value.phones.size(); a++) { String sphone = value.shortPhones.get(a); @@ -625,6 +629,7 @@ public class ContactsController { TLRPC.TL_inputPhoneContact imp = new TLRPC.TL_inputPhoneContact(); imp.client_id = value.id; + imp.client_id |= ((long) a) << 32; imp.first_name = value.first_name; imp.last_name = value.last_name; imp.phone = value.phones.get(a); @@ -644,13 +649,14 @@ public class ContactsController { TLRPC.TL_contact contact = contactsByPhone.get(sphone); if (contact != null) { TLRPC.User user = MessagesController.getInstance().getUser(contact.user_id); - if (user == null || user.first_name != null && user.first_name.length() != 0 || user.last_name != null && user.last_name.length() != 0) { + if (user == null || !TextUtils.isEmpty(user.first_name) || !TextUtils.isEmpty(user.last_name) || TextUtils.isEmpty(value.first_name) && TextUtils.isEmpty(value.last_name)) { continue; } } TLRPC.TL_inputPhoneContact imp = new TLRPC.TL_inputPhoneContact(); imp.client_id = value.id; + imp.client_id |= ((long) a) << 32; imp.first_name = value.first_name; imp.last_name = value.last_name; imp.phone = value.phones.get(a); @@ -697,7 +703,8 @@ public class ContactsController { try { final HashMap contactsPhonesShort = new HashMap<>(); - for (TLRPC.TL_contact value : contacts) { + for (int a = 0; a < contacts.size(); a++) { + TLRPC.TL_contact value = contacts.get(a); TLRPC.User user = MessagesController.getInstance().getUser(value.user_id); if (user == null || user.phone == null || user.phone.length() == 0) { continue; @@ -739,16 +746,19 @@ public class ContactsController { Contact value = pair.getValue(); int id = pair.getKey(); for (int a = 0; a < value.phones.size(); a++) { - String phone = value.shortPhones.get(a); - TLRPC.TL_contact contact = contactsByPhone.get(phone); - if (contact != null) { - TLRPC.User user = MessagesController.getInstance().getUser(contact.user_id); - if (user == null || user.first_name != null && user.first_name.length() != 0 || user.last_name != null && user.last_name.length() != 0) { - continue; + if (!force) { + String phone = value.shortPhones.get(a); + TLRPC.TL_contact contact = contactsByPhone.get(phone); + if (contact != null) { + TLRPC.User user = MessagesController.getInstance().getUser(contact.user_id); + if (user == null || !TextUtils.isEmpty(user.first_name) || !TextUtils.isEmpty(user.last_name) || TextUtils.isEmpty(value.first_name) && TextUtils.isEmpty(value.last_name)) { + continue; + } } } TLRPC.TL_inputPhoneContact imp = new TLRPC.TL_inputPhoneContact(); imp.client_id = id; + imp.client_id |= ((long) a) << 32; imp.first_name = value.first_name; imp.last_name = value.last_name; imp.phone = value.phones.get(a); @@ -767,23 +777,34 @@ public class ContactsController { FileLog.e("tmessages", "add contact " + contact.first_name + " " + contact.last_name + " " + contact.phone); } }*/ - final int count = (int)Math.ceil(toImport.size() / 500.0f); + + final HashMap contactsMapToSave = new HashMap<>(contactsMap); + completedRequestsCount = 0; + final int count = (int) Math.ceil(toImport.size() / 500.0f); for (int a = 0; a < count; a++) { ArrayList finalToImport = new ArrayList<>(); finalToImport.addAll(toImport.subList(a * 500, Math.min((a + 1) * 500, toImport.size()))); TLRPC.TL_contacts_importContacts req = new TLRPC.TL_contacts_importContacts(); req.contacts = finalToImport; req.replace = false; - final boolean isLastQuery = a == count - 1; ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { @Override public void run(TLObject response, TLRPC.TL_error error) { + completedRequestsCount++; if (error == null) { FileLog.e("tmessages", "contacts imported"); - if (isLastQuery && !contactsMap.isEmpty()) { - MessagesStorage.getInstance().putCachedPhoneBook(contactsMap); + TLRPC.TL_contacts_importedContacts res = (TLRPC.TL_contacts_importedContacts) response; + if (!res.retry_contacts.isEmpty()) { + for (int a = 0; a < res.retry_contacts.size(); a++) { + long id = res.retry_contacts.get(a); + contactsMapToSave.remove((int) id); + } } - TLRPC.TL_contacts_importedContacts res = (TLRPC.TL_contacts_importedContacts)response; + + if (completedRequestsCount == count && !contactsMapToSave.isEmpty()) { + MessagesStorage.getInstance().putCachedPhoneBook(contactsMapToSave); + } + /*if (BuildVars.DEBUG_VERSION) { for (TLRPC.User user : res.users) { FileLog.e("tmessages", "received user " + user.first_name + " " + user.last_name + " " + user.phone); @@ -791,16 +812,16 @@ public class ContactsController { }*/ MessagesStorage.getInstance().putUsersAndChats(res.users, null, true, true); ArrayList cArr = new ArrayList<>(); - for (TLRPC.TL_importedContact c : res.imported) { + for (int a = 0; a < res.imported.size(); a++) { TLRPC.TL_contact contact = new TLRPC.TL_contact(); - contact.user_id = c.user_id; + contact.user_id = res.imported.get(a).user_id; cArr.add(contact); } processLoadedContacts(cArr, res.users, 2); } else { FileLog.e("tmessages", "import contacts error " + error.text); } - if (isLastQuery) { + if (completedRequestsCount == count) { Utilities.stageQueue.postRunnable(new Runnable() { @Override public void run() { @@ -836,13 +857,13 @@ public class ContactsController { applyContactsUpdates(delayedContactsUpdate, null, null, null); delayedContactsUpdate.clear(); } - } - }); - AndroidUtilities.runOnUIThread(new Runnable() { - @Override - public void run() { - updateUnregisteredContacts(contacts); - NotificationCenter.getInstance().postNotificationName(NotificationCenter.contactsDidLoaded); + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + updateUnregisteredContacts(contacts); + NotificationCenter.getInstance().postNotificationName(NotificationCenter.contactsDidLoaded); + } + }); } }); } @@ -892,7 +913,7 @@ public class ContactsController { @Override public void run(TLObject response, TLRPC.TL_error error) { if (error == null) { - TLRPC.contacts_Contacts res = (TLRPC.contacts_Contacts)response; + TLRPC.contacts_Contacts res = (TLRPC.contacts_Contacts) response; if (res instanceof TLRPC.TL_contacts_contactsNotModified) { contactsLoaded = true; if (!delayedContactsUpdate.isEmpty() && contactsBookLoaded) { @@ -940,11 +961,10 @@ public class ContactsController { contactsArr.addAll(contacts); } - for (TLRPC.TL_contact contact : contactsArr) { - TLRPC.User user = MessagesController.getInstance().getUser(contact.user_id); + for (int a = 0; a < contactsArr.size(); a++) { + TLRPC.User user = MessagesController.getInstance().getUser(contactsArr.get(a).user_id); if (user != null) { usersDict.put(user.id, user); - //if (BuildVars.DEBUG_VERSION) { // FileLog.e("tmessages", "loaded user contact " + user.first_name + " " + user.last_name + " " + user.phone); //} @@ -955,7 +975,7 @@ public class ContactsController { @Override public void run() { FileLog.e("tmessages", "done loading contacts"); - if (from == 1 && (contactsArr.isEmpty() || UserConfig.lastContactsSyncTime < (int) (System.currentTimeMillis() / 1000) - 24 * 60 * 60)) { + if (from == 1 && (contactsArr.isEmpty() || Math.abs(System.currentTimeMillis() / 1000 - UserConfig.lastContactsSyncTime) >= 24 * 60 * 60)) { loadContacts(false, true); return; } @@ -1021,7 +1041,8 @@ public class ContactsController { final HashMap contactsByPhonesDictFinal = contactsByPhonesDict; - for (TLRPC.TL_contact value : contactsArr) { + for (int a = 0; a < contactsArr.size(); a++) { + TLRPC.TL_contact value = contactsArr.get(a); TLRPC.User user = usersDict.get(value.user_id); if (user == null) { continue; @@ -1354,7 +1375,8 @@ public class ContactsController { if (newC == null || contactsTD == null) { newC = new ArrayList<>(); contactsTD = new ArrayList<>(); - for (Integer uid : ids) { + for (int a = 0; a < ids.size(); a++) { + Integer uid = ids.get(a); if (uid > 0) { TLRPC.TL_contact contact = new TLRPC.TL_contact(); contact.user_id = uid; @@ -1370,7 +1392,8 @@ public class ContactsController { StringBuilder toDelete = new StringBuilder(); boolean reloadContacts = false; - for (TLRPC.TL_contact newContact : newC) { + for (int a = 0; a < newC.size(); a++) { + TLRPC.TL_contact newContact = newC.get(a); TLRPC.User user = null; if (userDict != null) { user = userDict.get(newContact.user_id); @@ -1398,7 +1421,8 @@ public class ContactsController { toAdd.append(user.phone); } - for (final Integer uid : contactsTD) { + for (int a = 0; a < contactsTD.size(); a++) { + final Integer uid = contactsTD.get(a); Utilities.phoneBookQueue.postRunnable(new Runnable() { @Override public void run() { @@ -1452,13 +1476,15 @@ public class ContactsController { AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { - for (TLRPC.TL_contact contact : newContacts) { + for (int a = 0; a < newContacts.size(); a++) { + TLRPC.TL_contact contact = newContacts.get(a); if (contactsDict.get(contact.user_id) == null) { contacts.add(contact); contactsDict.put(contact.user_id, contact); } } - for (Integer uid : contactsToDelete) { + for (int a = 0; a < contactsToDelete.size(); a++) { + Integer uid = contactsToDelete.get(a); TLRPC.TL_contact contact = contactsDict.get(uid); if (contact != null) { contacts.remove(contact); @@ -1469,7 +1495,7 @@ public class ContactsController { updateUnregisteredContacts(contacts); performWriteContactsToPhoneBook(); } - performSyncPhoneBook(getContactsCopy(contactsBook), false, false, false); + performSyncPhoneBook(getContactsCopy(contactsBook), false, false, false, false); buildContactsSectionsArrays(!newContacts.isEmpty()); NotificationCenter.getInstance().postNotificationName(NotificationCenter.contactsDidLoaded); } @@ -1643,7 +1669,7 @@ public class ContactsController { if (error != null) { return; } - final TLRPC.TL_contacts_importedContacts res = (TLRPC.TL_contacts_importedContacts)response; + final TLRPC.TL_contacts_importedContacts res = (TLRPC.TL_contacts_importedContacts) response; MessagesStorage.getInstance().putUsersAndChats(res.users, null, true, true); /*if (BuildVars.DEBUG_VERSION) { diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/DialogObject.java b/TMessagesProj/src/main/java/org/telegram/messenger/DialogObject.java new file mode 100644 index 000000000..a31ed060f --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/DialogObject.java @@ -0,0 +1,18 @@ +/* + * This is the source code of Telegram for Android v. 3.x.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-2016. + */ + +package org.telegram.messenger; + +import org.telegram.tgnet.TLRPC; + +public class DialogObject { + + public static boolean isChannel(TLRPC.TL_dialog dialog) { + return dialog != null && (dialog.flags & 1) != 0; + } +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/GcmInstanceIDListenerService.java b/TMessagesProj/src/main/java/org/telegram/messenger/GcmInstanceIDListenerService.java old mode 100644 new mode 100755 diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/GcmPushListenerService.java b/TMessagesProj/src/main/java/org/telegram/messenger/GcmPushListenerService.java old mode 100644 new mode 100755 diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/GcmRegistrationIntentService.java b/TMessagesProj/src/main/java/org/telegram/messenger/GcmRegistrationIntentService.java old mode 100644 new mode 100755 diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/ImageLoader.java b/TMessagesProj/src/main/java/org/telegram/messenger/ImageLoader.java index 7f38c91a9..fe2b2e137 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/ImageLoader.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/ImageLoader.java @@ -79,7 +79,6 @@ public class ImageLoader { private HashMap retryHttpsTasks = new HashMap<>(); private int currentHttpFileLoadTasksCount = 0; - public VMRuntimeHack runtimeHack = null; private String ignoreRemoval = null; private volatile long lastCacheOutTime = 0; @@ -631,7 +630,7 @@ public class ImageLoader { BitmapFactory.Options opts = new BitmapFactory.Options(); opts.inSampleSize = 1; - if (Build.VERSION.SDK_INT >= 14 && Build.VERSION.SDK_INT < 21) { + if (Build.VERSION.SDK_INT < 21) { opts.inPurgeable = true; } @@ -685,9 +684,6 @@ public class ImageLoader { } else if (blurType == 0 && opts.inPurgeable) { Utilities.pinBitmap(image); } - if (runtimeHack != null) { - runtimeHack.trackFree(image.getRowBytes() * image.getHeight()); - } } } catch (Throwable e) { FileLog.e("tmessages", e); @@ -715,9 +711,6 @@ public class ImageLoader { } int delay = 20; - if (runtimeHack != null) { - delay = 60; - } if (mediaId != null) { delay = 0; } @@ -782,7 +775,7 @@ public class ImageLoader { } else { opts.inPreferredConfig = Bitmap.Config.RGB_565; } - if (Build.VERSION.SDK_INT >= 14 && Build.VERSION.SDK_INT < 21) { + if (Build.VERSION.SDK_INT < 21) { opts.inPurgeable = true; } @@ -850,9 +843,6 @@ public class ImageLoader { if (!blured && opts.inPurgeable) { Utilities.pinBitmap(image); } - if (runtimeHack != null && image != null) { - runtimeHack.trackFree(image.getRowBytes() * image.getHeight()); - } } } catch (Throwable e) { //don't promt @@ -877,9 +867,6 @@ public class ImageLoader { toSet = bitmapDrawable; } else { Bitmap image = bitmapDrawable.getBitmap(); - if (runtimeHack != null) { - runtimeHack.trackAlloc(image.getRowBytes() * image.getHeight()); - } image.recycle(); } } @@ -1098,19 +1085,10 @@ public class ImageLoader { int cacheSize = Math.min(15, ((ActivityManager) ApplicationLoader.applicationContext.getSystemService(Context.ACTIVITY_SERVICE)).getMemoryClass() / 7) * 1024 * 1024; - if (Build.VERSION.SDK_INT < 11) { - runtimeHack = new VMRuntimeHack(); - cacheSize = 1024 * 1024 * 3; - } memCache = new LruCache(cacheSize) { @Override protected int sizeOf(String key, BitmapDrawable value) { - Bitmap b = value.getBitmap(); - if (Build.VERSION.SDK_INT < 12) { - return b.getRowBytes() * b.getHeight(); - } else { - return b.getByteCount(); - } + return value.getBitmap().getByteCount(); } @Override @@ -1121,9 +1099,6 @@ public class ImageLoader { final Integer count = bitmapUseCounts.get(key); if (count == null || count == 0) { Bitmap b = oldValue.getBitmap(); - if (runtimeHack != null) { - runtimeHack.trackAlloc(b.getRowBytes() * b.getHeight()); - } if (!b.isRecycled()) { b.recycle(); } @@ -1937,11 +1912,7 @@ public class ImageLoader { } while (currentHttpTasksCount < 4 && !httpTasks.isEmpty()) { HttpImageTask task = httpTasks.poll(); - if (android.os.Build.VERSION.SDK_INT >= 11) { - task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, null, null, null); - } else { - task.execute(null, null, null); - } + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, null, null, null); currentHttpTasksCount++; } } @@ -2006,11 +1977,7 @@ public class ImageLoader { } while (currentHttpFileLoadTasksCount < 2 && !httpFileLoadTasks.isEmpty()) { HttpFileTask task = httpFileLoadTasks.poll(); - if (android.os.Build.VERSION.SDK_INT >= 11) { - task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, null, null, null); - } else { - task.execute(null, null, null); - } + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, null, null, null); currentHttpFileLoadTasksCount++; } } @@ -2057,7 +2024,7 @@ public class ImageLoader { } bmOptions.inJustDecodeBounds = false; bmOptions.inSampleSize = (int) scaleFactor; - bmOptions.inPurgeable = Build.VERSION.SDK_INT >= 14 && Build.VERSION.SDK_INT < 21; + bmOptions.inPurgeable = Build.VERSION.SDK_INT < 21; String exifPath = null; if (path != null) { diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/ImageReceiver.java b/TMessagesProj/src/main/java/org/telegram/messenger/ImageReceiver.java index da2b80703..b12dc6099 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/ImageReceiver.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/ImageReceiver.java @@ -652,6 +652,8 @@ public class ImageReceiver implements NotificationCenter.NotificationCenterDeleg public Bitmap getBitmap() { if (currentImage instanceof AnimatedFileDrawable) { return ((AnimatedFileDrawable) currentImage).getAnimatedBitmap(); + } else if (staticThumb instanceof AnimatedFileDrawable) { + return ((AnimatedFileDrawable) staticThumb).getAnimatedBitmap(); } else if (currentImage instanceof BitmapDrawable) { return ((BitmapDrawable) currentImage).getBitmap(); } else if (currentThumb instanceof BitmapDrawable) { @@ -665,6 +667,8 @@ public class ImageReceiver implements NotificationCenter.NotificationCenterDeleg public int getBitmapWidth() { if (currentImage instanceof AnimatedFileDrawable) { return orientation % 360 == 0 || orientation % 360 == 180 ? currentImage.getIntrinsicWidth() : currentImage.getIntrinsicHeight(); + } else if (staticThumb instanceof AnimatedFileDrawable) { + return orientation % 360 == 0 || orientation % 360 == 180 ? staticThumb.getIntrinsicWidth() : staticThumb.getIntrinsicHeight(); } Bitmap bitmap = getBitmap(); return orientation % 360 == 0 || orientation % 360 == 180 ? bitmap.getWidth() : bitmap.getHeight(); @@ -673,6 +677,8 @@ public class ImageReceiver implements NotificationCenter.NotificationCenterDeleg public int getBitmapHeight() { if (currentImage instanceof AnimatedFileDrawable) { return orientation % 360 == 0 || orientation % 360 == 180 ? currentImage.getIntrinsicHeight() : currentImage.getIntrinsicWidth(); + } else if (staticThumb instanceof AnimatedFileDrawable) { + return orientation % 360 == 0 || orientation % 360 == 180 ? staticThumb.getIntrinsicHeight() : staticThumb.getIntrinsicWidth(); } Bitmap bitmap = getBitmap(); return orientation % 360 == 0 || orientation % 360 == 180 ? bitmap.getHeight() : bitmap.getWidth(); @@ -872,6 +878,10 @@ public class ImageReceiver implements NotificationCenter.NotificationCenterDeleg return currentImage instanceof AnimatedFileDrawable && ((AnimatedFileDrawable) currentImage).isRunning(); } + public AnimatedFileDrawable getAnimation() { + return currentImage instanceof AnimatedFileDrawable ? (AnimatedFileDrawable) currentImage : null; + } + protected Integer getTag(boolean thumb) { if (thumb) { return thumbTag; @@ -990,15 +1000,11 @@ public class ImageReceiver implements NotificationCenter.NotificationCenterDeleg if (key != null && (newKey == null || !newKey.equals(key)) && image != null) { if (image instanceof AnimatedFileDrawable) { AnimatedFileDrawable fileDrawable = (AnimatedFileDrawable) image; - fileDrawable.stop(); fileDrawable.recycle(); } else if (image instanceof BitmapDrawable) { Bitmap bitmap = ((BitmapDrawable) image).getBitmap(); boolean canDelete = ImageLoader.getInstance().decrementUseCount(key); if (!ImageLoader.getInstance().isInCache(key)) { - if (ImageLoader.getInstance().runtimeHack != null) { - ImageLoader.getInstance().runtimeHack.trackAlloc(bitmap.getRowBytes() * bitmap.getHeight()); - } if (canDelete) { bitmap.recycle(); } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/MediaController.java b/TMessagesProj/src/main/java/org/telegram/messenger/MediaController.java index ea42737a3..44b725c54 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/MediaController.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/MediaController.java @@ -323,7 +323,6 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, private long recordTimeCount; private long recordDialogId; private MessageObject recordReplyingMessageObject; - private boolean recordAsAdmin; private DispatchQueue fileDecodingQueue; private DispatchQueue playerQueue; private ArrayList usedPlayerBuffers = new ArrayList<>(); @@ -605,12 +604,12 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, fileDecodingQueue = new DispatchQueue("fileDecodingQueue"); SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE); - mobileDataDownloadMask = preferences.getInt("mobileDataDownloadMask", AUTODOWNLOAD_MASK_PHOTO | AUTODOWNLOAD_MASK_AUDIO | AUTODOWNLOAD_MASK_MUSIC | (Build.VERSION.SDK_INT >= 11 ? AUTODOWNLOAD_MASK_GIF : 0)); - wifiDownloadMask = preferences.getInt("wifiDownloadMask", AUTODOWNLOAD_MASK_PHOTO | AUTODOWNLOAD_MASK_AUDIO | AUTODOWNLOAD_MASK_MUSIC | (Build.VERSION.SDK_INT >= 11 ? AUTODOWNLOAD_MASK_GIF : 0)); + mobileDataDownloadMask = preferences.getInt("mobileDataDownloadMask", AUTODOWNLOAD_MASK_PHOTO | AUTODOWNLOAD_MASK_AUDIO | AUTODOWNLOAD_MASK_MUSIC | AUTODOWNLOAD_MASK_GIF); + wifiDownloadMask = preferences.getInt("wifiDownloadMask", AUTODOWNLOAD_MASK_PHOTO | AUTODOWNLOAD_MASK_AUDIO | AUTODOWNLOAD_MASK_MUSIC | AUTODOWNLOAD_MASK_GIF); roamingDownloadMask = preferences.getInt("roamingDownloadMask", 0); saveToGallery = preferences.getBoolean("save_gallery", false); - autoplayGifs = preferences.getBoolean("autoplay_gif", true) && Build.VERSION.SDK_INT >= 11; - raiseToSpeak = preferences.getBoolean("raise_to_speak", true) && Build.VERSION.SDK_INT >= 11; + autoplayGifs = preferences.getBoolean("autoplay_gif", true); + raiseToSpeak = preferences.getBoolean("raise_to_speak", true); customTabs = preferences.getBoolean("custom_tabs", true); directShare = preferences.getBoolean("direct_share", true); shuffleMusic = preferences.getBoolean("shuffleMusic", false); @@ -1085,9 +1084,6 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, } public void startMediaObserver() { - if (android.os.Build.VERSION.SDK_INT < 14) { - return; - } ApplicationLoader.applicationHandler.removeCallbacks(stopMediaObserverRunnable); startObserverToken++; try { @@ -1107,9 +1103,6 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, } public void stopMediaObserver() { - if (android.os.Build.VERSION.SDK_INT < 14) { - return; - } if (stopMediaObserverRunnable == null) { stopMediaObserverRunnable = new StopMediaObserverRunnable(); } @@ -1655,7 +1648,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, if (!raiseChat.playFirstUnreadVoiceMessage()) { raiseToEarRecord = true; useFrontSpeaker = false; - startRecording(raiseChat.getDialogId(), null, false); + startRecording(raiseChat.getDialogId(), null); } ignoreOnPause = true; if (proximityHasDifferentValues && proximityWakeLock != null && !proximityWakeLock.isHeld()) { @@ -1720,7 +1713,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, return; } raiseToEarRecord = true; - startRecording(raiseChat.getDialogId(), null, false); + startRecording(raiseChat.getDialogId(), null); ignoreOnPause = true; } @@ -2025,6 +2018,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, if (currentPlaylistNum == -1) { playlist.clear(); shuffledPlaylist.clear(); + currentPlaylistNum = playlist.size(); playlist.add(current); } if (current.isMusic()) { @@ -2540,7 +2534,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, return downloadingCurrentMessage; } - public void startRecording(final long dialog_id, final MessageObject reply_to_msg, final boolean asAdmin) { + public void startRecording(final long dialog_id, final MessageObject reply_to_msg) { boolean paused = false; if (playingMessageObject != null && isPlayingAudio(playingMessageObject) && !isAudioPaused()) { paused = true; @@ -2592,17 +2586,13 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, }); return; } - //if (Build.VERSION.SDK_INT >= 11) { - // audioRecorder = new AudioRecord(MediaRecorder.AudioSource.VOICE_COMMUNICATION, 16000, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, recordBufferSize * 10); - //} else { - audioRecorder = new AudioRecord(MediaRecorder.AudioSource.MIC, 16000, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, recordBufferSize * 10); - //} + + audioRecorder = new AudioRecord(MediaRecorder.AudioSource.MIC, 16000, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, recordBufferSize * 10); recordStartTime = System.currentTimeMillis(); recordTimeCount = 0; samplesCount = 0; recordDialogId = dialog_id; recordReplyingMessageObject = reply_to_msg; - recordAsAdmin = asAdmin; fileBuffer.rewind(); audioRecorder.startRecording(); @@ -2670,7 +2660,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, } TLRPC.TL_messages_messages messagesRes = new TLRPC.TL_messages_messages(); messagesRes.messages.add(messageObject.messageOwner); - MessagesStorage.getInstance().putMessages(messagesRes, messageObject.getDialogId(), -1, 0, 0, false); + MessagesStorage.getInstance().putMessages(messagesRes, messageObject.getDialogId(), -1, 0, false); ArrayList arrayList = new ArrayList<>(); arrayList.add(messageObject); NotificationCenter.getInstance().postNotificationName(NotificationCenter.replaceMessagesObjects, messageObject.getDialogId(), arrayList); @@ -2705,7 +2695,7 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, audioToSend.attributes.add(attributeAudio); if (duration > 700) { if (send == 1) { - SendMessagesHelper.getInstance().sendMessage(audioToSend, null, recordingAudioFileToSend.getAbsolutePath(), recordDialogId, recordReplyingMessageObject, recordAsAdmin, null, null); + SendMessagesHelper.getInstance().sendMessage(audioToSend, null, recordingAudioFileToSend.getAbsolutePath(), recordDialogId, recordReplyingMessageObject, null, null); } NotificationCenter.getInstance().postNotificationName(NotificationCenter.audioDidSent, send == 2 ? audioToSend : null, send == 2 ? recordingAudioFileToSend.getAbsolutePath() : null); } else { @@ -2866,10 +2856,8 @@ public class MediaController implements AudioManager.OnAudioFocusChangeListener, if (result) { if (type == 2) { - if (Build.VERSION.SDK_INT >= 12) { - DownloadManager downloadManager = (DownloadManager) ApplicationLoader.applicationContext.getSystemService(Context.DOWNLOAD_SERVICE); - downloadManager.addCompletedDownload(destFile.getName(), destFile.getName(), false, mime, destFile.getAbsolutePath(), destFile.length(), true); - } + DownloadManager downloadManager = (DownloadManager) ApplicationLoader.applicationContext.getSystemService(Context.DOWNLOAD_SERVICE); + downloadManager.addCompletedDownload(destFile.getName(), destFile.getName(), false, mime, destFile.getAbsolutePath(), destFile.length(), true); } else { AndroidUtilities.addMediaToGallery(Uri.fromFile(destFile)); } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/MessageObject.java b/TMessagesProj/src/main/java/org/telegram/messenger/MessageObject.java index 8dc531f0f..3f77fec4e 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/MessageObject.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/MessageObject.java @@ -314,6 +314,8 @@ public class MessageObject { messageText = LocaleController.getString("ActionMigrateFromGroup", R.string.ActionMigrateFromGroup); } else if (message.action instanceof TLRPC.TL_messageActionPinMessage) { generatePinMessageText(fromUser, fromUser == null ? chats.get(message.to_id.channel_id) : null); + } else if (message.action instanceof TLRPC.TL_messageActionHistoryClear) { + messageText = LocaleController.getString("HistoryCleared", R.string.HistoryCleared); } } } else if (!isMediaEmpty()) { @@ -370,7 +372,9 @@ public class MessageObject { if (messageOwner.message != null && messageOwner.id < 0 && messageOwner.message.length() > 6 && isVideo()) { videoEditedInfo = new VideoEditedInfo(); - videoEditedInfo.parseString(messageOwner.message); + if (!videoEditedInfo.parseString(messageOwner.message)) { + videoEditedInfo = null; + } } generateCaption(); @@ -510,6 +514,9 @@ public class MessageObject { contentType = -1; type = -1; } + } else if (messageOwner.action instanceof TLRPC.TL_messageActionHistoryClear) { + contentType = -1; + type = -1; } else { contentType = 1; type = 10; @@ -1199,27 +1206,10 @@ public class MessageObject { return message.media_unread; } - public boolean isImportant() { - return isImportant(messageOwner); - } - public boolean isMegagroup() { return isMegagroup(messageOwner); } - public static boolean isImportant(TLRPC.Message message) { - if (isMegagroup(message)) { - return message.post; - } - if (message.to_id.channel_id != 0) { - if (message instanceof TLRPC.TL_message_layer47 || message instanceof TLRPC.TL_message_old7) { - return message.to_id.channel_id != 0 && (message.from_id <= 0 || message.mentioned || message.out || (message.flags & TLRPC.MESSAGE_FLAG_HAS_FROM_ID) == 0); - } - return message.post; - } - return false; - } - public static boolean isMegagroup(TLRPC.Message message) { return (message.flags & TLRPC.MESSAGE_FLAG_MEGAGROUP) != 0; } @@ -1658,7 +1648,7 @@ public class MessageObject { return false; } } - if (chat.megagroup && message.out || !chat.megagroup && (chat.creator || chat.editor && isOut(message)) && isImportant(message)) { + if (chat.megagroup && message.out || !chat.megagroup && (chat.creator || chat.editor && isOut(message)) && message.post) { if (message.media instanceof TLRPC.TL_messageMediaPhoto || message.media instanceof TLRPC.TL_messageMediaDocument && !isStickerMessage(message) || message.media instanceof TLRPC.TL_messageMediaEmpty || diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/MessagesController.java b/TMessagesProj/src/main/java/org/telegram/messenger/MessagesController.java index 3315783c4..415689a47 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/MessagesController.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/MessagesController.java @@ -14,8 +14,6 @@ import android.app.ProgressDialog; import android.content.Context; import android.content.DialogInterface; import android.content.SharedPreferences; -import android.content.pm.PackageInfo; -import android.os.Build; import android.os.Bundle; import android.util.Base64; import android.util.SparseArray; @@ -24,10 +22,12 @@ import android.widget.Toast; import org.telegram.SQLite.SQLiteCursor; import org.telegram.messenger.query.BotQuery; +import org.telegram.messenger.query.DraftQuery; import org.telegram.messenger.query.MessagesQuery; import org.telegram.messenger.query.SearchQuery; import org.telegram.messenger.query.StickersQuery; import org.telegram.tgnet.ConnectionsManager; +import org.telegram.tgnet.NativeByteBuffer; import org.telegram.tgnet.RequestDelegate; import org.telegram.tgnet.SerializedData; import org.telegram.tgnet.TLObject; @@ -56,12 +56,13 @@ public class MessagesController implements NotificationCenter.NotificationCenter private HashMap exportedChats = new HashMap<>(); - public ArrayList dialogs = new ArrayList<>(); - public ArrayList dialogsServerOnly = new ArrayList<>(); - public ArrayList dialogsGroupsOnly = new ArrayList<>(); + public ArrayList dialogs = new ArrayList<>(); + public ArrayList dialogsServerOnly = new ArrayList<>(); + public ArrayList dialogsGroupsOnly = new ArrayList<>(); public int nextDialogsCacheOffset; public ConcurrentHashMap dialogs_read_inbox_max = new ConcurrentHashMap<>(100, 1.0f, 2); - public ConcurrentHashMap dialogs_dict = new ConcurrentHashMap<>(100, 1.0f, 2); + public ConcurrentHashMap dialogs_read_outbox_max = new ConcurrentHashMap<>(100, 1.0f, 2); + public ConcurrentHashMap dialogs_dict = new ConcurrentHashMap<>(100, 1.0f, 2); public HashMap dialogMessage = new HashMap<>(); public HashMap dialogMessagesByRandomIds = new HashMap<>(); public HashMap dialogMessagesByIds = new HashMap<>(); @@ -91,6 +92,9 @@ public class MessagesController implements NotificationCenter.NotificationCenter private HashMap channelsPts = new HashMap<>(); private HashMap gettingDifferenceChannels = new HashMap<>(); + private HashMap gettingUnknownChannels = new HashMap<>(); + private HashMap checkingLastMessagesDialogs = new HashMap<>(); + private ArrayList updatesQueueSeq = new ArrayList<>(); private ArrayList updatesQueuePts = new ArrayList<>(); private ArrayList updatesQueueQts = new ArrayList<>(); @@ -175,6 +179,46 @@ public class MessagesController implements NotificationCenter.NotificationCenter private static volatile MessagesController Instance = null; + private final Comparator dialogComparator = new Comparator() { + @Override + public int compare(TLRPC.TL_dialog dialog1, TLRPC.TL_dialog dialog2) { + TLRPC.DraftMessage draftMessage = DraftQuery.getDraft(dialog1.id); + int date1 = draftMessage != null && draftMessage.date >= dialog1.last_message_date ? draftMessage.date : dialog1.last_message_date; + draftMessage = DraftQuery.getDraft(dialog2.id); + int date2 = draftMessage != null && draftMessage.date >= dialog2.last_message_date ? draftMessage.date : dialog2.last_message_date; + if (date1 < date2) { + return 1; + } else if (date1 > date2) { + return -1; + } + return 0; + } + }; + + private final Comparator updatesComparator = new Comparator() { + @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); + } else if (ltype == 2) { + int lChannel = getUpdateChannelId(lhs); + int rChannel = getUpdateChannelId(rhs); + if (lChannel == rChannel) { + return AndroidUtilities.compare(lhs.pts, rhs.pts); + } else { + return AndroidUtilities.compare(lChannel, rChannel); + } + } + return 0; + } + }; + public static MessagesController getInstance() { MessagesController localInstance = Instance; if (localInstance == null) { @@ -451,7 +495,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter if (obj != null && obj.getId() == msgId) { obj.messageOwner.id = newMsgId; obj.messageOwner.send_state = MessageObject.MESSAGE_SEND_STATE_SENT; - TLRPC.Dialog dialog = dialogs_dict.get(did); + TLRPC.TL_dialog dialog = dialogs_dict.get(did); if (dialog != null) { if (dialog.top_message == msgId) { dialog.top_message = newMsgId; @@ -466,19 +510,21 @@ public class MessagesController implements NotificationCenter.NotificationCenter } } - public void cleanUp() { + public void cleanup() { ContactsController.getInstance().cleanup(); MediaController.getInstance().cleanup(); NotificationsController.getInstance().cleanup(); - SendMessagesHelper.getInstance().cleanUp(); - SecretChatHelper.getInstance().cleanUp(); + SendMessagesHelper.getInstance().cleanup(); + SecretChatHelper.getInstance().cleanup(); StickersQuery.cleanup(); - SearchQuery.cleanUp(); + SearchQuery.cleanup(); + DraftQuery.cleanup(); reloadingWebpages.clear(); reloadingWebpagesPending.clear(); dialogs_dict.clear(); dialogs_read_inbox_max.clear(); + dialogs_read_outbox_max.clear(); exportedChats.clear(); fullUsersAbout.clear(); dialogs.clear(); @@ -506,6 +552,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter updatesQueueSeq.clear(); updatesQueuePts.clear(); updatesQueueQts.clear(); + gettingUnknownChannels.clear(); updatesStartWaitTimeSeq = 0; updatesStartWaitTimePts = 0; updatesStartWaitTimeQts = 0; @@ -557,6 +604,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter } addSupportUser(); + NotificationCenter.getInstance().postNotificationName(NotificationCenter.dialogsNeedReload); } public TLRPC.User getUser(Integer id) { @@ -813,6 +861,88 @@ public class MessagesController implements NotificationCenter.NotificationCenter loadedFullChats.clear(); } + private void reloadDialogsReadValue(ArrayList dialogs, long did) { + if (dialogs.isEmpty()) { + return; + } + TLRPC.TL_messages_getPeerDialogs req = new TLRPC.TL_messages_getPeerDialogs(); + if (dialogs != null) { + for (int a = 0; a < dialogs.size(); a++) { + req.peers.add(getInputPeer((int) dialogs.get(a).id)); + } + } else { + req.peers.add(getInputPeer((int) did)); + } + ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { + @Override + public void run(TLObject response, TLRPC.TL_error error) { + if (response != null) { + TLRPC.TL_messages_peerDialogs res = (TLRPC.TL_messages_peerDialogs) response; + ArrayList arrayList = new ArrayList<>(); + for (int a = 0; a < res.dialogs.size(); a++) { + TLRPC.TL_dialog dialog = res.dialogs.get(a); + if (dialog.read_inbox_max_id == 0) { + dialog.read_inbox_max_id = 1; + } + if (dialog.read_outbox_max_id == 0) { + dialog.read_outbox_max_id = 1; + } + if (dialog.id == 0 && dialog.peer != null) { + if (dialog.peer.user_id != 0) { + dialog.id = dialog.peer.user_id; + } else if (dialog.peer.chat_id != 0) { + dialog.id = -dialog.peer.chat_id; + } else if (dialog.peer.channel_id != 0) { + dialog.id = -dialog.peer.channel_id; + } + } + + Integer value = dialogs_read_inbox_max.get(dialog.id); + if (value == null) { + value = 0; + } + dialogs_read_inbox_max.put(dialog.id, Math.max(dialog.read_inbox_max_id, value)); + if (value == 0) { + if (dialog.peer.channel_id != 0) { + TLRPC.TL_updateReadChannelInbox update = new TLRPC.TL_updateReadChannelInbox(); + update.channel_id = dialog.peer.channel_id; + update.max_id = dialog.read_inbox_max_id; + arrayList.add(update); + } else { + TLRPC.TL_updateReadHistoryInbox update = new TLRPC.TL_updateReadHistoryInbox(); + update.peer = dialog.peer; + update.max_id = dialog.read_inbox_max_id; + arrayList.add(update); + } + } + + value = dialogs_read_outbox_max.get(dialog.id); + if (value == null) { + value = 0; + } + dialogs_read_outbox_max.put(dialog.id, Math.max(dialog.read_outbox_max_id, value)); + if (value == 0) { + if (dialog.peer.channel_id != 0) { + TLRPC.TL_updateReadChannelOutbox update = new TLRPC.TL_updateReadChannelOutbox(); + update.channel_id = dialog.peer.channel_id; + update.max_id = dialog.read_outbox_max_id; + arrayList.add(update); + } else { + TLRPC.TL_updateReadHistoryOutbox update = new TLRPC.TL_updateReadHistoryOutbox(); + update.peer = dialog.peer; + update.max_id = dialog.read_outbox_max_id; + arrayList.add(update); + } + } + } + if (!arrayList.isEmpty()) { + processUpdateArray(arrayList, null, null, false); + } + } + } + }); + } + public void loadFullChat(final int chat_id, final int classGuid, boolean force) { if (loadingFullChats.contains(chat_id) || !force && loadedFullChats.contains(chat_id)) { return; @@ -834,11 +964,41 @@ public class MessagesController implements NotificationCenter.NotificationCenter public void run(TLObject response, final TLRPC.TL_error error) { if (error == null) { final TLRPC.TL_messages_chatFull res = (TLRPC.TL_messages_chatFull) response; - if (chat != null && chat.megagroup) { - res.full_chat.unread_important_count = Math.max(res.full_chat.unread_important_count, res.full_chat.unread_count); - } MessagesStorage.getInstance().putUsersAndChats(res.users, res.chats, true, true); MessagesStorage.getInstance().updateChatInfo(res.full_chat, false); + + if (ChatObject.isChannel(chat)) { + long dialog_id = -chat_id; + Integer value = dialogs_read_inbox_max.get(dialog_id); + if (value == null) { + value = MessagesStorage.getInstance().getDialogReadMax(true, dialog_id); + } + + dialogs_read_inbox_max.put(dialog_id, Math.max(res.full_chat.read_inbox_max_id, value)); + if (value == 0) { + ArrayList arrayList = new ArrayList<>(); + TLRPC.TL_updateReadChannelInbox update = new TLRPC.TL_updateReadChannelInbox(); + update.channel_id = chat_id; + update.max_id = res.full_chat.read_inbox_max_id; + arrayList.add(update); + processUpdateArray(arrayList, null, null, false); + } + + value = dialogs_read_outbox_max.get(dialog_id); + if (value == null) { + value = MessagesStorage.getInstance().getDialogReadMax(true, dialog_id); + } + dialogs_read_outbox_max.put(dialog_id, Math.max(res.full_chat.read_outbox_max_id, value)); + if (value == 0) { + ArrayList arrayList = new ArrayList<>(); + TLRPC.TL_updateReadChannelOutbox update = new TLRPC.TL_updateReadChannelOutbox(); + update.channel_id = chat_id; + update.max_id = res.full_chat.read_outbox_max_id; + arrayList.add(update); + processUpdateArray(arrayList, null, null, false); + } + } + AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { @@ -979,6 +1139,18 @@ public class MessagesController implements NotificationCenter.NotificationCenter chatsLocal.put(c.id, c); } + Integer inboxValue = dialogs_read_inbox_max.get(dialog_id); + if (inboxValue == null) { + inboxValue = MessagesStorage.getInstance().getDialogReadMax(true, dialog_id); + dialogs_read_inbox_max.put(dialog_id, inboxValue); + } + + Integer outboxValue = dialogs_read_outbox_max.get(dialog_id); + if (outboxValue == null) { + outboxValue = MessagesStorage.getInstance().getDialogReadMax(true, dialog_id); + dialogs_read_outbox_max.put(dialog_id, outboxValue); + } + final ArrayList objects = new ArrayList<>(); for (int a = 0; a < messagesRes.messages.size(); a++) { TLRPC.Message message = messagesRes.messages.get(a); @@ -986,11 +1158,12 @@ public class MessagesController implements NotificationCenter.NotificationCenter message.flags |= TLRPC.MESSAGE_FLAG_MEGAGROUP; } message.dialog_id = dialog_id; + message.unread = (message.out ? outboxValue : inboxValue) < message.id; objects.add(new MessageObject(message, usersLocal, chatsLocal, true)); } ImageLoader.saveMessagesThumbs(messagesRes.messages); - MessagesStorage.getInstance().putMessages(messagesRes, dialog_id, -1, 0, 0, false); + MessagesStorage.getInstance().putMessages(messagesRes, dialog_id, -1, 0, false); AndroidUtilities.runOnUIThread(new Runnable() { @Override @@ -1139,44 +1312,45 @@ public class MessagesController implements NotificationCenter.NotificationCenter protected void processNewChannelDifferenceParams(int pts, int pts_count, int channelId) { FileLog.e("tmessages", "processNewChannelDifferenceParams pts = " + pts + " pts_count = " + pts_count + " channeldId = " + channelId); - TLRPC.Dialog dialog = dialogs_dict.get((long) -channelId); - if (dialog instanceof TLRPC.TL_dialogChannel) { - Integer channelPts = channelsPts.get(channelId); - if (channelPts == null) { - channelPts = MessagesStorage.getInstance().getChannelPtsSync(channelId); - if (channelPts == 0) { - channelPts = 1; - } - channelsPts.put(channelId, channelPts); + TLRPC.TL_dialog dialog = dialogs_dict.get((long) -channelId); + if (!DialogObject.isChannel(dialog)) { + return; + } + Integer channelPts = channelsPts.get(channelId); + if (channelPts == null) { + channelPts = MessagesStorage.getInstance().getChannelPtsSync(channelId); + if (channelPts == 0) { + channelPts = 1; } - if (channelPts + pts_count == pts) { - FileLog.e("tmessages", "APPLY CHANNEL PTS"); - channelsPts.put(channelId, pts); - MessagesStorage.getInstance().saveChannelPts(channelId, pts); - } else if (channelPts != pts) { - Long updatesStartWaitTime = updatesStartWaitTimeChannels.get(channelId); - Boolean gettingDifferenceChannel = gettingDifferenceChannels.get(channelId); - if (gettingDifferenceChannel == null) { - gettingDifferenceChannel = false; + channelsPts.put(channelId, channelPts); + } + if (channelPts + pts_count == pts) { + FileLog.e("tmessages", "APPLY CHANNEL PTS"); + channelsPts.put(channelId, pts); + MessagesStorage.getInstance().saveChannelPts(channelId, pts); + } else if (channelPts != pts) { + Long updatesStartWaitTime = updatesStartWaitTimeChannels.get(channelId); + Boolean gettingDifferenceChannel = gettingDifferenceChannels.get(channelId); + if (gettingDifferenceChannel == null) { + gettingDifferenceChannel = false; + } + if (gettingDifferenceChannel || updatesStartWaitTime == null || Math.abs(System.currentTimeMillis() - updatesStartWaitTime) <= 1500) { + FileLog.e("tmessages", "ADD CHANNEL UPDATE TO QUEUE pts = " + pts + " pts_count = " + pts_count); + if (updatesStartWaitTime == null) { + updatesStartWaitTimeChannels.put(channelId, System.currentTimeMillis()); } - if (gettingDifferenceChannel || updatesStartWaitTime == null || Math.abs(System.currentTimeMillis() - updatesStartWaitTime) <= 1500) { - FileLog.e("tmessages", "ADD CHANNEL UPDATE TO QUEUE pts = " + pts + " pts_count = " + pts_count); - if (updatesStartWaitTime == null) { - updatesStartWaitTimeChannels.put(channelId, System.currentTimeMillis()); - } - UserActionUpdatesPts updates = new UserActionUpdatesPts(); - updates.pts = pts; - updates.pts_count = pts_count; - updates.chat_id = channelId; - ArrayList arrayList = updatesQueueChannels.get(channelId); - if (arrayList == null) { - arrayList = new ArrayList<>(); - updatesQueueChannels.put(channelId, arrayList); - } - arrayList.add(updates); - } else { - getChannelDifference(channelId); + UserActionUpdatesPts updates = new UserActionUpdatesPts(); + updates.pts = pts; + updates.pts_count = pts_count; + updates.chat_id = channelId; + ArrayList arrayList = updatesQueueChannels.get(channelId); + if (arrayList == null) { + arrayList = new ArrayList<>(); + updatesQueueChannels.put(channelId, arrayList); } + arrayList.add(updates); + } else { + getChannelDifference(channelId); } } } @@ -1315,29 +1489,59 @@ public class MessagesController implements NotificationCenter.NotificationCenter }); } - public void loadUserPhotos(final int uid, final int offset, final int count, final long max_id, final boolean fromCache, final int classGuid) { + public void loadDialogPhotos(final int did, final int offset, final int count, final long max_id, final boolean fromCache, final int classGuid) { if (fromCache) { - MessagesStorage.getInstance().getUserPhotos(uid, offset, count, max_id, classGuid); + MessagesStorage.getInstance().getDialogPhotos(did, offset, count, max_id, classGuid); } else { - TLRPC.User user = getUser(uid); - if (user == null) { - return; - } - TLRPC.TL_photos_getUserPhotos req = new TLRPC.TL_photos_getUserPhotos(); - req.limit = count; - req.offset = offset; - req.max_id = (int) max_id; - req.user_id = getInputUser(user); - int reqId = ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { - @Override - public void run(TLObject response, TLRPC.TL_error error) { - if (error == null) { - TLRPC.photos_Photos res = (TLRPC.photos_Photos) response; - processLoadedUserPhotos(res, uid, offset, count, max_id, false, classGuid); - } + if (did > 0) { + TLRPC.User user = getUser(did); + if (user == null) { + return; } - }); - ConnectionsManager.getInstance().bindRequestToGuid(reqId, classGuid); + TLRPC.TL_photos_getUserPhotos req = new TLRPC.TL_photos_getUserPhotos(); + req.limit = count; + req.offset = offset; + req.max_id = (int) max_id; + req.user_id = getInputUser(user); + int reqId = ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { + @Override + public void run(TLObject response, TLRPC.TL_error error) { + if (error == null) { + TLRPC.photos_Photos res = (TLRPC.photos_Photos) response; + processLoadedUserPhotos(res, did, offset, count, max_id, false, classGuid); + } + } + }); + ConnectionsManager.getInstance().bindRequestToGuid(reqId, classGuid); + } else if (did < 0) { + TLRPC.TL_messages_search req = new TLRPC.TL_messages_search(); + req.filter = new TLRPC.TL_inputMessagesFilterChatPhotos(); + req.limit = count; + req.offset = offset; + req.max_id = (int) max_id; + req.q = ""; + req.peer = MessagesController.getInputPeer(did); + int reqId = ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { + @Override + public void run(TLObject response, TLRPC.TL_error error) { + if (error == null) { + TLRPC.messages_Messages messages = (TLRPC.messages_Messages) response; + TLRPC.TL_photos_photos res = new TLRPC.TL_photos_photos(); + res.count = messages.count; + res.users.addAll(messages.users); + for (int a = 0; a < messages.messages.size(); a++) { + TLRPC.Message message = messages.messages.get(a); + if (message.action == null || message.action.photo == null) { + continue; + } + res.photos.add(message.action.photo); + } + processLoadedUserPhotos(res, did, offset, count, max_id, false, classGuid); + } + } + }); + ConnectionsManager.getInstance().bindRequestToGuid(reqId, classGuid); + } } } @@ -1494,19 +1698,19 @@ public class MessagesController implements NotificationCenter.NotificationCenter } } - public void processLoadedUserPhotos(final TLRPC.photos_Photos res, final int uid, final int offset, final int count, final long max_id, final boolean fromCache, final int classGuid) { + public void processLoadedUserPhotos(final TLRPC.photos_Photos res, final int did, final int offset, final int count, final long max_id, final boolean fromCache, final int classGuid) { if (!fromCache) { MessagesStorage.getInstance().putUsersAndChats(res.users, null, true, true); - MessagesStorage.getInstance().putUserPhotos(uid, res); + MessagesStorage.getInstance().putDialogPhotos(did, res); } else if (res == null || res.photos.isEmpty()) { - loadUserPhotos(uid, offset, count, max_id, false, classGuid); + loadDialogPhotos(did, offset, count, max_id, false, classGuid); return; } AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { putUsers(res.users, fromCache); - NotificationCenter.getInstance().postNotificationName(NotificationCenter.userPhotosLoaded, uid, offset, count, fromCache, classGuid, res.photos); + NotificationCenter.getInstance().postNotificationName(NotificationCenter.dialogPhotosLoaded, did, offset, count, fromCache, classGuid, res.photos); } }); } @@ -1603,10 +1807,6 @@ public class MessagesController implements NotificationCenter.NotificationCenter }); } - public void deleteDialog(final long did, final int onlyHistory) { - deleteDialog(did, true, onlyHistory, 0); - } - public void deleteUserChannelHistory(final TLRPC.Chat chat, final TLRPC.User user, int offset) { if (offset == 0) { MessagesStorage.getInstance().deleteUserChannelHistory(chat.id, user.id); @@ -1628,6 +1828,10 @@ public class MessagesController implements NotificationCenter.NotificationCenter }); } + public void deleteDialog(final long did, final int onlyHistory) { + deleteDialog(did, true, onlyHistory, 0); + } + private void deleteDialog(final long did, final boolean first, final int onlyHistory, final int max_id) { int lower_part = (int) did; int high_id = (int) (did >> 32); @@ -1637,43 +1841,80 @@ public class MessagesController implements NotificationCenter.NotificationCenter MessagesStorage.getInstance().deleteDialog(did, onlyHistory); return; } - if (onlyHistory == 0) { + if (onlyHistory == 0 || onlyHistory == 3) { AndroidUtilities.uninstallShortcut(did); } if (first) { - TLRPC.Dialog dialog = dialogs_dict.get(did); + MessagesStorage.getInstance().deleteDialog(did, onlyHistory); + TLRPC.TL_dialog dialog = dialogs_dict.get(did); if (dialog != null) { if (max_id_delete == 0) { max_id_delete = Math.max(0, dialog.top_message); } - if (onlyHistory == 0) { + if (onlyHistory == 0 || onlyHistory == 3) { dialogs.remove(dialog); - if (dialogsServerOnly.remove(dialog)) { - if (dialog instanceof TLRPC.TL_dialogChannel) { - Utilities.stageQueue.postRunnable(new Runnable() { - @Override - public void run() { - channelsPts.remove(-(int) did); - shortPollChannels.delete(-(int) did); - needShortPollChannels.delete(-(int) did); - } - }); - } + if (dialogsServerOnly.remove(dialog) && DialogObject.isChannel(dialog)) { + Utilities.stageQueue.postRunnable(new Runnable() { + @Override + public void run() { + channelsPts.remove(-(int) did); + shortPollChannels.delete(-(int) did); + needShortPollChannels.delete(-(int) did); + } + }); } dialogsGroupsOnly.remove(dialog); dialogs_dict.remove(did); dialogs_read_inbox_max.remove(did); + dialogs_read_outbox_max.remove(did); nextDialogsCacheOffset--; } else { dialog.unread_count = 0; } - dialogMessage.remove(dialog.id); - MessageObject object = dialogMessagesByIds.remove(dialog.top_message); + int lastMessageId; + MessageObject object = dialogMessage.remove(dialog.id); + if (object != null) { + lastMessageId = object.getId(); + dialogMessagesByIds.remove(object.getId()); + } else { + lastMessageId = dialog.top_message; + object = dialogMessagesByIds.remove(dialog.top_message); + } if (object != null && object.messageOwner.random_id != 0) { dialogMessagesByRandomIds.remove(object.messageOwner.random_id); } - dialog.top_message = 0; + if (onlyHistory == 1 && lower_part != 0 && lastMessageId > 0) { + TLRPC.TL_messageService message = new TLRPC.TL_messageService(); + message.id = dialog.top_message; + message.out = false; + message.from_id = UserConfig.getClientUserId(); + message.flags |= 256; + message.action = new TLRPC.TL_messageActionHistoryClear(); + message.date = dialog.last_message_date; + if (lower_part > 0) { + message.to_id = new TLRPC.TL_peerUser(); + message.to_id.user_id = lower_part; + } else { + TLRPC.Chat chat = getChat(-lower_part); + if (ChatObject.isChannel(chat)) { + message.to_id = new TLRPC.TL_peerChannel(); + message.to_id.channel_id = -lower_part; + } else { + message.to_id = new TLRPC.TL_peerChat(); + message.to_id.chat_id = -lower_part; + } + } + final MessageObject obj = new MessageObject(message, null, createdDialogIds.contains(message.dialog_id)); + final ArrayList objArr = new ArrayList<>(); + objArr.add(obj); + ArrayList arr = new ArrayList<>(); + arr.add(message); + updateInterfaceWithMessages(did, objArr); + MessagesStorage.getInstance().putMessages(arr, false, true, false, 0); + } else { + dialog.top_message = 0; + } } NotificationCenter.getInstance().postNotificationName(NotificationCenter.dialogsNeedReload); NotificationCenter.getInstance().postNotificationName(NotificationCenter.removeAllMessagesFromDialog, did, false); @@ -1688,11 +1929,9 @@ public class MessagesController implements NotificationCenter.NotificationCenter }); } }); - - MessagesStorage.getInstance().deleteDialog(did, onlyHistory); } - if (high_id == 1) { + if (high_id == 1 || onlyHistory == 3) { return; } @@ -1704,6 +1943,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter TLRPC.TL_messages_deleteHistory req = new TLRPC.TL_messages_deleteHistory(); req.peer = peer; req.max_id = max_id_delete; + req.just_clear = onlyHistory != 0; final int max_id_delete_final = max_id_delete; ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { @Override @@ -2201,52 +2441,36 @@ public class MessagesController implements NotificationCenter.NotificationCenter } } - public void loadMessages(final long dialog_id, final int count, final int max_id, boolean fromCache, int midDate, final int classGuid, final int load_type, final int last_message_id, final int important, final int loadIndex) { - loadMessages(dialog_id, count, max_id, fromCache, midDate, classGuid, load_type, last_message_id, important, loadIndex, 0, 0, 0, false); + public void loadMessages(final long dialog_id, final int count, final int max_id, boolean fromCache, int midDate, final int classGuid, final int load_type, final int last_message_id, final boolean isChannel, final int loadIndex) { + loadMessages(dialog_id, count, max_id, fromCache, midDate, classGuid, load_type, last_message_id, isChannel, loadIndex, 0, 0, 0, false); } - public void loadMessages(final long dialog_id, final int count, final int max_id, boolean fromCache, int midDate, final int classGuid, final int load_type, final int last_message_id, final int important, final int loadIndex, final int first_unread, final int unread_count, final int last_date, final boolean queryFromServer) { - FileLog.e("tmessages", "load messages in chat " + dialog_id + " count " + count + " max_id " + max_id + " cache " + fromCache + " mindate = " + midDate + " guid " + classGuid + " load_type " + load_type + " last_message_id " + last_message_id + " imp " + important + " index " + loadIndex + " firstUnread " + first_unread + " underad count " + unread_count + " last_date " + last_date + " queryFromServer " + queryFromServer); + public void loadMessages(final long dialog_id, final int count, final int max_id, boolean fromCache, int midDate, final int classGuid, final int load_type, final int last_message_id, final boolean isChannel, final int loadIndex, final int first_unread, final int unread_count, final int last_date, final boolean queryFromServer) { + FileLog.e("tmessages", "load messages in chat " + dialog_id + " count " + count + " max_id " + max_id + " cache " + fromCache + " mindate = " + midDate + " guid " + classGuid + " load_type " + load_type + " last_message_id " + last_message_id + " index " + loadIndex + " firstUnread " + first_unread + " underad count " + unread_count + " last_date " + last_date + " queryFromServer " + queryFromServer); int lower_part = (int) dialog_id; if (fromCache || lower_part == 0) { - MessagesStorage.getInstance().getMessages(dialog_id, count, max_id, midDate, classGuid, load_type, important, loadIndex); + MessagesStorage.getInstance().getMessages(dialog_id, count, max_id, midDate, classGuid, load_type, isChannel, loadIndex); } else { - TLObject request; - if (important == 2) { - TLRPC.TL_channels_getImportantHistory req = new TLRPC.TL_channels_getImportantHistory(); - req.channel = getInputChannel(-lower_part); - if (load_type == 3) { - req.add_offset = -count / 2; - } else if (load_type == 1) { - req.add_offset = -count - 1; - } else if (load_type == 2 && max_id != 0) { - req.add_offset = -count + 6; - } else { - if (max_id != 0) { + TLRPC.TL_messages_getHistory req = new TLRPC.TL_messages_getHistory(); + req.peer = getInputPeer(lower_part); + if (load_type == 3) { + req.add_offset = -count / 2; + } else if (load_type == 1) { + req.add_offset = -count - 1; + } else if (load_type == 2 && max_id != 0) { + req.add_offset = -count + 6; + } else { + if (lower_part < 0 && max_id != 0) { + TLRPC.Chat chat = getChat(-lower_part); + if (ChatObject.isChannel(chat)) { req.add_offset = -1; req.limit += 1; } } - req.limit += count; - req.offset_id = max_id; - request = req; - } else { - TLRPC.TL_messages_getHistory req = new TLRPC.TL_messages_getHistory(); - req.peer = getInputPeer(lower_part); - if (load_type == 3) { - req.add_offset = -count / 2; - } else if (load_type == 1) { - req.add_offset = -count - 1; - } else if (load_type == 2 && max_id != 0) { - req.add_offset = -count + 6; - } else { - req.add_offset = 0; - } - req.limit = count; - req.offset_id = max_id; - request = req; } - int reqId = ConnectionsManager.getInstance().sendRequest(request, new RequestDelegate() { + req.limit = count; + req.offset_id = max_id; + int reqId = ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { @Override public void run(TLObject response, TLRPC.TL_error error) { if (response != null) { @@ -2254,7 +2478,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter if (res.messages.size() > count) { res.messages.remove(0); } - processLoadedMessages(res, dialog_id, count, max_id, false, classGuid, first_unread, last_message_id, unread_count, last_date, load_type, important, false, loadIndex, queryFromServer); + processLoadedMessages(res, dialog_id, count, max_id, false, classGuid, first_unread, last_message_id, unread_count, last_date, load_type, isChannel, false, loadIndex, queryFromServer); } } }); @@ -2305,7 +2529,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter } } if (!messagesRes.messages.isEmpty()) { - MessagesStorage.getInstance().putMessages(messagesRes, dialog_id, -2, 0, 0, false); + MessagesStorage.getInstance().putMessages(messagesRes, dialog_id, -2, 0, false); NotificationCenter.getInstance().postNotificationName(NotificationCenter.replaceMessagesObjects, dialog_id, arrayList); } } @@ -2316,8 +2540,8 @@ public class MessagesController implements NotificationCenter.NotificationCenter } public void processLoadedMessages(final TLRPC.messages_Messages messagesRes, final long dialog_id, final int count, final int max_id, final boolean isCache, final int classGuid, - final int first_unread, final int last_message_id, final int unread_count, final int last_date, final int load_type, final int important, final boolean isEnd, final int loadIndex, final boolean queryFromServer) { - FileLog.e("tmessages", "processLoadedMessages size " + messagesRes.messages.size() + " in chat " + dialog_id + " count " + count + " max_id " + max_id + " cache " + isCache + " guid " + classGuid + " load_type " + load_type + " last_message_id " + last_message_id + " imp " + important + " index " + loadIndex + " firstUnread " + first_unread + " underad count " + unread_count + " last_date " + last_date + " queryFromServer " + queryFromServer); + final int first_unread, final int last_message_id, final int unread_count, final int last_date, final int load_type, final boolean isChannel, final boolean isEnd, final int loadIndex, final boolean queryFromServer) { + FileLog.e("tmessages", "processLoadedMessages size " + messagesRes.messages.size() + " in chat " + dialog_id + " count " + count + " max_id " + max_id + " cache " + isCache + " guid " + classGuid + " load_type " + load_type + " last_message_id " + last_message_id + " isChannel " + isChannel + " index " + loadIndex + " firstUnread " + first_unread + " underad count " + unread_count + " last_date " + last_date + " queryFromServer " + queryFromServer); Utilities.stageQueue.postRunnable(new Runnable() { @Override public void run() { @@ -2332,7 +2556,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter channelsPts.put(channelId, messagesRes.pts); createDialog = true; if (needShortPollChannels.indexOfKey(channelId) >= 0 && shortPollChannels.indexOfKey(channelId) < 0) { - getChannelDifference(channelId, 2); + getChannelDifference(channelId, 2, 0); } else { getChannelDifference(channelId); } @@ -2355,7 +2579,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { - loadMessages(dialog_id, count, load_type == 2 && queryFromServer ? first_unread : max_id, false, 0, classGuid, load_type, last_message_id, important, loadIndex, first_unread, unread_count, last_date, queryFromServer); + loadMessages(dialog_id, count, load_type == 2 && queryFromServer ? first_unread : max_id, false, 0, classGuid, load_type, last_message_id, isChannel, loadIndex, first_unread, unread_count, last_date, queryFromServer); } }); return; @@ -2372,6 +2596,18 @@ public class MessagesController implements NotificationCenter.NotificationCenter } int size = messagesRes.messages.size(); if (!isCache) { + Integer inboxValue = dialogs_read_inbox_max.get(dialog_id); + if (inboxValue == null) { + inboxValue = MessagesStorage.getInstance().getDialogReadMax(true, dialog_id); + dialogs_read_inbox_max.put(dialog_id, inboxValue); + } + + Integer outboxValue = dialogs_read_outbox_max.get(dialog_id); + if (outboxValue == null) { + outboxValue = MessagesStorage.getInstance().getDialogReadMax(true, dialog_id); + dialogs_read_outbox_max.put(dialog_id, outboxValue); + } + for (int a = 0; a < size; a++) { TLRPC.Message message = messagesRes.messages.get(a); if (!isCache && message.post && !message.out) { @@ -2380,17 +2616,21 @@ public class MessagesController implements NotificationCenter.NotificationCenter if (isMegagroup) { message.flags |= TLRPC.MESSAGE_FLAG_MEGAGROUP; } + if (message.action instanceof TLRPC.TL_messageActionChatDeleteUser) { TLRPC.User user = usersDict.get(message.action.user_id); if (user != null && user.bot) { message.reply_markup = new TLRPC.TL_replyKeyboardHide(); } - } else if (message.action instanceof TLRPC.TL_messageActionChatMigrateTo || message.action instanceof TLRPC.TL_messageActionChannelCreate) { + } + if (message.action instanceof TLRPC.TL_messageActionChatMigrateTo || message.action instanceof TLRPC.TL_messageActionChannelCreate) { message.unread = false; message.media_unread = false; + } else { + message.unread = (message.out ? outboxValue : inboxValue) < message.id; } } - MessagesStorage.getInstance().putMessages(messagesRes, dialog_id, load_type, max_id, important, createDialog); + MessagesStorage.getInstance().putMessages(messagesRes, dialog_id, load_type, max_id, createDialog); } final ArrayList objects = new ArrayList<>(); final ArrayList messagesToReload = new ArrayList<>(); @@ -2437,7 +2677,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter if (first_unread_final == Integer.MAX_VALUE) { first_unread_final = first_unread; } - NotificationCenter.getInstance().postNotificationName(NotificationCenter.messagesDidLoaded, dialog_id, count, objects, isCache, first_unread_final, last_message_id, unread_count, last_date, load_type, messagesRes.collapsed, isEnd, classGuid, loadIndex); + NotificationCenter.getInstance().postNotificationName(NotificationCenter.messagesDidLoaded, dialog_id, count, objects, isCache, first_unread_final, last_message_id, unread_count, last_date, load_type, isEnd, classGuid, loadIndex); if (!messagesToReload.isEmpty()) { reloadMessages(messagesToReload, dialog_id); } @@ -2464,13 +2704,13 @@ public class MessagesController implements NotificationCenter.NotificationCenter req.limit = count; boolean found = false; for (int a = dialogs.size() - 1; a >= 0; a--) { - TLRPC.Dialog dialog = dialogs.get(a); + TLRPC.TL_dialog dialog = dialogs.get(a); int lower_id = (int) dialog.id; int high_id = (int) (dialog.id >> 32); if (lower_id != 0 && high_id != 1 && dialog.top_message > 0) { MessageObject message = dialogMessage.get(dialog.id); if (message != null && message.getId() > 0) { - req.offset_date = Math.max(dialog.last_message_date_i, message.messageOwner.date); + req.offset_date = message.messageOwner.date; req.offset_id = message.messageOwner.id; int id; if (message.messageOwner.to_id.channel_id != 0) { @@ -2494,7 +2734,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter public void run(TLObject response, TLRPC.TL_error error) { if (error == null) { final TLRPC.messages_Dialogs dialogsRes = (TLRPC.messages_Dialogs) response; - processLoadedDialogs(dialogsRes, null, 0, count, false, false, false); + processLoadedDialogs(dialogsRes, null, 0, count, 0, false, false); } } }); @@ -2585,9 +2825,9 @@ public class MessagesController implements NotificationCenter.NotificationCenter } StringBuilder dids = new StringBuilder(dialogsRes.dialogs.size() * 12); - HashMap dialogHashMap = new HashMap<>(); + HashMap dialogHashMap = new HashMap<>(); for (int a = 0; a < dialogsRes.dialogs.size(); a++) { - TLRPC.Dialog dialog = dialogsRes.dialogs.get(a); + TLRPC.TL_dialog dialog = dialogsRes.dialogs.get(a); if (dialog.peer.channel_id != 0) { dialog.id = -dialog.peer.channel_id; } else if (dialog.peer.chat_id != 0) { @@ -2604,7 +2844,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter SQLiteCursor cursor = MessagesStorage.getInstance().getDatabase().queryFinalized(String.format(Locale.US, "SELECT did FROM dialogs WHERE did IN (%s)", dids.toString())); while (cursor.next()) { long did = cursor.longValue(0); - TLRPC.Dialog dialog = dialogHashMap.remove(did); + TLRPC.TL_dialog dialog = dialogHashMap.remove(did); if (dialog != null) { dialogsRes.dialogs.remove(dialog); for (int a = 0; a < dialogsRes.messages.size(); a++) { @@ -2617,10 +2857,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter if (message.id == dialog.top_message) { dialog.top_message = 0; } - if (message.id == dialog.top_not_important_message) { - dialog.top_not_important_message = 0; - } - if (dialog.top_message == 0 && dialog.top_not_important_message == 0) { + if (dialog.top_message == 0) { break; } } @@ -2636,7 +2873,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter offsetId = -1; dialogsRes.messages.remove(a); a--; - TLRPC.Dialog dialog = dialogHashMap.remove(MessageObject.getDialogId(message)); + TLRPC.TL_dialog dialog = dialogHashMap.remove(MessageObject.getDialogId(message)); if (dialog != null) { dialogsRes.dialogs.remove(dialog); } @@ -2645,7 +2882,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter } cursor.dispose(); - processLoadedDialogs(dialogsRes, null, offsetId, 0, false, false, true); + processLoadedDialogs(dialogsRes, null, offsetId, 0, 0, false, true); } catch (Exception e) { FileLog.e("tmessages", e); AndroidUtilities.runOnUIThread(new Runnable() { @@ -2667,15 +2904,14 @@ public class MessagesController implements NotificationCenter.NotificationCenter } } }); - } - public void processLoadedDialogs(final TLRPC.messages_Dialogs dialogsRes, final ArrayList encChats, final int offset, final int count, final boolean isCache, final boolean resetEnd, final boolean migrate) { + public void processLoadedDialogs(final TLRPC.messages_Dialogs dialogsRes, final ArrayList encChats, final int offset, final int count, final int loadType, final boolean resetEnd, final boolean migrate) { Utilities.stageQueue.postRunnable(new Runnable() { @Override public void run() { - FileLog.e("tmessages", "loaded from " + isCache + " count " + dialogsRes.dialogs.size()); - if (isCache && dialogsRes.dialogs.size() == 0) { + FileLog.e("tmessages", "loaded loadType " + loadType + " count " + dialogsRes.dialogs.size()); + if (loadType == 1 && dialogsRes.dialogs.size() == 0) { AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { @@ -2691,9 +2927,8 @@ public class MessagesController implements NotificationCenter.NotificationCenter return; } - final HashMap new_dialogs_dict = new HashMap<>(); + final HashMap new_dialogs_dict = new HashMap<>(); final HashMap new_dialogMessage = new HashMap<>(); - final HashMap notImportantDates = new HashMap<>(); final HashMap usersDict = new HashMap<>(); final HashMap chatsDict = new HashMap<>(); @@ -2705,18 +2940,15 @@ public class MessagesController implements NotificationCenter.NotificationCenter TLRPC.Chat c = dialogsRes.chats.get(a); chatsDict.put(c.id, c); } - if (isCache) { + if (loadType == 1) { nextDialogsCacheOffset = offset + count; } for (int a = 0; a < dialogsRes.messages.size(); a++) { TLRPC.Message message = dialogsRes.messages.get(a); if (message.to_id.channel_id != 0) { - if (!MessageObject.isImportant(message)) { - notImportantDates.put(-message.to_id.channel_id, message.date); - } TLRPC.Chat chat = chatsDict.get(message.to_id.channel_id); - if (chat != null && chat.left/* && !chat.megagroup*/) { + if (chat != null && chat.left) { continue; } if (chat != null && chat.megagroup) { @@ -2728,18 +2960,16 @@ public class MessagesController implements NotificationCenter.NotificationCenter continue; } } - if (!isCache && message.post && !message.out) { + if (loadType != 1 && message.post && !message.out) { message.media_unread = true; } MessageObject messageObject = new MessageObject(message, usersDict, chatsDict, false); - MessageObject currentMessage = new_dialogMessage.get(messageObject.getDialogId()); - if (currentMessage == null || messageObject.isMegagroup() || messageObject.isImportant()) { - new_dialogMessage.put(messageObject.getDialogId(), messageObject); - } + new_dialogMessage.put(messageObject.getDialogId(), messageObject); } + final ArrayList dialogsToReload = new ArrayList<>(); for (int a = 0; a < dialogsRes.dialogs.size(); a++) { - TLRPC.Dialog d = dialogsRes.dialogs.get(a); + TLRPC.TL_dialog d = dialogsRes.dialogs.get(a); if (d.id == 0 && d.peer != null) { if (d.peer.user_id != 0) { d.id = d.peer.user_id; @@ -2758,20 +2988,16 @@ public class MessagesController implements NotificationCenter.NotificationCenter d.last_message_date = mess.messageOwner.date; } } - if (d.last_message_date_i == 0 && d.top_not_important_message != 0) { - Integer date = notImportantDates.get((int) d.id); - if (date != null) { - d.last_message_date_i = date; - } - } - if (d instanceof TLRPC.TL_dialogChannel) { + boolean allowCheck = true; + if (DialogObject.isChannel(d)) { TLRPC.Chat chat = chatsDict.get(-(int) d.id); - if (chat != null && chat.megagroup) { - d.top_message = Math.max(d.top_message, d.top_not_important_message); - d.unread_count = Math.max(d.unread_count, d.unread_not_important_count); - } - if (chat != null && chat.left/* && !chat.megagroup*/) { - continue; + if (chat != null) { + if (!chat.megagroup) { + allowCheck = false; + } + if (chat.left) { + continue; + } } channelsPts.put(-(int) d.id, d.pts); } else if ((int) d.id < 0) { @@ -2782,14 +3008,24 @@ public class MessagesController implements NotificationCenter.NotificationCenter } new_dialogs_dict.put(d.id, d); + if (allowCheck && loadType == 1 && (d.read_outbox_max_id == 0 || d.read_inbox_max_id == 0) && d.top_message != 0) { + dialogsToReload.add(d); + } + Integer value = dialogs_read_inbox_max.get(d.id); if (value == null) { value = 0; } dialogs_read_inbox_max.put(d.id, Math.max(value, d.read_inbox_max_id)); + + value = dialogs_read_outbox_max.get(d.id); + if (value == null) { + value = 0; + } + dialogs_read_outbox_max.put(d.id, Math.max(value, d.read_outbox_max_id)); } - if (!isCache) { + if (loadType != 1) { ImageLoader.saveMessagesThumbs(dialogsRes.messages); for (int a = 0; a < dialogsRes.messages.size(); a++) { @@ -2799,22 +3035,40 @@ public class MessagesController implements NotificationCenter.NotificationCenter if (user != null && user.bot) { message.reply_markup = new TLRPC.TL_replyKeyboardHide(); } - } else if (message.action instanceof TLRPC.TL_messageActionChatMigrateTo || message.action instanceof TLRPC.TL_messageActionChannelCreate) { + } + + if (message.action instanceof TLRPC.TL_messageActionChatMigrateTo || message.action instanceof TLRPC.TL_messageActionChannelCreate) { message.unread = false; message.media_unread = false; + } else { + ConcurrentHashMap read_max = message.out ? dialogs_read_outbox_max : dialogs_read_inbox_max; + Integer value = read_max.get(message.dialog_id); + if (value == null) { + value = MessagesStorage.getInstance().getDialogReadMax(message.out, message.dialog_id); + read_max.put(message.dialog_id, value); + } + message.unread = value < message.id; } } MessagesStorage.getInstance().putDialogs(dialogsRes); } + if (loadType == 2) { + TLRPC.Chat chat = dialogsRes.chats.get(0); + getChannelDifference(chat.id); + checkChannelInviter(chat.id); + } AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { - if (!isCache) { + if (loadType != 1) { applyDialogsNotificationsSettings(dialogsRes.dialogs); + if (!UserConfig.draftsLoaded) { + DraftQuery.loadDrafts(); + } } - putUsers(dialogsRes.users, isCache); - putChats(dialogsRes.chats, isCache); + putUsers(dialogsRes.users, loadType == 1); + putChats(dialogsRes.chats, loadType == 1); if (encChats != null) { for (int a = 0; a < encChats.size(); a++) { TLRPC.EncryptedChat encryptedChat = encChats.get(a); @@ -2830,13 +3084,16 @@ public class MessagesController implements NotificationCenter.NotificationCenter boolean added = false; int lastDialogDate = migrate && !dialogs.isEmpty() ? dialogs.get(dialogs.size() - 1).last_message_date : 0; - for (HashMap.Entry pair : new_dialogs_dict.entrySet()) { + for (HashMap.Entry pair : new_dialogs_dict.entrySet()) { Long key = pair.getKey(); - TLRPC.Dialog value = pair.getValue(); + TLRPC.TL_dialog value = pair.getValue(); if (migrate && lastDialogDate != 0 && value.last_message_date < lastDialogDate) { continue; } - TLRPC.Dialog currentDialog = dialogs_dict.get(key); + TLRPC.TL_dialog currentDialog = dialogs_dict.get(key); + if (loadType != 1 && value.draft instanceof TLRPC.TL_draftMessage) { + DraftQuery.saveDraft(value.id, value.draft, null, false); + } if (currentDialog == null) { added = true; dialogs_dict.put(key, value); @@ -2849,7 +3106,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter } } } else { - if (!isCache) { + if (loadType != 1) { currentDialog.notify_settings = value.notify_settings; } MessageObject oldMsg = dialogMessage.get(key); @@ -2892,52 +3149,14 @@ public class MessagesController implements NotificationCenter.NotificationCenter } dialogs.clear(); - dialogsServerOnly.clear(); - dialogsGroupsOnly.clear(); dialogs.addAll(dialogs_dict.values()); - Collections.sort(dialogs, new Comparator() { - @Override - public int compare(TLRPC.Dialog tl_dialog, TLRPC.Dialog tl_dialog2) { - if (tl_dialog.last_message_date == tl_dialog2.last_message_date) { - return 0; - } else if (tl_dialog.last_message_date < tl_dialog2.last_message_date) { - return 1; - } else { - return -1; - } - } - }); - for (int a = 0; a < dialogs.size(); a++) { - TLRPC.Dialog d = dialogs.get(a); + sortDialogs(migrate ? chatsDict : null); - int high_id = (int) (d.id >> 32); - if ((int) d.id != 0 && high_id != 1) { - if (d instanceof TLRPC.TL_dialog) { - if (d.id < 0) { - if (migrate) { - TLRPC.Chat chat = chatsDict.get(-(int) d.id); - if (chat != null && chat.migrated_to != null) { - dialogs.remove(a); - a--; - continue; - } - } - dialogsGroupsOnly.add(d); - } - } else if (d instanceof TLRPC.TL_dialogChannel) { - int lower_id = (int) d.id; - TLRPC.Chat chat = getChat(-lower_id); - if (chat != null && (chat.megagroup && chat.editor || chat.creator)) { - dialogsGroupsOnly.add(d); - } - } - dialogsServerOnly.add(d); + if (loadType != 2) { + if (!migrate) { + dialogsEndReached = (dialogsRes.dialogs.size() == 0 || dialogsRes.dialogs.size() != count) && loadType == 0; } } - - if (!migrate) { - dialogsEndReached = (dialogsRes.dialogs.size() == 0 || dialogsRes.dialogs.size() != count) && !isCache; - } NotificationCenter.getInstance().postNotificationName(NotificationCenter.dialogsNeedReload); if (migrate) { @@ -2947,11 +3166,14 @@ public class MessagesController implements NotificationCenter.NotificationCenter NotificationCenter.getInstance().postNotificationName(NotificationCenter.needReloadRecentDialogsSearch); } else { generateUpdateMessage(); - if (!added && isCache) { + if (!added && loadType == 1) { loadDialogs(0, count, false); } } migrateDialogs(UserConfig.migrateOffsetId, UserConfig.migrateOffsetDate, UserConfig.migrateOffsetUserId, UserConfig.migrateOffsetChatId, UserConfig.migrateOffsetChannelId, UserConfig.migrateOffsetAccess); + if (!dialogsToReload.isEmpty()) { + reloadDialogsReadValue(dialogsToReload, 0); + } } }); } @@ -2964,7 +3186,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter int currentValue2 = preferences.getInt("notifyuntil_" + dialog_id, 0); SharedPreferences.Editor editor = preferences.edit(); boolean updated = false; - TLRPC.Dialog dialog = dialogs_dict.get(dialog_id); + TLRPC.TL_dialog dialog = dialogs_dict.get(dialog_id); if (dialog != null) { dialog.notify_settings = notify_settings; } @@ -3008,10 +3230,10 @@ public class MessagesController implements NotificationCenter.NotificationCenter } } - private void applyDialogsNotificationsSettings(ArrayList dialogs) { + private void applyDialogsNotificationsSettings(ArrayList dialogs) { SharedPreferences.Editor editor = null; for (int a = 0; a < dialogs.size(); a++) { - TLRPC.Dialog dialog = dialogs.get(a); + TLRPC.TL_dialog dialog = dialogs.get(a); if (dialog.peer != null && dialog.notify_settings instanceof TLRPC.TL_peerNotifySettings) { if (editor == null) { SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("Notifications", Activity.MODE_PRIVATE); @@ -3049,7 +3271,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter @Override public void run() { for (HashMap.Entry entry : dialogsToUpdate.entrySet()) { - TLRPC.Dialog currentDialog = dialogs_dict.get(entry.getKey()); + TLRPC.TL_dialog currentDialog = dialogs_dict.get(entry.getKey()); if (currentDialog != null) { currentDialog.unread_count = entry.getValue(); } @@ -3060,11 +3282,97 @@ public class MessagesController implements NotificationCenter.NotificationCenter }); } + protected void checkLastDialogMessage(final TLRPC.TL_dialog dialog, final TLRPC.InputPeer peer, long taskId) { + final int lower_id = (int) dialog.id; + if (lower_id == 0 || checkingLastMessagesDialogs.containsKey(lower_id)) { + return; + } + TLRPC.TL_messages_getHistory req = new TLRPC.TL_messages_getHistory(); + req.peer = peer == null ? getInputPeer(lower_id) : peer; + if (req.peer == null) { + return; + } + req.limit = 1; + checkingLastMessagesDialogs.put(lower_id, true); + + final long newTaskId; + if (taskId == 0) { + NativeByteBuffer data = null; + try { + data = new NativeByteBuffer(40 + peer.getObjectSize()); + data.writeInt32(2); + data.writeInt64(dialog.id); + data.writeInt32(dialog.top_message); + data.writeInt32(dialog.read_inbox_max_id); + data.writeInt32(dialog.read_outbox_max_id); + data.writeInt32(dialog.unread_count); + data.writeInt32(dialog.last_message_date); + data.writeInt32(dialog.pts); + data.writeInt32(dialog.flags); + peer.serializeToStream(data); + } catch (Exception e) { + FileLog.e("tmessages", e); + } + newTaskId = MessagesStorage.getInstance().createPendingTask(data); + } else { + newTaskId = taskId; + } + + ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { + @Override + public void run(TLObject response, TLRPC.TL_error error) { + if (response != null) { + TLRPC.messages_Messages res = (TLRPC.messages_Messages) response; + if (!res.messages.isEmpty()) { + TLRPC.TL_messages_dialogs dialogs = new TLRPC.TL_messages_dialogs(); + TLRPC.Message newMessage = res.messages.get(0); + TLRPC.TL_dialog newDialog = new TLRPC.TL_dialog(); + newDialog.flags = dialog.flags; + newDialog.top_message = newMessage.id; + newDialog.last_message_date = newMessage.date; + newDialog.notify_settings = dialog.notify_settings; + newDialog.pts = dialog.pts; + newDialog.unread_count = dialog.unread_count; + newDialog.read_inbox_max_id = dialog.read_inbox_max_id; + newDialog.read_outbox_max_id = dialog.read_outbox_max_id; + newMessage.dialog_id = newDialog.id = dialog.id; + dialogs.users.addAll(res.users); + dialogs.chats.addAll(res.chats); + dialogs.dialogs.add(newDialog); + dialogs.messages.addAll(res.messages); + dialogs.count = 1; + processDialogsUpdate(dialogs, null); + MessagesStorage.getInstance().putMessages(res.messages, true, true, false, MediaController.getInstance().getAutodownloadMask(), true); + } else { + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + TLRPC.TL_dialog currentDialog = dialogs_dict.get(dialog.id); + if (currentDialog != null && currentDialog.top_message == 0) { + deleteDialog(dialog.id, 3); + } + } + }); + } + } + if (newTaskId != 0) { + MessagesStorage.getInstance().removePendingTask(newTaskId); + } + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + checkingLastMessagesDialogs.remove(lower_id); + } + }); + } + }); + } + public void processDialogsUpdate(final TLRPC.messages_Dialogs dialogsRes, ArrayList encChats) { Utilities.stageQueue.postRunnable(new Runnable() { @Override public void run() { - final HashMap new_dialogs_dict = new HashMap<>(); + final HashMap new_dialogs_dict = new HashMap<>(); final HashMap new_dialogMessage = new HashMap<>(); final HashMap usersDict = new HashMap<>(); final HashMap chatsDict = new HashMap<>(); @@ -3083,7 +3391,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter TLRPC.Message message = dialogsRes.messages.get(a); if (message.to_id.channel_id != 0) { TLRPC.Chat chat = chatsDict.get(message.to_id.channel_id); - if (chat != null && chat.left/* && !chat.megagroup*/) { + if (chat != null && chat.left) { continue; } } else if (message.to_id.chat_id != 0) { @@ -3096,7 +3404,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter new_dialogMessage.put(messageObject.getDialogId(), messageObject); } for (int a = 0; a < dialogsRes.dialogs.size(); a++) { - TLRPC.Dialog d = dialogsRes.dialogs.get(a); + TLRPC.TL_dialog d = dialogsRes.dialogs.get(a); if (d.id == 0) { if (d.peer.user_id != 0) { d.id = d.peer.user_id; @@ -3106,9 +3414,9 @@ public class MessagesController implements NotificationCenter.NotificationCenter d.id = -d.peer.channel_id; } } - if (d instanceof TLRPC.TL_dialogChannel) { + if (DialogObject.isChannel(d)) { TLRPC.Chat chat = chatsDict.get(-(int) d.id); - if (chat != null && chat.left/* && !chat.megagroup*/) { + if (chat != null && chat.left) { continue; } } else if ((int) d.id < 0) { @@ -3131,6 +3439,12 @@ public class MessagesController implements NotificationCenter.NotificationCenter value = 0; } dialogs_read_inbox_max.put(d.id, Math.max(value, d.read_inbox_max_id)); + + value = dialogs_read_outbox_max.get(d.id); + if (value == null) { + value = 0; + } + dialogs_read_outbox_max.put(d.id, Math.max(value, d.read_outbox_max_id)); } AndroidUtilities.runOnUIThread(new Runnable() { @@ -3139,10 +3453,10 @@ public class MessagesController implements NotificationCenter.NotificationCenter putUsers(dialogsRes.users, true); putChats(dialogsRes.chats, true); - for (HashMap.Entry pair : new_dialogs_dict.entrySet()) { + for (HashMap.Entry pair : new_dialogs_dict.entrySet()) { Long key = pair.getKey(); - TLRPC.Dialog value = pair.getValue(); - TLRPC.Dialog currentDialog = dialogs_dict.get(key); + TLRPC.TL_dialog value = pair.getValue(); + TLRPC.TL_dialog currentDialog = dialogs_dict.get(key); if (currentDialog == null) { nextDialogsCacheOffset++; dialogs_dict.put(key, value); @@ -3174,6 +3488,9 @@ public class MessagesController implements NotificationCenter.NotificationCenter dialogMessagesByRandomIds.remove(oldMsg.messageOwner.random_id); } } + if (messageObject == null) { + checkLastDialogMessage(value, null, 0); + } } } else { MessageObject newMsg = new_dialogMessage.get(value.id); @@ -3196,39 +3513,8 @@ public class MessagesController implements NotificationCenter.NotificationCenter } dialogs.clear(); - dialogsServerOnly.clear(); - dialogsGroupsOnly.clear(); dialogs.addAll(dialogs_dict.values()); - Collections.sort(dialogs, new Comparator() { - @Override - public int compare(TLRPC.Dialog dialog, TLRPC.Dialog dialog2) { - if (dialog.last_message_date == dialog2.last_message_date) { - return 0; - } else if (dialog.last_message_date < dialog2.last_message_date) { - return 1; - } else { - return -1; - } - } - }); - for (int a = 0; a < dialogs.size(); a++) { - TLRPC.Dialog d = dialogs.get(a); - int high_id = (int) (d.id >> 32); - if ((int) d.id != 0 && high_id != 1) { - dialogsServerOnly.add(d); - if (d instanceof TLRPC.TL_dialog) { - if (d.id < 0) { - dialogsGroupsOnly.add(d); - } - } else if (d instanceof TLRPC.TL_dialogChannel) { - int lower_id = (int) d.id; - TLRPC.Chat chat = getChat(-lower_id); - if (chat != null && (chat.megagroup && chat.editor || chat.creator)) { - dialogsGroupsOnly.add(d); - } - } - } - } + sortDialogs(null); NotificationCenter.getInstance().postNotificationName(NotificationCenter.dialogsNeedReload); NotificationsController.getInstance().processDialogsUpdateRead(dialogsToUpdate); } @@ -3346,14 +3632,14 @@ public class MessagesController implements NotificationCenter.NotificationCenter } dialogs_read_inbox_max.put(dialog_id, Math.max(value, max_positive_id)); - MessagesStorage.getInstance().processPendingRead(dialog_id, messageId, max_date, false); + MessagesStorage.getInstance().processPendingRead(dialog_id, messageId, max_date); MessagesStorage.getInstance().getStorageQueue().postRunnable(new Runnable() { @Override public void run() { AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { - TLRPC.Dialog dialog = dialogs_dict.get(dialog_id); + TLRPC.TL_dialog dialog = dialogs_dict.get(dialog_id); if (dialog != null) { dialog.unread_count = 0; NotificationCenter.getInstance().postNotificationName(NotificationCenter.updateInterfaces, UPDATE_MASK_READ_DIALOG_MESSAGE); @@ -3375,12 +3661,10 @@ public class MessagesController implements NotificationCenter.NotificationCenter }); if (max_positive_id != Integer.MAX_VALUE) { - final long messageIdFinal = messageId; ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { @Override public void run(TLObject response, TLRPC.TL_error error) { if (error == null) { - MessagesStorage.getInstance().processPendingRead(dialog_id, messageIdFinal, max_date, true); if (response instanceof TLRPC.TL_messages_affectedMessages) { TLRPC.TL_messages_affectedMessages res = (TLRPC.TL_messages_affectedMessages) response; processNewDifferenceParams(-1, res.pts, -1, res.pts_count); @@ -3408,7 +3692,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter } }); } - MessagesStorage.getInstance().processPendingRead(dialog_id, max_id, max_date, false); + MessagesStorage.getInstance().processPendingRead(dialog_id, max_id, max_date); MessagesStorage.getInstance().getStorageQueue().postRunnable(new Runnable() { @Override @@ -3417,7 +3701,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter @Override public void run() { NotificationsController.getInstance().processReadMessages(null, dialog_id, max_date, 0, popup); - TLRPC.Dialog dialog = dialogs_dict.get(dialog_id); + TLRPC.TL_dialog dialog = dialogs_dict.get(dialog_id); if (dialog != null) { dialog.unread_count = 0; NotificationCenter.getInstance().postNotificationName(NotificationCenter.updateInterfaces, UPDATE_MASK_READ_DIALOG_MESSAGE); @@ -3687,26 +3971,6 @@ public class MessagesController implements NotificationCenter.NotificationCenter }, ConnectionsManager.RequestFlagInvokeAfter); } - public void toogleChannelComments(int chat_id, boolean enabled) { - TLRPC.TL_channels_toggleComments req = new TLRPC.TL_channels_toggleComments(); - req.channel = getInputChannel(chat_id); - req.enabled = enabled; - ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { - @Override - public void run(TLObject response, TLRPC.TL_error error) { - if (response != null) { - processUpdates((TLRPC.Updates) response, false); - AndroidUtilities.runOnUIThread(new Runnable() { - @Override - public void run() { - NotificationCenter.getInstance().postNotificationName(NotificationCenter.updateInterfaces, UPDATE_MASK_CHANNEL); - } - }); - } - } - }, ConnectionsManager.RequestFlagInvokeAfter); - } - public void toogleChannelSignatures(int chat_id, boolean enabled) { TLRPC.TL_channels_toggleSignatures req = new TLRPC.TL_channels_toggleSignatures(); req.channel = getInputChannel(chat_id); @@ -4147,16 +4411,16 @@ public class MessagesController implements NotificationCenter.NotificationCenter ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { @Override public void run(TLObject response, TLRPC.TL_error error) { - ConnectionsManager.getInstance().cleanUp(); + ConnectionsManager.getInstance().cleanup(); } }); } else { - ConnectionsManager.getInstance().cleanUp(); + ConnectionsManager.getInstance().cleanup(); } UserConfig.clearConfig(); NotificationCenter.getInstance().postNotificationName(NotificationCenter.appDidLogout); - MessagesStorage.getInstance().cleanUp(false); - cleanUp(); + MessagesStorage.getInstance().cleanup(false); + cleanup(); ContactsController.getInstance().deleteAllAppAccounts(); } @@ -4165,23 +4429,6 @@ public class MessagesController implements NotificationCenter.NotificationCenter return; } TLRPC.TL_help_getAppChangelog req = new TLRPC.TL_help_getAppChangelog(); - req.app_version = BuildVars.BUILD_VERSION_STRING; - try { - req.lang_code = LocaleController.getLocaleStringIso639(); - req.device_model = Build.MANUFACTURER + Build.MODEL; - req.system_version = "SDK " + Build.VERSION.SDK_INT; - } catch (Exception e) { - FileLog.e("tmessages", e); - } - if (req.lang_code == null || req.lang_code.trim().length() == 0) { - req.lang_code = "en"; - } - if (req.device_model == null || req.device_model.trim().length() == 0) { - req.device_model = "Android unknown"; - } - if (req.system_version == null || req.system_version.trim().length() == 0) { - req.system_version = "SDK Unknown"; - } ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { @Override public void run(TLObject response, TLRPC.TL_error error) { @@ -4197,7 +4444,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter update.popup = false; ArrayList updates = new ArrayList<>(); updates.add(update); - processUpdateArray(updates, null, null); + processUpdateArray(updates, null, null, false); } } }); @@ -4214,37 +4461,6 @@ public class MessagesController implements NotificationCenter.NotificationCenter TLRPC.TL_account_registerDevice req = new TLRPC.TL_account_registerDevice(); req.token_type = 2; req.token = regid; - req.app_sandbox = false; - try { - req.lang_code = LocaleController.getLocaleStringIso639(); - if (req.lang_code.length() == 0) { - req.lang_code = "en"; - } - req.device_model = Build.MANUFACTURER + Build.MODEL; - req.system_version = "SDK " + Build.VERSION.SDK_INT; - PackageInfo pInfo = ApplicationLoader.applicationContext.getPackageManager().getPackageInfo(ApplicationLoader.applicationContext.getPackageName(), 0); - req.app_version = pInfo.versionName + " (" + pInfo.versionCode + ")"; - } catch (Exception e) { - FileLog.e("tmessages", e); - req.lang_code = "en"; - req.device_model = "Android unknown"; - req.system_version = "SDK " + Build.VERSION.SDK_INT; - req.app_version = "App version unknown"; - } - - if (req.lang_code == null || req.lang_code.trim().length() == 0) { - req.lang_code = "en"; - } - if (req.device_model == null || req.device_model.trim().length() == 0) { - req.device_model = "Android unknown"; - } - if (req.app_version == null || req.app_version.trim().length() == 0) { - req.app_version = "App version unknown"; - } - if (req.system_version == null || req.system_version.trim().length() == 0) { - req.system_version = "SDK Unknown"; - } - ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { @Override public void run(TLObject response, TLRPC.TL_error error) { @@ -4484,6 +4700,53 @@ public class MessagesController implements NotificationCenter.NotificationCenter setUpdatesStartTime(type, 0); } + protected void loadUnknownChannel(final TLRPC.Chat channel, final long taskId) { + if (!(channel instanceof TLRPC.TL_channel) || gettingUnknownChannels.containsKey(channel.id)) { + return; + } + gettingUnknownChannels.put(channel.id, true); + TLRPC.TL_inputPeerChannel inputPeer = new TLRPC.TL_inputPeerChannel(); + inputPeer.channel_id = channel.id; + inputPeer.access_hash = channel.access_hash; + TLRPC.TL_messages_getPeerDialogs req = new TLRPC.TL_messages_getPeerDialogs(); + req.peers.add(inputPeer); + final long newTaskId; + if (taskId == 0) { + NativeByteBuffer data = null; + try { + data = new NativeByteBuffer(4 + channel.getObjectSize()); + data.writeInt32(0); + channel.serializeToStream(data); + + } catch (Exception e) { + FileLog.e("tmessages", e); + } + newTaskId = MessagesStorage.getInstance().createPendingTask(data); + } else { + newTaskId = taskId; + } + ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { + @Override + public void run(TLObject response, TLRPC.TL_error error) { + if (response != null) { + TLRPC.TL_messages_peerDialogs res = (TLRPC.TL_messages_peerDialogs) response; + if (!res.dialogs.isEmpty() && !res.chats.isEmpty()) { + TLRPC.TL_messages_dialogs dialogs = new TLRPC.TL_messages_dialogs(); + dialogs.dialogs.addAll(res.dialogs); + dialogs.messages.addAll(res.messages); + dialogs.users.addAll(res.users); + dialogs.chats.addAll(res.chats); + processLoadedDialogs(dialogs, null, 0, 1, 2, false, false); + } + } + if (newTaskId != 0) { + MessagesStorage.getInstance().removePendingTask(newTaskId); + } + gettingUnknownChannels.remove(channel.id); + } + }); + } + public void startShortPoll(final int channelId, final boolean stop) { Utilities.stageQueue.postRunnable(new Runnable() { @Override @@ -4493,18 +4756,18 @@ public class MessagesController implements NotificationCenter.NotificationCenter } else { needShortPollChannels.put(channelId, 0); if (shortPollChannels.indexOfKey(channelId) < 0) { - getChannelDifference(channelId, 2); + getChannelDifference(channelId, 2, 0); } } } }); } - public void getChannelDifference(final int channelId) { - getChannelDifference(channelId, 0); + private void getChannelDifference(final int channelId) { + getChannelDifference(channelId, 0, 0); } - public void getChannelDifference(final int channelId, final int newDialogType) { + protected void getChannelDifference(final int channelId, final int newDialogType, final long taskId) { Boolean gettingDifferenceChannel = gettingDifferenceChannels.get(channelId); if (gettingDifferenceChannel == null) { gettingDifferenceChannel = false; @@ -4536,6 +4799,23 @@ public class MessagesController implements NotificationCenter.NotificationCenter return; } } + + final long newTaskId; + if (taskId == 0) { + NativeByteBuffer data = null; + try { + data = new NativeByteBuffer(12); + data.writeInt32(1); + data.writeInt32(channelId); + data.writeInt32(newDialogType); + } catch (Exception e) { + FileLog.e("tmessages", e); + } + newTaskId = MessagesStorage.getInstance().createPendingTask(data); + } else { + newTaskId = taskId; + } + gettingDifferenceChannels.put(channelId, true); TLRPC.TL_updates_getChannelDifference req = new TLRPC.TL_updates_getChannelDifference(); req.channel = getInputChannel(channelId); @@ -4546,7 +4826,6 @@ public class MessagesController implements NotificationCenter.NotificationCenter ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { @Override public void run(TLObject response, final TLRPC.TL_error error) { - if (error == null) { final TLRPC.updates_ChannelDifference res = (TLRPC.updates_ChannelDifference) response; @@ -4623,35 +4902,28 @@ public class MessagesController implements NotificationCenter.NotificationCenter ImageLoader.saveMessagesThumbs(res.new_messages); final ArrayList pushMessages = new ArrayList<>(); + long dialog_id = -channelId; + Integer inboxValue = dialogs_read_inbox_max.get(dialog_id); + if (inboxValue == null) { + inboxValue = MessagesStorage.getInstance().getDialogReadMax(true, dialog_id); + dialogs_read_inbox_max.put(dialog_id, inboxValue); + } + + Integer outboxValue = dialogs_read_outbox_max.get(dialog_id); + if (outboxValue == null) { + outboxValue = MessagesStorage.getInstance().getDialogReadMax(true, dialog_id); + dialogs_read_outbox_max.put(dialog_id, outboxValue); + } + for (int a = 0; a < res.new_messages.size(); a++) { TLRPC.Message message = res.new_messages.get(a); - if (!message.out) { - message.unread = true; - if (message.post || channelFinal != null && channelFinal.megagroup) { - message.media_unread = true; - } - } - if (message.action instanceof TLRPC.TL_messageActionChannelCreate) { - message.unread = false; - message.media_unread = false; - } + message.unread = !(channelFinal != null && channelFinal.left || (message.out ? outboxValue : inboxValue) >= message.id || message.action instanceof TLRPC.TL_messageActionChannelCreate); if (channelFinal != null && channelFinal.megagroup) { message.flags |= TLRPC.MESSAGE_FLAG_MEGAGROUP; } - long dialog_id = -channelId; - Integer value = dialogs_read_inbox_max.get(dialog_id); - if (value == null) { - value = MessagesStorage.getInstance().getDialogReadInboxMax(dialog_id); - } - MessageObject obj = new MessageObject(message, usersDict, createdDialogIds.contains(dialog_id)); - if (channelFinal != null && channelFinal.left || value >= obj.getId()) { - obj.setIsRead(); - obj.setContentIsRead(); - } - - if (!obj.isOut() && obj.isContentUnread()) { + if (!obj.isOut() && obj.isUnread()) { pushMessages.add(obj); } @@ -4692,43 +4964,33 @@ public class MessagesController implements NotificationCenter.NotificationCenter } if (!res.other_updates.isEmpty()) { - processUpdateArray(res.other_updates, res.users, res.chats); + processUpdateArray(res.other_updates, res.users, res.chats, true); } processChannelsUpdatesQueue(channelId, 1); MessagesStorage.getInstance().saveChannelPts(channelId, res.pts); } else if (res instanceof TLRPC.TL_updates_channelDifferenceTooLong) { long dialog_id = -channelId; - Integer value = dialogs_read_inbox_max.get(dialog_id); - if (value == null) { - value = MessagesStorage.getInstance().getDialogReadInboxMax(dialog_id); + + Integer inboxValue = dialogs_read_inbox_max.get(dialog_id); + if (inboxValue == null) { + inboxValue = MessagesStorage.getInstance().getDialogReadMax(true, dialog_id); + dialogs_read_inbox_max.put(dialog_id, inboxValue); } - value = Math.max(value, res.read_inbox_max_id); - dialogs_read_inbox_max.put(dialog_id, value); + + Integer outboxValue = dialogs_read_outbox_max.get(dialog_id); + if (outboxValue == null) { + outboxValue = MessagesStorage.getInstance().getDialogReadMax(true, dialog_id); + dialogs_read_outbox_max.put(dialog_id, outboxValue); + } + for (int a = 0; a < res.messages.size(); a++) { TLRPC.Message message = res.messages.get(a); message.dialog_id = -channelId; - if (!message.out) { - message.unread = true; - if (message.post || channelFinal != null && channelFinal.megagroup) { - message.media_unread = true; - } - } - if (message.action instanceof TLRPC.TL_messageActionChannelCreate) { - message.unread = false; - message.media_unread = false; - } - if (channelFinal != null && channelFinal.left || value >= message.id) { - message.unread = false; - message.media_unread = false; - } + message.unread = !(message.action instanceof TLRPC.TL_messageActionChannelCreate || channelFinal != null && channelFinal.left || (message.out ? outboxValue : inboxValue) >= message.id); if (channelFinal != null && channelFinal.megagroup) { message.flags |= TLRPC.MESSAGE_FLAG_MEGAGROUP; } } - if (channelFinal != null && channelFinal.megagroup) { - res.unread_important_count = Math.max(res.unread_count, res.unread_important_count); - res.top_important_message = Math.max(res.top_important_message, res.top_message); - } MessagesStorage.getInstance().overwriteChannel(channelId, (TLRPC.TL_updates_channelDifferenceTooLong) res, newDialogType); } gettingDifferenceChannels.remove(channelId); @@ -4742,6 +5004,10 @@ public class MessagesController implements NotificationCenter.NotificationCenter } FileLog.e("tmessages", "received channel difference with pts = " + res.pts + " channelId = " + channelId); FileLog.e("tmessages", "new_messages = " + res.new_messages.size() + " messages = " + res.messages.size() + " users = " + res.users.size() + " chats = " + res.chats.size() + " other updates = " + res.other_updates.size()); + + if (newTaskId != 0) { + MessagesStorage.getInstance().removePendingTask(newTaskId); + } } }); } @@ -4754,6 +5020,9 @@ public class MessagesController implements NotificationCenter.NotificationCenter } }); gettingDifferenceChannels.remove(channelId); + if (newTaskId != 0) { + MessagesStorage.getInstance().removePendingTask(newTaskId); + } } } }); @@ -4895,35 +5164,42 @@ public class MessagesController implements NotificationCenter.NotificationCenter if (user != null && user.bot) { message.reply_markup = new TLRPC.TL_replyKeyboardHide(); } - } else if (message.action instanceof TLRPC.TL_messageActionChatMigrateTo || message.action instanceof TLRPC.TL_messageActionChannelCreate) { - message.unread = false; - message.media_unread = false; } - long uid; - if (message.dialog_id != 0) { - uid = message.dialog_id; - } else { + if (message.dialog_id == 0) { if (message.to_id.chat_id != 0) { - uid = -message.to_id.chat_id; + message.dialog_id = -message.to_id.chat_id; } else { if (message.to_id.user_id == UserConfig.getClientUserId()) { message.to_id.user_id = message.from_id; } - uid = message.to_id.user_id; + message.dialog_id = message.to_id.user_id; } } - MessageObject obj = new MessageObject(message, usersDict, chatsDict, createdDialogIds.contains(uid)); + if (message.action instanceof TLRPC.TL_messageActionChatMigrateTo || message.action instanceof TLRPC.TL_messageActionChannelCreate) { + message.unread = false; + message.media_unread = false; + } else { + ConcurrentHashMap read_max = message.out ? dialogs_read_outbox_max : dialogs_read_inbox_max; + Integer value = read_max.get(message.dialog_id); + if (value == null) { + value = MessagesStorage.getInstance().getDialogReadMax(message.out, message.dialog_id); + read_max.put(message.dialog_id, value); + } + message.unread = value < message.id; + } + + MessageObject obj = new MessageObject(message, usersDict, chatsDict, createdDialogIds.contains(message.dialog_id)); if (!obj.isOut() && obj.isUnread()) { pushMessages.add(obj); } - ArrayList arr = messages.get(uid); + ArrayList arr = messages.get(message.dialog_id); if (arr == null) { arr = new ArrayList<>(); - messages.put(uid, arr); + messages.put(message.dialog_id, arr); } arr.add(obj); } @@ -4958,7 +5234,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter } if (!res.other_updates.isEmpty()) { - processUpdateArray(res.other_updates, res.users, res.chats); + processUpdateArray(res.other_updates, res.users, res.chats, true); } if (res instanceof TLRPC.TL_updates_difference) { @@ -5152,6 +5428,16 @@ public class MessagesController implements NotificationCenter.NotificationCenter } } + private int getUpdateChannelId(TLRPC.Update update) { + if (update instanceof TLRPC.TL_updateNewChannelMessage) { + return ((TLRPC.TL_updateNewChannelMessage) update).message.to_id.channel_id; + } else if (update instanceof TLRPC.TL_updateEditChannelMessage) { + return ((TLRPC.TL_updateEditChannelMessage) update).message.to_id.channel_id; + } else { + return update.channel_id; + } + } + public void processUpdates(final TLRPC.Updates updates, boolean fromQueue) { ArrayList needGetChannelsDiff = null; boolean needGetDiff = false; @@ -5160,7 +5446,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter if (updates instanceof TLRPC.TL_updateShort) { ArrayList arr = new ArrayList<>(); arr.add(updates.update); - processUpdateArray(arr, null, null); + processUpdateArray(arr, null, null, false); } else if (updates instanceof TLRPC.TL_updateShortChatMessage || updates instanceof TLRPC.TL_updateShortMessage) { final int user_id = updates instanceof TLRPC.TL_updateShortChatMessage ? updates.from_id : updates.user_id; TLRPC.User user = getUser(user_id); @@ -5263,10 +5549,10 @@ public class MessagesController implements NotificationCenter.NotificationCenter message.to_id.chat_id = updates.chat_id; message.dialog_id = -updates.chat_id; } + message.fwd_from = updates.fwd_from; message.silent = updates.silent; message.out = updates.out; - message.unread = updates.unread; message.mentioned = updates.mentioned; message.media_unread = updates.media_unread; message.entities = updates.entities; @@ -5276,6 +5562,15 @@ public class MessagesController implements NotificationCenter.NotificationCenter message.flags = updates.flags | TLRPC.MESSAGE_FLAG_HAS_FROM_ID; message.reply_to_msg_id = updates.reply_to_msg_id; message.media = new TLRPC.TL_messageMediaEmpty(); + + ConcurrentHashMap read_max = message.out ? dialogs_read_outbox_max : dialogs_read_inbox_max; + Integer value = read_max.get(message.dialog_id); + if (value == null) { + value = MessagesStorage.getInstance().getDialogReadMax(message.out, message.dialog_id); + read_max.put(message.dialog_id, value); + } + message.unread = value < message.id; + MessagesStorage.lastPtsValue = updates.pts; final MessageObject obj = new MessageObject(message, null, createdDialogIds.contains(message.dialog_id)); final ArrayList objArr = new ArrayList<>(); @@ -5314,20 +5609,19 @@ public class MessagesController implements NotificationCenter.NotificationCenter } }); } - - MessagesStorage.getInstance().getStorageQueue().postRunnable(new Runnable() { - @Override - public void run() { - AndroidUtilities.runOnUIThread(new Runnable() { - @Override - public void run() { - if (!obj.isOut()) { + if (!obj.isOut()) { + MessagesStorage.getInstance().getStorageQueue().postRunnable(new Runnable() { + @Override + public void run() { + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { NotificationsController.getInstance().processNewMessages(objArr, true); } - } - }); - } - }); + }); + } + }); + } MessagesStorage.getInstance().putMessages(arr, false, true, false, 0); } else if (MessagesStorage.lastPtsValue != updates.pts) { FileLog.e("tmessages", "need get diff short message, pts: " + MessagesStorage.lastPtsValue + " " + updates.pts + " count = " + updates.pts_count); @@ -5346,20 +5640,22 @@ public class MessagesController implements NotificationCenter.NotificationCenter HashMap minChannels = null; for (int a = 0; a < updates.chats.size(); a++) { TLRPC.Chat chat = updates.chats.get(a); - if (chat instanceof TLRPC.TL_channel && chat.min) { - TLRPC.Chat existChat = getChat(chat.id); - if (existChat == null || existChat.min) { - TLRPC.Chat cacheChat = MessagesStorage.getInstance().getChatSync(updates.chat_id); - if (existChat == null) { - putChat(cacheChat, true); + if (chat instanceof TLRPC.TL_channel) { + if (chat.min) { + TLRPC.Chat existChat = getChat(chat.id); + if (existChat == null || existChat.min) { + TLRPC.Chat cacheChat = MessagesStorage.getInstance().getChatSync(updates.chat_id); + if (existChat == null) { + putChat(cacheChat, true); + } + existChat = cacheChat; } - existChat = cacheChat; - } - if (existChat == null || existChat.min) { - if (minChannels == null) { - minChannels = new HashMap<>(); + if (existChat == null || existChat.min) { + if (minChannels == null) { + minChannels = new HashMap<>(); + } + minChannels.put(chat.id, chat); } - minChannels.put(chat.id, chat); } } } @@ -5378,21 +5674,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter } if (!needGetDiff) { MessagesStorage.getInstance().putUsersAndChats(updates.users, updates.chats, true, true); - Collections.sort(updates.updates, new Comparator() { - @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 || ltype == 2) { - return AndroidUtilities.compare(lhs.pts, rhs.pts); - } else if (ltype == 1) { - return AndroidUtilities.compare(lhs.qts, rhs.qts); - } - return 0; - } - }); + Collections.sort(updates.updates, updatesComparator); for (int a = 0; a < updates.updates.size(); a++) { TLRPC.Update update = updates.updates.get(a); if (getUpdateType(update) == 0) { @@ -5413,7 +5695,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter } } if (MessagesStorage.lastPtsValue + updatesNew.pts_count == updatesNew.pts) { - if (!processUpdateArray(updatesNew.updates, updates.users, updates.chats)) { + if (!processUpdateArray(updatesNew.updates, updates.users, updates.chats, false)) { FileLog.e("tmessages", "need get diff inner TL_updates, seq: " + MessagesStorage.lastSeqValue + " " + updates.seq); needGetDiff = true; } else { @@ -5447,7 +5729,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter } } if (MessagesStorage.lastQtsValue == 0 || MessagesStorage.lastQtsValue + updatesNew.updates.size() == updatesNew.pts) { - processUpdateArray(updatesNew.updates, updates.users, updates.chats); + processUpdateArray(updatesNew.updates, updates.users, updates.chats, false); MessagesStorage.lastQtsValue = updatesNew.pts; needReceivedQueue = true; } else if (MessagesStorage.lastPtsValue != updatesNew.pts) { @@ -5463,21 +5745,23 @@ public class MessagesController implements NotificationCenter.NotificationCenter } } } else if (getUpdateType(update) == 2) { - int channelId; - if (update instanceof TLRPC.TL_updateNewChannelMessage) { - channelId = ((TLRPC.TL_updateNewChannelMessage) update).message.to_id.channel_id; - } else if (update instanceof TLRPC.TL_updateEditChannelMessage) { - channelId = ((TLRPC.TL_updateEditChannelMessage) update).message.to_id.channel_id; - } else { - channelId = update.channel_id; - } + int channelId = getUpdateChannelId(update); + boolean skipUpdate = false; Integer channelPts = channelsPts.get(channelId); if (channelPts == null) { channelPts = MessagesStorage.getInstance().getChannelPtsSync(channelId); if (channelPts == 0) { - channelPts = update.pts - update.pts_count; + for (int c = 0; c < updates.chats.size(); c++) { + TLRPC.Chat chat = updates.chats.get(c); + if (chat.id == channelId) { + loadUnknownChannel(chat, 0); + skipUpdate = true; + break; + } + } + } else { + channelsPts.put(channelId, channelPts); } - channelsPts.put(channelId, channelPts); } TLRPC.TL_updates updatesNew = new TLRPC.TL_updates(); updatesNew.updates.add(update); @@ -5485,7 +5769,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter updatesNew.pts_count = update.pts_count; for (int b = a + 1; b < updates.updates.size(); b++) { TLRPC.Update update2 = updates.updates.get(b); - if (getUpdateType(update2) == 2 && updatesNew.pts + update2.pts_count == update2.pts) { + if (getUpdateType(update2) == 2 && channelId == getUpdateChannelId(update2) && updatesNew.pts + update2.pts_count == update2.pts) { updatesNew.updates.add(update2); updatesNew.pts = update2.pts; updatesNew.pts_count += update2.pts_count; @@ -5495,43 +5779,47 @@ public class MessagesController implements NotificationCenter.NotificationCenter break; } } - if (channelPts + updatesNew.pts_count == updatesNew.pts) { - if (!processUpdateArray(updatesNew.updates, updates.users, updates.chats)) { - FileLog.e("tmessages", "need get channel diff inner TL_updates, channel_id = " + channelId); - if (needGetChannelsDiff == null) { - needGetChannelsDiff = new ArrayList<>(); - } else if (!needGetChannelsDiff.contains(channelId)) { - needGetChannelsDiff.add(channelId); + if (!skipUpdate) { + if (channelPts + updatesNew.pts_count == updatesNew.pts) { + if (!processUpdateArray(updatesNew.updates, updates.users, updates.chats, false)) { + FileLog.e("tmessages", "need get channel diff inner TL_updates, channel_id = " + channelId); + if (needGetChannelsDiff == null) { + needGetChannelsDiff = new ArrayList<>(); + } else if (!needGetChannelsDiff.contains(channelId)) { + needGetChannelsDiff.add(channelId); + } + } else { + channelsPts.put(channelId, updatesNew.pts); + MessagesStorage.getInstance().saveChannelPts(channelId, updatesNew.pts); } - } else { - channelsPts.put(channelId, updatesNew.pts); - MessagesStorage.getInstance().saveChannelPts(channelId, updatesNew.pts); - } - } else if (channelPts != updatesNew.pts) { - FileLog.e("tmessages", update + " need get channel diff, pts: " + channelPts + " " + updatesNew.pts + " count = " + updatesNew.pts_count + " channelId = " + channelId); - Long updatesStartWaitTime = updatesStartWaitTimeChannels.get(channelId); - Boolean gettingDifferenceChannel = gettingDifferenceChannels.get(channelId); - if (gettingDifferenceChannel == null) { - gettingDifferenceChannel = false; - } - if (gettingDifferenceChannel || updatesStartWaitTime == null || Math.abs(System.currentTimeMillis() - updatesStartWaitTime) <= 1500) { - if (updatesStartWaitTime == null) { - updatesStartWaitTimeChannels.put(channelId, System.currentTimeMillis()); + } else if (channelPts != updatesNew.pts) { + FileLog.e("tmessages", update + " need get channel diff, pts: " + channelPts + " " + updatesNew.pts + " count = " + updatesNew.pts_count + " channelId = " + channelId); + Long updatesStartWaitTime = updatesStartWaitTimeChannels.get(channelId); + Boolean gettingDifferenceChannel = gettingDifferenceChannels.get(channelId); + if (gettingDifferenceChannel == null) { + gettingDifferenceChannel = false; } - FileLog.e("tmessages", "add to queue"); - ArrayList arrayList = updatesQueueChannels.get(channelId); - if (arrayList == null) { - arrayList = new ArrayList<>(); - updatesQueueChannels.put(channelId, arrayList); - } - arrayList.add(updatesNew); - } else { - if (needGetChannelsDiff == null) { - needGetChannelsDiff = new ArrayList<>(); - } else if (!needGetChannelsDiff.contains(channelId)) { - needGetChannelsDiff.add(channelId); + if (gettingDifferenceChannel || updatesStartWaitTime == null || Math.abs(System.currentTimeMillis() - updatesStartWaitTime) <= 1500) { + if (updatesStartWaitTime == null) { + updatesStartWaitTimeChannels.put(channelId, System.currentTimeMillis()); + } + FileLog.e("tmessages", "add to queue"); + ArrayList arrayList = updatesQueueChannels.get(channelId); + if (arrayList == null) { + arrayList = new ArrayList<>(); + updatesQueueChannels.put(channelId, arrayList); + } + arrayList.add(updatesNew); + } else { + if (needGetChannelsDiff == null) { + needGetChannelsDiff = new ArrayList<>(); + } else if (!needGetChannelsDiff.contains(channelId)) { + needGetChannelsDiff.add(channelId); + } } } + } else { + FileLog.e("tmessages", "need load unknown channel = " + channelId); } } else { break; @@ -5547,7 +5835,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter processUpdate = MessagesStorage.lastSeqValue + 1 == updates.seq || updates.seq == 0 || updates.seq == MessagesStorage.lastSeqValue; } if (processUpdate) { - processUpdateArray(updates.updates, updates.users, updates.chats); + processUpdateArray(updates.updates, updates.users, updates.chats, false); if (updates.date != 0) { MessagesStorage.lastDateValue = updates.date; } @@ -5625,7 +5913,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter MessagesStorage.getInstance().saveDiffParams(MessagesStorage.lastSeqValue, MessagesStorage.lastPtsValue, MessagesStorage.lastDateValue, MessagesStorage.lastQtsValue); } - public boolean processUpdateArray(ArrayList updates, final ArrayList usersArr, final ArrayList chatsArr) { + public boolean processUpdateArray(ArrayList updates, final ArrayList usersArr, final ArrayList chatsArr, boolean fromGetDifference) { if (updates.isEmpty()) { if (usersArr != null || chatsArr != null) { AndroidUtilities.runOnUIThread(new Runnable() { @@ -5646,9 +5934,8 @@ public class MessagesController implements NotificationCenter.NotificationCenter final ArrayList messagesArr = new ArrayList<>(); final HashMap> editingMessages = new HashMap<>(); final SparseArray channelViews = new SparseArray<>(); - final SparseArray> channelsGroups = new SparseArray<>(); final SparseArray markAsReadMessagesInbox = new SparseArray<>(); - final SparseIntArray markAsReadMessagesOutbox = new SparseIntArray(); + final SparseArray markAsReadMessagesOutbox = new SparseArray<>(); final ArrayList markAsReadMessages = new ArrayList<>(); final HashMap markAsReadEncrypted = new HashMap<>(); final SparseArray> deletedMessages = new SparseArray<>(); @@ -5681,6 +5968,9 @@ public class MessagesController implements NotificationCenter.NotificationCenter checkForUsers = false; chatsDict = chats; } + if (fromGetDifference) { + checkForUsers = false; + } if (usersArr != null || chatsArr != null) { AndroidUtilities.runOnUIThread(new Runnable() { @@ -5702,7 +5992,10 @@ public class MessagesController implements NotificationCenter.NotificationCenter message = ((TLRPC.TL_updateNewMessage) update).message; } else { message = ((TLRPC.TL_updateNewChannelMessage) update).message; - if (!message.out && message.from_id == UserConfig.getClientUserId()) { //TODO remove later + if (BuildVars.DEBUG_VERSION) { + FileLog.d("tmessages", update + " channelId = " + message.to_id.channel_id); + } + if (!message.out && message.from_id == UserConfig.getClientUserId()) { message.out = true; } } @@ -5727,6 +6020,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter putChat(chat, true); } if (chat == null) { + FileLog.d("tmessages", "not found chat " + chat_id); return false; } if (chat.megagroup) { @@ -5736,9 +6030,13 @@ public class MessagesController implements NotificationCenter.NotificationCenter int count = 3 + message.entities.size(); for (int a = 0; a < count; a++) { + boolean allowMin = false; if (a != 0) { if (a == 1) { user_id = message.from_id; + if (message.post) { + allowMin = true; + } } else if (a == 2) { user_id = message.fwd_from != null ? message.fwd_from.from_id : 0; } else { @@ -5748,17 +6046,18 @@ public class MessagesController implements NotificationCenter.NotificationCenter } if (user_id > 0) { TLRPC.User user = usersDict.get(user_id); - if (user == null || user.min) { + if (user == null || !allowMin && user.min) { user = getUser(user_id); } - if (user == null || user.min) { + if (user == null || !allowMin && user.min) { user = MessagesStorage.getInstance().getUserSync(user_id); - if (user != null && user.min) { + if (user != null && !allowMin && user.min) { user = null; } putUser(user, true); } if (user == null) { + FileLog.d("tmessages", "not found user " + user_id); return false; } if (a == 1 && user.status != null && user.status.expires <= 0) { @@ -5768,6 +6067,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter } } } + if (message.action instanceof TLRPC.TL_messageActionChatDeleteUser) { TLRPC.User user = usersDict.get(message.action.user_id); if (user != null && user.bot) { @@ -5775,28 +6075,8 @@ public class MessagesController implements NotificationCenter.NotificationCenter } else if (message.from_id == UserConfig.getClientUserId() && message.action.user_id == UserConfig.getClientUserId()) { continue; } - } else if (message.action instanceof TLRPC.TL_messageActionChatMigrateTo || message.action instanceof TLRPC.TL_messageActionChannelCreate) { - message.unread = false; - message.media_unread = false; } - if (update instanceof TLRPC.TL_updateNewChannelMessage) { - if (message.to_id.channel_id != 0 && !message.out) { - message.unread = true; - if (message.post || (message.flags & TLRPC.MESSAGE_FLAG_MEGAGROUP) != 0) { - message.media_unread = true; - } - } - long dialog_id = -message.to_id.channel_id; - Integer value = dialogs_read_inbox_max.get(dialog_id); - if (value == null) { - value = MessagesStorage.getInstance().getDialogReadInboxMax(dialog_id); - } - if (value >= message.id || ChatObject.isNotInChat(chat)) { - message.unread = false; - message.media_unread = false; - } - } messagesArr.add(message); ImageLoader.saveMessageThumbs(message); if (message.to_id.chat_id != 0) { @@ -5809,6 +6089,15 @@ public class MessagesController implements NotificationCenter.NotificationCenter } message.dialog_id = message.to_id.user_id; } + + ConcurrentHashMap read_max = message.out ? dialogs_read_outbox_max : dialogs_read_inbox_max; + Integer value = read_max.get(message.dialog_id); + if (value == null) { + value = MessagesStorage.getInstance().getDialogReadMax(message.out, message.dialog_id); + read_max.put(message.dialog_id, value); + } + message.unread = !(value >= message.id || chat != null && ChatObject.isNotInChat(chat) || message.action instanceof TLRPC.TL_messageActionChatMigrateTo || message.action instanceof TLRPC.TL_messageActionChannelCreate); + MessageObject obj = new MessageObject(message, usersDict, chatsDict, createdDialogIds.contains(message.dialog_id)); if (obj.type == 11) { interfaceUpdateMask |= UPDATE_MASK_CHAT_AVATAR; @@ -5821,7 +6110,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter messages.put(message.dialog_id, arr); } arr.add(obj); - if (!obj.isOut() && (obj.isUnread() && message.to_id.channel_id == 0 || obj.isContentUnread())) { + if (!obj.isOut() && obj.isUnread()) { pushMessages.add(obj); } } else if (update instanceof TLRPC.TL_updateReadMessagesContents) { @@ -5829,20 +6118,35 @@ public class MessagesController implements NotificationCenter.NotificationCenter long id = update.messages.get(a); markAsReadMessages.add(id); } - } else if (update instanceof TLRPC.TL_updateReadHistoryInbox) { - TLRPC.Peer peer = ((TLRPC.TL_updateReadHistoryInbox) update).peer; - if (peer.chat_id != 0) { - markAsReadMessagesInbox.put(-peer.chat_id, (long) update.max_id); + } else if (update instanceof TLRPC.TL_updateReadHistoryInbox || update instanceof TLRPC.TL_updateReadHistoryOutbox) { + long dialog_id; + ConcurrentHashMap read_max; + if (update instanceof TLRPC.TL_updateReadHistoryInbox) { + TLRPC.Peer peer = ((TLRPC.TL_updateReadHistoryInbox) update).peer; + if (peer.chat_id != 0) { + markAsReadMessagesInbox.put(-peer.chat_id, (long) update.max_id); + dialog_id = -peer.chat_id; + } else { + markAsReadMessagesInbox.put(peer.user_id, (long) update.max_id); + dialog_id = peer.user_id; + } + read_max = dialogs_read_inbox_max; } else { - markAsReadMessagesInbox.put(peer.user_id, (long) update.max_id); + TLRPC.Peer peer = ((TLRPC.TL_updateReadHistoryOutbox) update).peer; + if (peer.chat_id != 0) { + markAsReadMessagesOutbox.put(-peer.chat_id, (long) update.max_id); + dialog_id = -peer.chat_id; + } else { + markAsReadMessagesOutbox.put(peer.user_id, (long) update.max_id); + dialog_id = peer.user_id; + } + read_max = dialogs_read_outbox_max; } - } else if (update instanceof TLRPC.TL_updateReadHistoryOutbox) { - TLRPC.Peer peer = ((TLRPC.TL_updateReadHistoryOutbox) update).peer; - if (peer.chat_id != 0) { - markAsReadMessagesOutbox.put(-peer.chat_id, update.max_id); - } else { - markAsReadMessagesOutbox.put(peer.user_id, update.max_id); + Integer value = read_max.get(dialog_id); + if (value == null) { + value = MessagesStorage.getInstance().getDialogReadMax(update instanceof TLRPC.TL_updateReadHistoryOutbox, dialog_id); } + read_max.put(dialog_id, Math.max(value, update.max_id)); } else if (update instanceof TLRPC.TL_updateDeleteMessages) { ArrayList arrayList = deletedMessages.get(0); if (arrayList == null) { @@ -5957,35 +6261,37 @@ public class MessagesController implements NotificationCenter.NotificationCenter } } } else if (update instanceof TLRPC.TL_updateNewAuthorization) { - AndroidUtilities.runOnUIThread(new Runnable() { - @Override - public void run() { - NotificationCenter.getInstance().postNotificationName(NotificationCenter.newSessionReceived); - } - }); - TLRPC.TL_messageService newMessage = new TLRPC.TL_messageService(); - newMessage.action = new TLRPC.TL_messageActionLoginUnknownLocation(); - newMessage.action.title = update.device; - newMessage.action.address = update.location; - newMessage.local_id = newMessage.id = UserConfig.getNewMessageId(); - UserConfig.saveConfig(false); - newMessage.unread = true; - newMessage.flags = TLRPC.MESSAGE_FLAG_HAS_FROM_ID; - newMessage.date = update.date; - newMessage.from_id = 777000; - newMessage.to_id = new TLRPC.TL_peerUser(); - newMessage.to_id.user_id = UserConfig.getClientUserId(); - newMessage.dialog_id = 777000; + if (!MessagesStorage.getInstance().hasAuthMessage(update.date)) { + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + NotificationCenter.getInstance().postNotificationName(NotificationCenter.newSessionReceived); + } + }); + TLRPC.TL_messageService newMessage = new TLRPC.TL_messageService(); + newMessage.action = new TLRPC.TL_messageActionLoginUnknownLocation(); + newMessage.action.title = update.device; + newMessage.action.address = update.location; + newMessage.local_id = newMessage.id = UserConfig.getNewMessageId(); + UserConfig.saveConfig(false); + newMessage.unread = true; + newMessage.flags = TLRPC.MESSAGE_FLAG_HAS_FROM_ID; + newMessage.date = update.date; + newMessage.from_id = 777000; + newMessage.to_id = new TLRPC.TL_peerUser(); + newMessage.to_id.user_id = UserConfig.getClientUserId(); + newMessage.dialog_id = 777000; - messagesArr.add(newMessage); - MessageObject obj = new MessageObject(newMessage, usersDict, chatsDict, createdDialogIds.contains(newMessage.dialog_id)); - ArrayList arr = messages.get(newMessage.dialog_id); - if (arr == null) { - arr = new ArrayList<>(); - messages.put(newMessage.dialog_id, arr); + messagesArr.add(newMessage); + MessageObject obj = new MessageObject(newMessage, usersDict, chatsDict, createdDialogIds.contains(newMessage.dialog_id)); + ArrayList arr = messages.get(newMessage.dialog_id); + if (arr == null) { + arr = new ArrayList<>(); + messages.put(newMessage.dialog_id, arr); + } + arr.add(obj); + pushMessages.add(obj); } - arr.add(obj); - pushMessages.add(obj); } else if (update instanceof TLRPC.TL_updateNewGeoChatMessage) { //DEPRECATED } else if (update instanceof TLRPC.TL_updateNewEncryptedMessage) { @@ -6109,40 +6415,60 @@ public class MessagesController implements NotificationCenter.NotificationCenter } else if (update instanceof TLRPC.TL_updateWebPage) { webPages.put(update.webpage.id, update.webpage); } else if (update instanceof TLRPC.TL_updateChannelTooLong) { - if ((update.flags & 1) != 0) { - Integer channelPts = channelsPts.get(update.channel_id); - if (channelPts == null) { - channelPts = MessagesStorage.getInstance().getChannelPtsSync(update.channel_id); - if (channelPts == 0) { - channelPts = 1; + if (BuildVars.DEBUG_VERSION) { + FileLog.d("tmessages", update + " channelId = " + update.channel_id); + } + Integer channelPts = channelsPts.get(update.channel_id); + if (channelPts == null) { + channelPts = MessagesStorage.getInstance().getChannelPtsSync(update.channel_id); + if (channelPts == 0) { + TLRPC.Chat chat = chatsDict.get(update.channel_id); + if (chat == null || chat.min) { + chat = getChat(update.channel_id); } + if (chat == null || chat.min) { + chat = MessagesStorage.getInstance().getChatSync(update.channel_id); + putChat(chat, true); + } + if (chat != null && !chat.min) { + loadUnknownChannel(chat, 0); + } + } else { channelsPts.put(update.channel_id, channelPts); } - if (update.pts > channelPts) { + } + if (channelPts != 0) { + if ((update.flags & 1) != 0) { + if (update.pts > channelPts) { + getChannelDifference(update.channel_id); + } + } else { getChannelDifference(update.channel_id); } - } else { - getChannelDifference(update.channel_id); } - } else if (update instanceof TLRPC.TL_updateChannelGroup) { - ArrayList arrayList = channelsGroups.get(update.channel_id); - if (arrayList == null) { - arrayList = new ArrayList<>(); - channelsGroups.put(update.channel_id, arrayList); - } - arrayList.add(update.group); - } else if (update instanceof TLRPC.TL_updateReadChannelInbox) { + } else if (update instanceof TLRPC.TL_updateReadChannelInbox || update instanceof TLRPC.TL_updateReadChannelOutbox) { long message_id = update.max_id; message_id |= ((long) update.channel_id) << 32; - markAsReadMessagesInbox.put(-update.channel_id, message_id); - long dialog_id = -update.channel_id; - Integer value = dialogs_read_inbox_max.get(dialog_id); - if (value == null) { - value = MessagesStorage.getInstance().getDialogReadInboxMax(dialog_id); + + ConcurrentHashMap read_max; + if (update instanceof TLRPC.TL_updateReadChannelInbox) { + read_max = dialogs_read_inbox_max; + markAsReadMessagesInbox.put(-update.channel_id, message_id); + } else { + read_max = dialogs_read_outbox_max; + markAsReadMessagesOutbox.put(-update.channel_id, message_id); } - dialogs_read_inbox_max.put(dialog_id, Math.max(value, update.max_id)); + + Integer value = read_max.get(dialog_id); + if (value == null) { + value = MessagesStorage.getInstance().getDialogReadMax(update instanceof TLRPC.TL_updateReadChannelOutbox, dialog_id); + } + read_max.put(dialog_id, Math.max(value, update.max_id)); } else if (update instanceof TLRPC.TL_updateDeleteChannelMessages) { + if (BuildVars.DEBUG_VERSION) { + FileLog.d("tmessages", update + " channelId = " + update.channel_id); + } ArrayList arrayList = deletedMessages.get(update.channel_id); if (arrayList == null) { arrayList = new ArrayList<>(); @@ -6150,8 +6476,14 @@ public class MessagesController implements NotificationCenter.NotificationCenter } arrayList.addAll(update.messages); } else if (update instanceof TLRPC.TL_updateChannel) { + if (BuildVars.DEBUG_VERSION) { + FileLog.d("tmessages", update + " channelId = " + update.channel_id); + } updatesOnMainThread.add(update); } else if (update instanceof TLRPC.TL_updateChannelMessageViews) { + if (BuildVars.DEBUG_VERSION) { + FileLog.d("tmessages", update + " channelId = " + update.channel_id); + } TLRPC.TL_updateChannelMessageViews updateChannelMessageViews = (TLRPC.TL_updateChannelMessageViews) update; SparseIntArray array = channelViews.get(update.channel_id); if (array == null) { @@ -6169,12 +6501,17 @@ public class MessagesController implements NotificationCenter.NotificationCenter updatesOnMainThread.add(update); } else if (update instanceof TLRPC.TL_updateNewStickerSet) { updatesOnMainThread.add(update); + } else if (update instanceof TLRPC.TL_updateDraftMessage) { + updatesOnMainThread.add(update); } else if (update instanceof TLRPC.TL_updateSavedGifs) { updatesOnMainThread.add(update); } else if (update instanceof TLRPC.TL_updateEditChannelMessage || update instanceof TLRPC.TL_updateEditMessage) { TLRPC.Message message; if (update instanceof TLRPC.TL_updateEditChannelMessage) { message = ((TLRPC.TL_updateEditChannelMessage) update).message; + if (BuildVars.DEBUG_VERSION) { + FileLog.d("tmessages", update + " channelId = " + message.to_id.channel_id); + } TLRPC.Chat chat = chatsDict.get(message.to_id.channel_id); if (chat == null) { chat = getChat(message.to_id.channel_id); @@ -6193,24 +6530,26 @@ public class MessagesController implements NotificationCenter.NotificationCenter message.message = "-1"; message.attachPath = ""; } - int count = message.entities.size(); - for (int a = 0; a < count; a++) { - TLRPC.MessageEntity entity = message.entities.get(a); - if (entity instanceof TLRPC.TL_messageEntityMentionName) { - int user_id = ((TLRPC.TL_messageEntityMentionName) entity).user_id; - TLRPC.User user = usersDict.get(user_id); - if (user == null || user.min) { - user = getUser(user_id); - } - if (user == null || user.min) { - user = MessagesStorage.getInstance().getUserSync(user_id); - if (user != null && user.min) { - user = null; + if (!fromGetDifference) { + int count = message.entities.size(); + for (int a = 0; a < count; a++) { + TLRPC.MessageEntity entity = message.entities.get(a); + if (entity instanceof TLRPC.TL_messageEntityMentionName) { + int user_id = ((TLRPC.TL_messageEntityMentionName) entity).user_id; + TLRPC.User user = usersDict.get(user_id); + if (user == null || user.min) { + user = getUser(user_id); + } + if (user == null || user.min) { + user = MessagesStorage.getInstance().getUserSync(user_id); + if (user != null && user.min) { + user = null; + } + putUser(user, true); + } + if (user == null) { + return false; } - putUser(user, true); - } - if (user == null) { - return false; } } } @@ -6226,23 +6565,13 @@ public class MessagesController implements NotificationCenter.NotificationCenter message.dialog_id = message.to_id.user_id; } - if (!message.out) { - if (message.to_id.channel_id != 0) { - message.unread = true; - if (message.post || (message.flags & TLRPC.MESSAGE_FLAG_MEGAGROUP) != 0) { - message.media_unread = true; - } - } - Integer value = dialogs_read_inbox_max.get(message.dialog_id); - if (value == null) { - value = MessagesStorage.getInstance().getDialogReadInboxMax(message.dialog_id); - } - if (value >= message.id) { - message.unread = message.media_unread = false; - } else { - message.unread = message.media_unread = true; - } + ConcurrentHashMap read_max = message.out ? dialogs_read_outbox_max : dialogs_read_inbox_max; + Integer value = read_max.get(message.dialog_id); + if (value == null) { + value = MessagesStorage.getInstance().getDialogReadMax(message.out, message.dialog_id); + read_max.put(message.dialog_id, value); } + message.unread = value < message.id; ImageLoader.saveMessageThumbs(message); @@ -6255,6 +6584,9 @@ public class MessagesController implements NotificationCenter.NotificationCenter } arr.add(obj); } else if (update instanceof TLRPC.TL_updateChannelPinnedMessage) { + if (BuildVars.DEBUG_VERSION) { + FileLog.d("tmessages", update + " channelId = " + update.channel_id); + } TLRPC.TL_updateChannelPinnedMessage updateChannelPinnedMessage = (TLRPC.TL_updateChannelPinnedMessage) update; MessagesStorage.getInstance().updateChannelPinnedMessage(update.channel_id, updateChannelPinnedMessage.id); } @@ -6304,21 +6636,19 @@ public class MessagesController implements NotificationCenter.NotificationCenter for (int a = 0; a < messageObjects.size(); a++) { messagesRes.messages.add(messageObjects.get(a).messageOwner); } - MessagesStorage.getInstance().putMessages(messagesRes, pair.getKey(), -2, 0, 0, false); + MessagesStorage.getInstance().putMessages(messagesRes, pair.getKey(), -2, 0, false); } } if (channelViews.size() != 0) { MessagesStorage.getInstance().putChannelViews(channelViews, true); } - if (channelsGroups.size() != 0) { - //MessagesStorage.getInstance().applyNewChannelsGroups(channelsGroups); - } AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { int updateMask = interfaceUpdateMaskFinal; + boolean hasDraftUpdates = false; if (!updatesOnMainThread.isEmpty()) { ArrayList dbUsers = new ArrayList<>(); @@ -6404,7 +6734,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter dialog_id = -updateNotifySettings.peer.peer.channel_id; } - TLRPC.Dialog dialog = dialogs_dict.get(dialog_id); + TLRPC.TL_dialog dialog = dialogs_dict.get(dialog_id); if (dialog != null) { dialog.notify_settings = update.notify_settings; } @@ -6435,17 +6765,19 @@ public class MessagesController implements NotificationCenter.NotificationCenter } } } else if (update instanceof TLRPC.TL_updateChannel) { - TLRPC.Dialog dialog = dialogs_dict.get(-(long) update.channel_id); + TLRPC.TL_dialog dialog = dialogs_dict.get(-(long) update.channel_id); TLRPC.Chat chat = getChat(update.channel_id); - if (dialog == null && chat instanceof TLRPC.TL_channel && !chat.left) { - Utilities.stageQueue.postRunnable(new Runnable() { - @Override - public void run() { - getChannelDifference(update.channel_id, 1); - } - }); - } else if (chat.left && dialog != null) { - deleteDialog(dialog.id, 0); + if (chat != null) { + if (dialog == null && chat instanceof TLRPC.TL_channel && !chat.left) { + Utilities.stageQueue.postRunnable(new Runnable() { + @Override + public void run() { + getChannelDifference(update.channel_id, 1, 0); + } + }); + } else if (chat.left && dialog != null) { + deleteDialog(dialog.id, 0); + } } updateMask |= UPDATE_MASK_CHANNEL; loadFullChat(update.channel_id, 0, true); @@ -6460,6 +6792,18 @@ public class MessagesController implements NotificationCenter.NotificationCenter } else if (update instanceof TLRPC.TL_updateSavedGifs) { SharedPreferences.Editor editor2 = ApplicationLoader.applicationContext.getSharedPreferences("emoji", Activity.MODE_PRIVATE).edit(); editor2.putLong("lastGifLoadTime", 0).commit(); + } else if (update instanceof TLRPC.TL_updateDraftMessage) { + hasDraftUpdates = true; + long did; + TLRPC.Peer peer = ((TLRPC.TL_updateDraftMessage) update).peer; + if (peer.user_id != 0) { + did = peer.user_id; + } else if (peer.channel_id != 0) { + did = -peer.channel_id; + } else { + did = -peer.chat_id; + } + DraftQuery.saveDraft(did, update.draft, null, true); } } if (editor != null) { @@ -6506,6 +6850,9 @@ public class MessagesController implements NotificationCenter.NotificationCenter updateInterfaceWithMessages(key, value); } updateDialogs = true; + } else if (hasDraftUpdates) { + sortDialogs(null); + updateDialogs = true; } if (!editingMessages.isEmpty()) { for (HashMap.Entry> pair : editingMessages.entrySet()) { @@ -6566,11 +6913,11 @@ public class MessagesController implements NotificationCenter.NotificationCenter NotificationsController.getInstance().processReadMessages(markAsReadMessagesInbox, 0, 0, 0, false); for (int b = 0; b < markAsReadMessagesInbox.size(); b++) { int key = markAsReadMessagesInbox.keyAt(b); - long messageId = markAsReadMessagesInbox.get(key); - TLRPC.Dialog dialog = dialogs_dict.get((long) key); - if (dialog != null && dialog.top_message <= (int) messageId) { + int messageId = (int) ((long) markAsReadMessagesInbox.get(key)); + TLRPC.TL_dialog dialog = dialogs_dict.get((long) key); + if (dialog != null && dialog.top_message <= messageId) { MessageObject obj = dialogMessage.get(dialog.id); - if (obj != null) { + if (obj != null && !obj.isOut()) { obj.setIsRead(); updateMask |= UPDATE_MASK_READ_DIALOG_MESSAGE; } @@ -6578,11 +6925,11 @@ public class MessagesController implements NotificationCenter.NotificationCenter } for (int b = 0; b < markAsReadMessagesOutbox.size(); b++) { int key = markAsReadMessagesOutbox.keyAt(b); - int messageId = markAsReadMessagesOutbox.get(key); - TLRPC.Dialog dialog = dialogs_dict.get((long) key); + int messageId = (int) ((long) markAsReadMessagesOutbox.get(key)); + TLRPC.TL_dialog dialog = dialogs_dict.get((long) key); if (dialog != null && dialog.top_message <= messageId) { MessageObject obj = dialogMessage.get(dialog.id); - if (obj != null) { + if (obj != null && obj.isOut()) { obj.setIsRead(); updateMask |= UPDATE_MASK_READ_DIALOG_MESSAGE; } @@ -6593,7 +6940,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter for (HashMap.Entry entry : markAsReadEncrypted.entrySet()) { NotificationCenter.getInstance().postNotificationName(NotificationCenter.messagesReadEncrypted, entry.getKey(), entry.getValue()); long dialog_id = (long) (entry.getKey()) << 32; - TLRPC.Dialog dialog = dialogs_dict.get(dialog_id); + TLRPC.TL_dialog dialog = dialogs_dict.get(dialog_id); if (dialog != null) { MessageObject message = dialogMessage.get(dialog_id); if (message != null && message.messageOwner.date <= entry.getValue()) { @@ -6649,7 +6996,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter } if (markAsReadMessagesInbox.size() != 0 || markAsReadMessagesOutbox.size() != 0 || !markAsReadEncrypted.isEmpty()) { if (markAsReadMessagesInbox.size() != 0) { - MessagesStorage.getInstance().updateDialogsWithReadMessages(markAsReadMessagesInbox, true); + MessagesStorage.getInstance().updateDialogsWithReadMessages(markAsReadMessagesInbox, markAsReadMessagesOutbox, true); } MessagesStorage.getInstance().markMessagesAsRead(markAsReadMessagesInbox, markAsReadMessagesOutbox, markAsReadEncrypted, true); } @@ -6756,9 +7103,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter for (int a = 0; a < messages.size(); a++) { MessageObject message = messages.get(a); 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) { - if (message.messageOwner.to_id.channel_id == 0 || message.isMegagroup() || message.isImportant()) { - lastMessage = message; - } + lastMessage = message; if (message.messageOwner.to_id.channel_id != 0) { channelId = message.messageOwner.to_id.channel_id; } @@ -6776,7 +7121,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter if (lastMessage == null) { return; } - TLRPC.Dialog dialog = dialogs_dict.get(uid); + TLRPC.TL_dialog dialog = dialogs_dict.get(uid); if (lastMessage.messageOwner.action instanceof TLRPC.TL_messageActionChatMigrateTo) { if (dialog != null) { dialogs.remove(dialog); @@ -6784,6 +7129,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter dialogsGroupsOnly.remove(dialog); dialogs_dict.remove(dialog.id); dialogs_read_inbox_max.remove(dialog.id); + dialogs_read_outbox_max.remove(dialog.id); nextDialogsCacheOffset--; dialogMessage.remove(dialog.id); MessageObject object = dialogMessagesByIds.remove(dialog.top_message); @@ -6805,15 +7151,12 @@ public class MessagesController implements NotificationCenter.NotificationCenter if (channelId != 0 && chat == null || chat != null && chat.left) { return; } - if (!ChatObject.isChannel(chat)) { - dialog = new TLRPC.TL_dialog(); - } else { - dialog = new TLRPC.TL_dialogChannel(); - } + dialog = new TLRPC.TL_dialog(); dialog.id = uid; dialog.unread_count = 0; dialog.top_message = lastMessage.getId(); dialog.last_message_date = lastMessage.messageOwner.date; + dialog.flags = ChatObject.isChannel(chat) ? 1 : 0; dialogs_dict.put(uid, dialog); dialogs.add(dialog); dialogMessage.put(uid, lastMessage); @@ -6850,38 +7193,7 @@ public class MessagesController implements NotificationCenter.NotificationCenter } if (changed) { - dialogsServerOnly.clear(); - dialogsGroupsOnly.clear(); - Collections.sort(dialogs, new Comparator() { - @Override - public int compare(TLRPC.Dialog tl_dialog, TLRPC.Dialog tl_dialog2) { - if (tl_dialog.last_message_date == tl_dialog2.last_message_date) { - return 0; - } else if (tl_dialog.last_message_date < tl_dialog2.last_message_date) { - return 1; - } else { - return -1; - } - } - }); - for (int a = 0; a < dialogs.size(); a++) { - TLRPC.Dialog d = dialogs.get(a); - int high_id = (int) (d.id >> 32); - if ((int) d.id != 0 && high_id != 1) { - dialogsServerOnly.add(d); - if (d instanceof TLRPC.TL_dialog) { - if (d.id < 0) { - dialogsGroupsOnly.add(d); - } - } else if (d instanceof TLRPC.TL_dialogChannel) { - int lower_id = (int) d.id; - TLRPC.Chat chat = getChat(-lower_id); - if (chat != null && (chat.megagroup && chat.editor || chat.creator)) { - dialogsGroupsOnly.add(d); - } - } - } - } + sortDialogs(null); } if (updateRating) { @@ -6889,6 +7201,36 @@ public class MessagesController implements NotificationCenter.NotificationCenter } } + public void sortDialogs(HashMap chatsDict) { + dialogsServerOnly.clear(); + dialogsGroupsOnly.clear(); + Collections.sort(dialogs, dialogComparator); + for (int a = 0; a < dialogs.size(); a++) { + TLRPC.TL_dialog d = dialogs.get(a); + int high_id = (int) (d.id >> 32); + int lower_id = (int) d.id; + if (lower_id != 0 && high_id != 1) { + dialogsServerOnly.add(d); + if (DialogObject.isChannel(d)) { + TLRPC.Chat chat = getChat(-lower_id); + if (chat != null && (chat.megagroup && chat.editor || chat.creator)) { + dialogsGroupsOnly.add(d); + } + } else if (lower_id < 0) { + if (chatsDict != null) { + TLRPC.Chat chat = chatsDict.get(-lower_id); + if (chat != null && chat.migrated_to != null) { + dialogs.remove(a); + a--; + continue; + } + } + dialogsGroupsOnly.add(d); + } + } + } + } + private static String getRestrictionReason(String reason) { if (reason == null || reason.length() == 0) { return null; diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/MessagesStorage.java b/TMessagesProj/src/main/java/org/telegram/messenger/MessagesStorage.java index bfa0d16f2..4e21d0b40 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/MessagesStorage.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/MessagesStorage.java @@ -34,11 +34,14 @@ import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.concurrent.Semaphore; +import java.util.concurrent.atomic.AtomicLong; public class MessagesStorage { private DispatchQueue storageQueue = new DispatchQueue("storageQueue"); private SQLiteDatabase database; private File cacheFile; + private AtomicLong lastTaskId = new AtomicLong(System.currentTimeMillis()); + public static int lastDateValue = 0; public static int lastPtsValue = 0; public static int lastQtsValue = 0; @@ -53,6 +56,7 @@ public class MessagesStorage { private int lastSavedQts = 0; private static volatile MessagesStorage Instance = null; + public static MessagesStorage getInstance() { MessagesStorage localInstance = Instance; if (localInstance == null) { @@ -92,12 +96,8 @@ public class MessagesStorage { database.executeFast("PRAGMA secure_delete = ON").stepThis().dispose(); database.executeFast("PRAGMA temp_store = 1").stepThis().dispose(); if (createTable) { - database.executeFast("CREATE TABLE channel_group(uid INTEGER, start INTEGER, end INTEGER, count INTEGER, PRIMARY KEY(uid, start));").stepThis().dispose(); - database.executeFast("CREATE TABLE messages_holes(uid INTEGER, start INTEGER, end INTEGER, PRIMARY KEY(uid, start));").stepThis().dispose(); database.executeFast("CREATE INDEX IF NOT EXISTS uid_end_messages_holes ON messages_holes(uid, end);").stepThis().dispose(); - database.executeFast("CREATE TABLE messages_imp_holes(uid INTEGER, start INTEGER, end INTEGER, PRIMARY KEY(uid, start));").stepThis().dispose(); - database.executeFast("CREATE INDEX IF NOT EXISTS uid_end_messages_imp_holes ON messages_imp_holes(uid, end);").stepThis().dispose(); database.executeFast("CREATE TABLE media_holes_v2(uid INTEGER, type INTEGER, start INTEGER, end INTEGER, PRIMARY KEY(uid, type, start));").stepThis().dispose(); database.executeFast("CREATE INDEX IF NOT EXISTS uid_end_media_holes_v2 ON media_holes_v2(uid, type, end);").stepThis().dispose(); @@ -105,8 +105,6 @@ 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, imp INTEGER)").stepThis().dispose(); database.executeFast("CREATE INDEX IF NOT EXISTS uid_mid_idx_messages ON messages(uid, mid);").stepThis().dispose(); database.executeFast("CREATE INDEX IF NOT EXISTS uid_date_mid_idx_messages ON messages(uid, date, mid);").stepThis().dispose(); - database.executeFast("CREATE INDEX IF NOT EXISTS uid_mid_idx_imp_messages ON messages(uid, mid, imp) WHERE imp = 1;").stepThis().dispose(); - database.executeFast("CREATE INDEX IF NOT EXISTS uid_date_mid_imp_idx_messages ON messages(uid, date, mid, imp) WHERE imp = 1;").stepThis().dispose(); database.executeFast("CREATE INDEX IF NOT EXISTS mid_out_idx_messages ON messages(mid, out);").stepThis().dispose(); database.executeFast("CREATE INDEX IF NOT EXISTS task_idx_messages ON messages(uid, out, read_state, ttl, date, send_state);").stepThis().dispose(); database.executeFast("CREATE INDEX IF NOT EXISTS send_state_idx_messages ON messages(mid, send_state, date) WHERE mid < 0 AND send_state = 1;").stepThis().dispose(); @@ -157,7 +155,6 @@ public class MessagesStorage { 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 channel_users_v2(did INTEGER, uid INTEGER, date INTEGER, data BLOB, PRIMARY KEY(did, uid))").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(); database.executeFast("CREATE TABLE wallpapers(uid INTEGER PRIMARY KEY, data BLOB)").stepThis().dispose(); database.executeFast("CREATE TABLE user_photos(uid INTEGER, id INTEGER, data BLOB, PRIMARY KEY (uid, id))").stepThis().dispose(); database.executeFast("CREATE TABLE blocked_users(uid INTEGER PRIMARY KEY)").stepThis().dispose(); @@ -172,9 +169,10 @@ public class MessagesStorage { database.executeFast("CREATE TABLE media_counts_v2(uid INTEGER, type INTEGER, count INTEGER, PRIMARY KEY(uid, type))").stepThis().dispose(); database.executeFast("CREATE TABLE keyvalue(id TEXT PRIMARY KEY, value TEXT)").stepThis().dispose(); database.executeFast("CREATE TABLE bot_info(uid INTEGER PRIMARY KEY, info BLOB)").stepThis().dispose(); + database.executeFast("CREATE TABLE pending_tasks(id INTEGER PRIMARY KEY, data BLOB);").stepThis().dispose(); //version - database.executeFast("PRAGMA user_version = 32").stepThis().dispose(); + database.executeFast("PRAGMA user_version = 34").stepThis().dispose(); //database.executeFast("CREATE TABLE secret_holes(uid INTEGER, seq_in INTEGER, seq_out INTEGER, data BLOB, PRIMARY KEY (uid, seq_in, seq_out));").stepThis().dispose(); //database.executeFast("CREATE TABLE attach_data(uid INTEGER, id INTEGER, data BLOB, PRIMARY KEY (uid, id))").stepThis().dispose(); @@ -208,7 +206,7 @@ public class MessagesStorage { } } int version = database.executeInt("PRAGMA user_version"); - if (version < 32) { + if (version < 34) { updateDbToLastVersion(version); } } @@ -216,6 +214,7 @@ public class MessagesStorage { FileLog.e("tmessages", e); } loadUnreadMessages(); + loadPendingTasks(); } public void updateDbToLastVersion(final int currentVersion) { @@ -456,13 +455,8 @@ public class MessagesStorage { database.executeFast("CREATE INDEX IF NOT EXISTS last_mid_i_idx_dialogs ON dialogs(last_mid_i);").stepThis().dispose(); database.executeFast("CREATE INDEX IF NOT EXISTS unread_count_i_idx_dialogs ON dialogs(unread_count_i);").stepThis().dispose(); database.executeFast("ALTER TABLE messages ADD COLUMN imp INTEGER default 0").stepThis().dispose(); - database.executeFast("CREATE INDEX IF NOT EXISTS uid_mid_idx_imp_messages ON messages(uid, mid, imp) WHERE imp = 1;").stepThis().dispose(); - database.executeFast("CREATE INDEX IF NOT EXISTS uid_date_mid_imp_idx_messages ON messages(uid, date, mid, imp) WHERE imp = 1;").stepThis().dispose(); - database.executeFast("CREATE TABLE IF NOT EXISTS channel_group(uid INTEGER, start INTEGER, end INTEGER, count INTEGER, PRIMARY KEY(uid, start));").stepThis().dispose(); database.executeFast("CREATE TABLE IF NOT EXISTS messages_holes(uid INTEGER, start INTEGER, end INTEGER, PRIMARY KEY(uid, start));").stepThis().dispose(); database.executeFast("CREATE INDEX IF NOT EXISTS uid_end_messages_holes ON messages_holes(uid, end);").stepThis().dispose(); - database.executeFast("CREATE TABLE IF NOT EXISTS messages_imp_holes(uid INTEGER, start INTEGER, end INTEGER, PRIMARY KEY(uid, start));").stepThis().dispose(); - database.executeFast("CREATE INDEX IF NOT EXISTS uid_end_messages_imp_holes ON messages_imp_holes(uid, end);").stepThis().dispose(); database.executeFast("PRAGMA user_version = 22").stepThis().dispose(); version = 22; } @@ -511,7 +505,18 @@ public class MessagesStorage { database.executeFast("CREATE TABLE IF NOT EXISTS chat_hints(did INTEGER, type INTEGER, rating REAL, date INTEGER, PRIMARY KEY(did, type))").stepThis().dispose(); database.executeFast("CREATE INDEX IF NOT EXISTS chat_hints_rating_idx ON chat_hints(rating);").stepThis().dispose(); database.executeFast("PRAGMA user_version = 32").stepThis().dispose(); - //version = 32; + version = 32; + } + if (version == 32) { + database.executeFast("DROP INDEX IF EXISTS uid_mid_idx_imp_messages;").stepThis().dispose(); + database.executeFast("DROP INDEX IF EXISTS uid_date_mid_imp_idx_messages;").stepThis().dispose(); + database.executeFast("PRAGMA user_version = 33").stepThis().dispose(); + version = 33; + } + if (version == 33) { + database.executeFast("CREATE TABLE IF NOT EXISTS pending_tasks(id INTEGER PRIMARY KEY, data BLOB);").stepThis().dispose(); + database.executeFast("PRAGMA user_version = 34").stepThis().dispose(); + //version = 34; } } catch (Exception e) { FileLog.e("tmessages", e); @@ -520,7 +525,7 @@ public class MessagesStorage { }); } - public void cleanUp(final boolean isLogin) { + public void cleanup(final boolean isLogin) { storageQueue.cleanupQueue(); storageQueue.postRunnable(new Runnable() { @Override @@ -582,6 +587,106 @@ public class MessagesStorage { }); } + public long createPendingTask(final NativeByteBuffer data) { + if (data == null) { + return 0; + } + final long id = lastTaskId.getAndAdd(1); + storageQueue.postRunnable(new Runnable() { + @Override + public void run() { + try { + SQLitePreparedStatement state = database.executeFast("REPLACE INTO pending_tasks VALUES(?, ?)"); + state.bindLong(1, id); + state.bindByteBuffer(2, data); + state.step(); + state.dispose(); + } catch (Exception e) { + FileLog.e("tmessages", e); + } finally { + data.reuse(); + } + } + }); + return id; + } + + public void removePendingTask(final long id) { + storageQueue.postRunnable(new Runnable() { + @Override + public void run() { + try { + database.executeFast("DELETE FROM pending_tasks WHERE id = " + id).stepThis().dispose(); + } catch (Exception e) { + FileLog.e("tmessages", e); + } + } + }); + } + + private void loadPendingTasks() { + storageQueue.postRunnable(new Runnable() { + @Override + public void run() { + try { + SQLiteCursor cursor = database.queryFinalized("SELECT id, data FROM pending_tasks WHERE 1"); + while (cursor.next()) { + final long taskId = cursor.longValue(0); + NativeByteBuffer data = cursor.byteBufferValue(1); + if (data != null) { + int type = data.readInt32(false); + switch (type) { + case 0: + final TLRPC.Chat chat = TLRPC.Chat.TLdeserialize(data, data.readInt32(false), false); + if (chat != null) { + Utilities.stageQueue.postRunnable(new Runnable() { + @Override + public void run() { + MessagesController.getInstance().loadUnknownChannel(chat, taskId); + } + }); + } + break; + case 1: + final int channelId = data.readInt32(false); + final int newDialogType = data.readInt32(false); + Utilities.stageQueue.postRunnable(new Runnable() { + @Override + public void run() { + MessagesController.getInstance().getChannelDifference(channelId, newDialogType, taskId); + } + }); + break; + case 2: + final TLRPC.TL_dialog dialog = new TLRPC.TL_dialog(); + dialog.id = data.readInt64(false); + dialog.top_message = data.readInt32(false); + dialog.read_inbox_max_id = data.readInt32(false); + dialog.read_outbox_max_id = data.readInt32(false); + dialog.unread_count = data.readInt32(false); + dialog.last_message_date = data.readInt32(false); + dialog.pts = data.readInt32(false); + dialog.flags = data.readInt32(false); + final TLRPC.InputPeer peer = TLRPC.InputPeer.TLdeserialize(data, data.readInt32(false), false); + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + MessagesController.getInstance().checkLastDialogMessage(dialog, peer, taskId); + } + }); + break; + } + data.reuse(); + } + } + cursor.dispose(); + } catch (Exception e) { + FileLog.e("tmessages", e); + } + } + }); + } + public void saveChannelPts(final int channelId, final int pts) { storageQueue.postRunnable(new Runnable() { @Override @@ -643,6 +748,10 @@ public class MessagesStorage { @Override public void run() { try { + ArrayList usersToLoad = new ArrayList<>(); + ArrayList chatsToLoad = new ArrayList<>(); + ArrayList encryptedChatIds = new ArrayList<>(); + final HashMap pushDialogs = new HashMap<>(); SQLiteCursor cursor = database.queryFinalized("SELECT d.did, d.unread_count, s.flags FROM dialogs as d LEFT JOIN dialog_settings as s ON d.did = s.did WHERE d.unread_count != 0"); StringBuilder ids = new StringBuilder(); @@ -655,6 +764,23 @@ public class MessagesStorage { ids.append(","); } ids.append(did); + int lower_id = (int) did; + int high_id = (int) (did >> 32); + if (lower_id != 0) { + if (lower_id < 0) { + if (!chatsToLoad.contains(-lower_id)) { + chatsToLoad.add(-lower_id); + } + } else { + if (!usersToLoad.contains(lower_id)) { + usersToLoad.add(lower_id); + } + } + } else { + if (!encryptedChatIds.contains(high_id)) { + encryptedChatIds.add(high_id); + } + } } } cursor.dispose(); @@ -664,10 +790,6 @@ public class MessagesStorage { final ArrayList chats = new ArrayList<>(); final ArrayList encryptedChats = new ArrayList<>(); if (ids.length() > 0) { - ArrayList usersToLoad = new ArrayList<>(); - ArrayList chatsToLoad = new ArrayList<>(); - ArrayList 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 IN(0,2) ORDER BY date DESC LIMIT 50"); while (cursor.next()) { NativeByteBuffer data = cursor.byteBufferValue(1); @@ -680,27 +802,10 @@ public class MessagesStorage { message.dialog_id = cursor.longValue(5); messages.add(message); - int lower_id = (int)message.dialog_id; - int high_id = (int)(message.dialog_id >> 32); - - if (lower_id != 0) { - if (lower_id < 0) { - if (!chatsToLoad.contains(-lower_id)) { - chatsToLoad.add(-lower_id); - } - } else { - if (!usersToLoad.contains(lower_id)) { - usersToLoad.add(lower_id); - } - } - } else { - if (!encryptedChatIds.contains(high_id)) { - encryptedChatIds.add(high_id); - } - } + int lower_id = (int) message.dialog_id; addUsersAndChatsFromMessage(message, usersToLoad, chatsToLoad); message.send_state = cursor.intValue(2); - if (message.to_id.channel_id == 0 && !MessageObject.isUnread(message) && lower_id != 0 || message.id > 0) { + if (message.to_id.channel_id == 0 && !MessageObject.isUnread(message) && lower_id != 0 || message.id > 0) { //TODO check message.send_state = 0; } if (lower_id == 0 && !cursor.isNull(5)) { @@ -1106,6 +1211,17 @@ public class MessagesStorage { @Override public void run() { try { + if (messagesOnly == 3) { + int lastMid = -1; + SQLiteCursor cursor = database.queryFinalized("SELECT last_mid FROM dialogs WHERE did = " + did); + if (cursor.next()) { + lastMid = cursor.intValue(0); + } + cursor.dispose(); + if (lastMid != 0) { + return; + } + } if ((int) did == 0 || messagesOnly == 2) { SQLiteCursor cursor = database.queryFinalized("SELECT data FROM messages WHERE uid = " + did); ArrayList filesToDelete = new ArrayList<>(); @@ -1143,14 +1259,14 @@ public class MessagesStorage { FileLoader.getInstance().deleteFiles(filesToDelete, messagesOnly); } - if (messagesOnly == 0) { + if (messagesOnly == 0 || messagesOnly == 3) { database.executeFast("DELETE FROM dialogs WHERE did = " + did).stepThis().dispose(); database.executeFast("DELETE FROM chat_settings_v2 WHERE uid = " + did).stepThis().dispose(); database.executeFast("DELETE FROM chat_pinned WHERE uid = " + did).stepThis().dispose(); database.executeFast("DELETE FROM channel_users_v2 WHERE did = " + did).stepThis().dispose(); database.executeFast("DELETE FROM search_recent WHERE did = " + did).stepThis().dispose(); - int lower_id = (int)did; - int high_id = (int)(did >> 32); + int lower_id = (int) did; + int high_id = (int) (did >> 32); if (lower_id != 0) { if (high_id == 1) { database.executeFast("DELETE FROM chats WHERE uid = " + lower_id).stepThis().dispose(); @@ -1163,7 +1279,7 @@ public class MessagesStorage { } } else if (messagesOnly == 2) { SQLiteCursor cursor = database.queryFinalized("SELECT last_mid_i, last_mid FROM dialogs WHERE did = " + did); - ArrayList arrayList = new ArrayList<>(); + int messageId = -1; if (cursor.next()) { long last_mid_i = cursor.longValue(0); long last_mid = cursor.longValue(1); @@ -1175,7 +1291,7 @@ public class MessagesStorage { TLRPC.Message message = TLRPC.Message.TLdeserialize(data, data.readInt32(false), false); data.reuse(); if (message != null) { - arrayList.add(message); + messageId = message.id; } } } @@ -1185,9 +1301,7 @@ public class MessagesStorage { cursor2.dispose(); database.executeFast("DELETE FROM messages WHERE uid = " + did + " AND mid != " + last_mid_i + " AND mid != " + last_mid).stepThis().dispose(); - database.executeFast("DELETE FROM channel_group WHERE uid = " + did).stepThis().dispose(); database.executeFast("DELETE FROM messages_holes WHERE uid = " + did).stepThis().dispose(); - database.executeFast("DELETE FROM messages_imp_holes WHERE uid = " + did).stepThis().dispose(); database.executeFast("DELETE FROM bot_keyboard WHERE uid = " + did).stepThis().dispose(); database.executeFast("DELETE FROM media_counts_v2 WHERE uid = " + did).stepThis().dispose(); database.executeFast("DELETE FROM media_v2 WHERE uid = " + did).stepThis().dispose(); @@ -1196,13 +1310,11 @@ public class MessagesStorage { SQLitePreparedStatement state5 = database.executeFast("REPLACE INTO messages_holes VALUES(?, ?, ?)"); SQLitePreparedStatement state6 = database.executeFast("REPLACE INTO media_holes_v2 VALUES(?, ?, ?, ?)"); - SQLitePreparedStatement state7 = database.executeFast("REPLACE INTO messages_imp_holes VALUES(?, ?, ?)"); - SQLitePreparedStatement state8 = database.executeFast("REPLACE INTO channel_group VALUES(?, ?, ?, ?)"); - createFirstHoles(did, state5, state6, state7, state8, arrayList); + if (messageId != -1) { + createFirstHoles(did, state5, state6, messageId); + } state5.dispose(); state6.dispose(); - state7.dispose(); - state8.dispose(); } cursor.dispose(); return; @@ -1210,12 +1322,10 @@ public class MessagesStorage { database.executeFast("UPDATE dialogs SET unread_count = 0, unread_count_i = 0 WHERE did = " + did).stepThis().dispose(); database.executeFast("DELETE FROM messages WHERE uid = " + did).stepThis().dispose(); - database.executeFast("DELETE FROM channel_group WHERE uid = " + did).stepThis().dispose(); database.executeFast("DELETE FROM bot_keyboard WHERE uid = " + did).stepThis().dispose(); database.executeFast("DELETE FROM media_counts_v2 WHERE uid = " + did).stepThis().dispose(); database.executeFast("DELETE FROM media_v2 WHERE uid = " + did).stepThis().dispose(); database.executeFast("DELETE FROM messages_holes WHERE uid = " + did).stepThis().dispose(); - database.executeFast("DELETE FROM messages_imp_holes WHERE uid = " + did).stepThis().dispose(); database.executeFast("DELETE FROM media_holes_v2 WHERE uid = " + did).stepThis().dispose(); BotQuery.clearBotKeyboard(did, null); AndroidUtilities.runOnUIThread(new Runnable() { @@ -1231,7 +1341,7 @@ public class MessagesStorage { }); } - public void getUserPhotos(final int uid, final int offset, final int count, final long max_id, final int classGuid) { + public void getDialogPhotos(final int did, final int offset, final int count, final long max_id, final int classGuid) { storageQueue.postRunnable(new Runnable() { @Override public void run() { @@ -1239,9 +1349,9 @@ public class MessagesStorage { SQLiteCursor cursor; if (max_id != 0) { - cursor = database.queryFinalized(String.format(Locale.US, "SELECT data FROM user_photos WHERE uid = %d AND id < %d ORDER BY id DESC LIMIT %d", uid, max_id, count)); + cursor = database.queryFinalized(String.format(Locale.US, "SELECT data FROM user_photos WHERE uid = %d AND id < %d ORDER BY id DESC LIMIT %d", did, max_id, count)); } else { - cursor = database.queryFinalized(String.format(Locale.US, "SELECT data FROM user_photos WHERE uid = %d ORDER BY id DESC LIMIT %d,%d", uid, offset, count)); + cursor = database.queryFinalized(String.format(Locale.US, "SELECT data FROM user_photos WHERE uid = %d ORDER BY id DESC LIMIT %d,%d", did, offset, count)); } final TLRPC.photos_Photos res = new TLRPC.photos_Photos(); @@ -1259,7 +1369,7 @@ public class MessagesStorage { Utilities.stageQueue.postRunnable(new Runnable() { @Override public void run() { - MessagesController.getInstance().processLoadedUserPhotos(res, uid, offset, count, max_id, true, classGuid); + MessagesController.getInstance().processLoadedUserPhotos(res, did, offset, count, max_id, true, classGuid); } }); } catch (Exception e) { @@ -1295,7 +1405,7 @@ public class MessagesStorage { }); } - public void putUserPhotos(final int uid, final TLRPC.photos_Photos photos) { + public void putDialogPhotos(final int did, final TLRPC.photos_Photos photos) { if (photos == null || photos.photos.isEmpty()) { return; } @@ -1311,7 +1421,7 @@ public class MessagesStorage { state.requery(); NativeByteBuffer data = new NativeByteBuffer(photo.getObjectSize()); photo.serializeToStream(data); - state.bindInteger(1, uid); + state.bindInteger(1, did); state.bindLong(2, photo.id); state.bindByteBuffer(3, data); state.step(); @@ -1429,7 +1539,7 @@ public class MessagesStorage { }); } - private void updateDialogsWithReadMessagesInternal(final ArrayList messages, final SparseArray inbox) { + private void updateDialogsWithReadMessagesInternal(final ArrayList messages, final SparseArray inbox, final SparseArray outbox) { try { HashMap dialogsToUpdate = new HashMap<>(); @@ -1454,24 +1564,39 @@ public class MessagesStorage { } } cursor.dispose(); - } else if (inbox != null && inbox.size() != 0) { - for (int b = 0; b < inbox.size(); b++) { - int key = inbox.keyAt(b); - long messageId = inbox.get(key); - 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", key, messageId)); - if (cursor.next()) { - int count = cursor.intValue(0); - dialogsToUpdate.put((long) key, count); - } - cursor.dispose(); + } else { + if (inbox != null && inbox.size() != 0) { + for (int b = 0; b < inbox.size(); b++) { + int key = inbox.keyAt(b); + long messageId = inbox.get(key); + 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", key, messageId)); + if (cursor.next()) { + int count = cursor.intValue(0); + dialogsToUpdate.put((long) key, count); + } + cursor.dispose(); - SQLitePreparedStatement state = database.executeFast("UPDATE dialogs SET inbox_max = max((SELECT inbox_max FROM dialogs WHERE did = ?), ?) WHERE did = ?"); - state.requery(); - state.bindLong(1, key); - state.bindInteger(2, (int) messageId); - state.bindLong(3, key); - state.step(); - state.dispose(); + SQLitePreparedStatement state = database.executeFast("UPDATE dialogs SET inbox_max = max((SELECT inbox_max FROM dialogs WHERE did = ?), ?) WHERE did = ?"); + state.requery(); + state.bindLong(1, key); + state.bindInteger(2, (int) messageId); + state.bindLong(3, key); + state.step(); + state.dispose(); + } + } + if (outbox != null && outbox.size() != 0) { + for (int b = 0; b < outbox.size(); b++) { + int key = outbox.keyAt(b); + long messageId = outbox.get(key); + SQLitePreparedStatement state = database.executeFast("UPDATE dialogs SET outbox_max = max((SELECT outbox_max FROM dialogs WHERE did = ?), ?) WHERE did = ?"); + state.requery(); + state.bindLong(1, key); + state.bindInteger(2, (int) messageId); + state.bindLong(3, key); + state.step(); + state.dispose(); + } } } @@ -1496,7 +1621,7 @@ public class MessagesStorage { } } - public void updateDialogsWithReadMessages(final SparseArray inbox, boolean useQueue) { + public void updateDialogsWithReadMessages(final SparseArray inbox, final SparseArray outbox, boolean useQueue) { if (inbox.size() == 0) { return; } @@ -1504,11 +1629,11 @@ public class MessagesStorage { storageQueue.postRunnable(new Runnable() { @Override public void run() { - updateDialogsWithReadMessagesInternal(null, inbox); + updateDialogsWithReadMessagesInternal(null, inbox, outbox); } }); } else { - updateDialogsWithReadMessagesInternal(null, inbox); + updateDialogsWithReadMessagesInternal(null, inbox, outbox); } } @@ -1617,31 +1742,30 @@ public class MessagesStorage { data.reuse(); if (info instanceof TLRPC.TL_channelFull) { - SQLiteCursor cursor = database.queryFinalized("SELECT date, last_mid_i, pts, date_i, last_mid, inbox_max FROM dialogs WHERE did = " + (-info.id)); + SQLiteCursor cursor = database.queryFinalized("SELECT date, pts, last_mid, inbox_max, outbox_max FROM dialogs WHERE did = " + (-info.id)); if (cursor.next()) { - int inbox_max = cursor.intValue(5); - if (inbox_max < info.read_inbox_max_id) { + int inbox_max = cursor.intValue(3); + if (inbox_max <= info.read_inbox_max_id) { int inbox_diff = info.read_inbox_max_id - inbox_max; - if (inbox_diff < info.unread_important_count) { - info.unread_important_count = inbox_diff; + if (inbox_diff < info.unread_count) { + info.unread_count = inbox_diff; } int dialog_date = cursor.intValue(0); - long last_mid_i = cursor.longValue(1); - int pts = cursor.intValue(2); - int dialog_date_i = cursor.intValue(3); - long last_mid = cursor.longValue(4); + int pts = cursor.intValue(1); + long last_mid = cursor.longValue(2); + int outbox_max = cursor.intValue(4); state = database.executeFast("REPLACE INTO dialogs VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); state.bindLong(1, -info.id); state.bindInteger(2, dialog_date); - state.bindInteger(3, info.unread_important_count); + state.bindInteger(3, info.unread_count); state.bindLong(4, last_mid); state.bindInteger(5, info.read_inbox_max_id); - state.bindInteger(6, 0); - state.bindLong(7, last_mid_i); - state.bindInteger(8, info.unread_count); + state.bindInteger(6, Math.max(outbox_max, info.read_outbox_max_id)); + state.bindLong(7, 0); + state.bindInteger(8, 0); state.bindInteger(9, pts); - state.bindInteger(10, dialog_date_i); + state.bindInteger(10, 0); state.step(); state.dispose(); } @@ -1921,57 +2045,48 @@ public class MessagesStorage { }); } - public void processPendingRead(final long dialog_id, final long max_id, final int max_date, final boolean delete) { + public void processPendingRead(final long dialog_id, final long max_id, final int max_date) { storageQueue.postRunnable(new Runnable() { @Override public void run() { try { - if (delete) { - //database.executeFast("DELETE FROM pending_read WHERE uid = " + dialog_id).stepThis().dispose(); - } else { - database.beginTransaction(); - SQLitePreparedStatement state;/* = database.executeFast("REPLACE INTO pending_read VALUES(?, ?)"); + database.beginTransaction(); + SQLitePreparedStatement state; + + int lower_id = (int) dialog_id; + + if (lower_id != 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();*/ - - int lower_id = (int) dialog_id; - - if (lower_id != 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.bindLong(2, max_id); - state.step(); - state.dispose(); - } else { - 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); - state.step(); - state.dispose(); - } - - int currentMaxId = 0; - SQLiteCursor cursor = database.queryFinalized("SELECT inbox_max FROM dialogs WHERE did = " + dialog_id); - if (cursor.next()) { - currentMaxId = cursor.intValue(0); - } - cursor.dispose(); - currentMaxId = Math.max(currentMaxId, (int) max_id); - - state = database.executeFast("UPDATE dialogs SET unread_count = 0, unread_count_i = 0, inbox_max = ? WHERE did = ?"); - state.requery(); - state.bindInteger(1, currentMaxId); - state.bindLong(2, dialog_id); + state.bindLong(2, max_id); + state.step(); + state.dispose(); + } else { + 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); state.step(); state.dispose(); - - database.commitTransaction(); } + + int currentMaxId = 0; + SQLiteCursor cursor = database.queryFinalized("SELECT inbox_max FROM dialogs WHERE did = " + dialog_id); + if (cursor.next()) { + currentMaxId = cursor.intValue(0); + } + cursor.dispose(); + currentMaxId = Math.max(currentMaxId, (int) max_id); + + state = database.executeFast("UPDATE dialogs SET unread_count = 0, unread_count_i = 0, inbox_max = ? WHERE did = ?"); + state.requery(); + state.bindInteger(1, currentMaxId); + state.bindLong(2, dialog_id); + state.step(); + state.dispose(); + + database.commitTransaction(); } catch (Exception e) { FileLog.e("tmessages", e); } @@ -2122,7 +2237,7 @@ public class MessagesStorage { contactHashMap.clear(); FileLog.e("tmessages", e); } - ContactsController.getInstance().performSyncPhoneBook(contactHashMap, true, true, false); + ContactsController.getInstance().performSyncPhoneBook(contactHashMap, true, true, false, false); } }); } @@ -2225,7 +2340,7 @@ public class MessagesStorage { addUsersAndChatsFromMessage(message, usersToLoad, chatsToLoad); message.send_state = cursor.intValue(2); - if (message.to_id.channel_id == 0 && !MessageObject.isUnread(message) && lower_id != 0 || message.id > 0) { + if (message.to_id.channel_id == 0 && !MessageObject.isUnread(message) && lower_id != 0 || message.id > 0) { //TODO check message.send_state = 0; } if (lower_id == 0 && !cursor.isNull(5)) { @@ -2302,7 +2417,7 @@ public class MessagesStorage { return result[0]; } - public void getMessages(final long dialog_id, final int count, final int max_id, final int minDate, final int classGuid, final int load_type, final int important, final int loadIndex) { + public void getMessages(final long dialog_id, final int count, final int max_id, final int minDate, final int classGuid, final int load_type, final boolean isChannel, final int loadIndex) { storageQueue.postRunnable(new Runnable() { @Override public void run() { @@ -2317,7 +2432,7 @@ public class MessagesStorage { long messageMaxId = max_id; int max_id_query = max_id; int channelId = 0; - if (important != 0) { + if (isChannel) { channelId = -(int) dialog_id; } if (messageMaxId != 0 && channelId != 0) { @@ -2335,9 +2450,6 @@ public class MessagesStorage { SQLiteCursor cursor; int lower_id = (int) dialog_id; if (lower_id != 0) { - String imp = important == 2 ? " AND imp = 1 " : ""; - String holesTable = important == 2 ? "messages_imp_holes" : "messages_holes"; - if (load_type != 1 && load_type != 3 && minDate == 0) { if (load_type == 2) { cursor = database.queryFinalized("SELECT inbox_max, unread_count, date FROM dialogs WHERE did = " + dialog_id); @@ -2352,14 +2464,14 @@ public class MessagesStorage { } cursor.dispose(); if (!queryFromServer) { - 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" + imp, 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 " + imp + "AND out = 0 AND read_state IN(0,2)", 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); } @@ -2367,13 +2479,13 @@ public class MessagesStorage { } } else if (max_id_query == 0) { int existingUnreadCount = 0; - cursor = database.queryFinalized(String.format(Locale.US, "SELECT COUNT(*) FROM messages WHERE uid = %d AND mid > 0 " + imp + "AND out = 0 AND read_state IN(0,2)", dialog_id)); + cursor = database.queryFinalized(String.format(Locale.US, "SELECT COUNT(*) FROM messages WHERE uid = %d AND mid > 0 AND out = 0 AND read_state IN(0,2)", dialog_id)); if (cursor.next()) { existingUnreadCount = cursor.intValue(0); } cursor.dispose(); if (existingUnreadCount == count_unread) { - cursor = database.queryFinalized(String.format(Locale.US, "SELECT min(mid) FROM messages WHERE uid = %d AND out = 0 AND read_state IN(0,2) AND mid > 0" + imp, dialog_id)); + cursor = database.queryFinalized(String.format(Locale.US, "SELECT min(mid) FROM messages WHERE uid = %d AND out = 0 AND read_state IN(0,2) AND mid > 0", dialog_id)); if (cursor.next()) { messageMaxId = max_id_query = min_unread_id = cursor.intValue(0); if (messageMaxId != 0 && channelId != 0) { @@ -2383,12 +2495,12 @@ public class MessagesStorage { cursor.dispose(); } } else { - cursor = database.queryFinalized(String.format(Locale.US, "SELECT start, end FROM " + holesTable + " WHERE uid = %d AND start < %d AND end > %d", dialog_id, max_id_query, max_id_query)); + cursor = database.queryFinalized(String.format(Locale.US, "SELECT start, end FROM messages_holes WHERE uid = %d AND start < %d AND end > %d", dialog_id, max_id_query, max_id_query)); boolean containMessage = !cursor.next(); cursor.dispose(); if (containMessage) { - cursor = database.queryFinalized(String.format(Locale.US, "SELECT min(mid) FROM messages WHERE uid = %d AND out = 0 AND read_state IN(0,2) AND mid > %d" + imp, dialog_id, max_id_query)); + cursor = database.queryFinalized(String.format(Locale.US, "SELECT min(mid) FROM messages WHERE uid = %d AND out = 0 AND read_state IN(0,2) AND mid > %d", dialog_id, max_id_query)); if (cursor.next()) { messageMaxId = max_id_query = cursor.intValue(0); if (messageMaxId != 0 && channelId != 0) { @@ -2415,7 +2527,7 @@ public class MessagesStorage { } } - cursor = database.queryFinalized(String.format(Locale.US, "SELECT start FROM " + holesTable + " WHERE uid = %d AND start IN (0, 1)", dialog_id)); + cursor = database.queryFinalized(String.format(Locale.US, "SELECT start FROM messages_holes WHERE uid = %d AND start IN (0, 1)", dialog_id)); if (cursor.next()) { isEnd = cursor.intValue(0) == 1; cursor.dispose(); @@ -2425,7 +2537,7 @@ public class MessagesStorage { if (cursor.next()) { int mid = cursor.intValue(0); if (mid != 0) { - SQLitePreparedStatement state = database.executeFast("REPLACE INTO " + holesTable + " VALUES(?, ?, ?)"); + SQLitePreparedStatement state = database.executeFast("REPLACE INTO messages_holes VALUES(?, ?, ?)"); state.requery(); state.bindLong(1, dialog_id); state.bindInteger(2, 0); @@ -2446,7 +2558,7 @@ public class MessagesStorage { boolean containMessage = max_id_query != 0; if (containMessage) { - cursor = database.queryFinalized(String.format(Locale.US, "SELECT start FROM " + holesTable + " WHERE uid = %d AND start < %d AND end > %d", dialog_id, max_id_query, max_id_query)); + cursor = database.queryFinalized(String.format(Locale.US, "SELECT start FROM messages_holes WHERE uid = %d AND start < %d AND end > %d", dialog_id, max_id_query, max_id_query)); if (cursor.next()) { containMessage = false; } @@ -2456,7 +2568,7 @@ public class MessagesStorage { if (containMessage) { long holeMessageMaxId = 0; long holeMessageMinId = 1; - cursor = database.queryFinalized(String.format(Locale.US, "SELECT start FROM " + holesTable + " WHERE uid = %d AND start >= %d ORDER BY start ASC LIMIT 1", dialog_id, max_id_query)); + cursor = database.queryFinalized(String.format(Locale.US, "SELECT start FROM messages_holes WHERE uid = %d AND start >= %d ORDER BY start ASC LIMIT 1", dialog_id, max_id_query)); if (cursor.next()) { holeMessageMaxId = cursor.intValue(0); if (channelId != 0) { @@ -2464,7 +2576,7 @@ public class MessagesStorage { } } cursor.dispose(); - cursor = database.queryFinalized(String.format(Locale.US, "SELECT end FROM " + holesTable + " WHERE uid = %d AND end <= %d ORDER BY end DESC LIMIT 1", dialog_id, max_id_query)); + cursor = database.queryFinalized(String.format(Locale.US, "SELECT end FROM messages_holes WHERE uid = %d AND end <= %d ORDER BY end DESC LIMIT 1", dialog_id, max_id_query)); if (cursor.next()) { holeMessageMinId = cursor.intValue(0); if (channelId != 0) { @@ -2483,18 +2595,18 @@ public class MessagesStorage { holeMessageMaxId |= ((long) channelId) << 32; } } - cursor = database.queryFinalized(String.format(Locale.US, "SELECT * FROM (SELECT m.read_state, m.data, m.send_state, m.mid, m.date, r.random_id, m.replydata, m.media, m.ttl FROM messages as m LEFT JOIN randoms as r ON r.mid = m.mid WHERE m.uid = %d AND m.mid <= %d AND m.mid >= %d " + imp + "ORDER BY m.date DESC, m.mid DESC LIMIT %d) UNION " + - "SELECT * FROM (SELECT m.read_state, m.data, m.send_state, m.mid, m.date, r.random_id, m.replydata, m.media, m.ttl FROM messages as m LEFT JOIN randoms as r ON r.mid = m.mid WHERE m.uid = %d AND m.mid > %d AND m.mid <= %d " + imp + "ORDER BY m.date ASC, m.mid ASC LIMIT %d)", dialog_id, messageMaxId, holeMessageMinId, count_query / 2, dialog_id, messageMaxId, holeMessageMaxId, count_query / 2)); + cursor = database.queryFinalized(String.format(Locale.US, "SELECT * FROM (SELECT m.read_state, m.data, m.send_state, m.mid, m.date, r.random_id, m.replydata, m.media, m.ttl FROM messages as m LEFT JOIN randoms as r ON r.mid = m.mid WHERE m.uid = %d AND m.mid <= %d AND m.mid >= %d ORDER BY m.date DESC, m.mid DESC LIMIT %d) UNION " + + "SELECT * FROM (SELECT m.read_state, m.data, m.send_state, m.mid, m.date, r.random_id, m.replydata, m.media, m.ttl FROM messages as m LEFT JOIN randoms as r ON r.mid = m.mid WHERE m.uid = %d AND m.mid > %d AND m.mid <= %d ORDER BY m.date ASC, m.mid ASC LIMIT %d)", dialog_id, messageMaxId, holeMessageMinId, count_query / 2, dialog_id, messageMaxId, holeMessageMaxId, count_query / 2)); } else { - cursor = database.queryFinalized(String.format(Locale.US, "SELECT * FROM (SELECT m.read_state, m.data, m.send_state, m.mid, m.date, r.random_id, m.replydata, m.media, m.ttl FROM messages as m LEFT JOIN randoms as r ON r.mid = m.mid WHERE m.uid = %d AND m.mid <= %d " + imp + "ORDER BY m.date DESC, m.mid DESC LIMIT %d) UNION " + - "SELECT * FROM (SELECT m.read_state, m.data, m.send_state, m.mid, m.date, r.random_id, m.replydata, m.media, m.ttl FROM messages as m LEFT JOIN randoms as r ON r.mid = m.mid WHERE m.uid = %d AND m.mid > %d " + imp + "ORDER BY m.date ASC, m.mid ASC LIMIT %d)", dialog_id, messageMaxId, count_query / 2, dialog_id, messageMaxId, count_query / 2)); + cursor = database.queryFinalized(String.format(Locale.US, "SELECT * FROM (SELECT m.read_state, m.data, m.send_state, m.mid, m.date, r.random_id, m.replydata, m.media, m.ttl FROM messages as m LEFT JOIN randoms as r ON r.mid = m.mid WHERE m.uid = %d AND m.mid <= %d ORDER BY m.date DESC, m.mid DESC LIMIT %d) UNION " + + "SELECT * FROM (SELECT m.read_state, m.data, m.send_state, m.mid, m.date, r.random_id, m.replydata, m.media, m.ttl FROM messages as m LEFT JOIN randoms as r ON r.mid = m.mid WHERE m.uid = %d AND m.mid > %d ORDER BY m.date ASC, m.mid ASC LIMIT %d)", dialog_id, messageMaxId, count_query / 2, dialog_id, messageMaxId, count_query / 2)); } } else { cursor = null; } } else if (load_type == 1) { long holeMessageId = 0; - cursor = database.queryFinalized(String.format(Locale.US, "SELECT start, end FROM " + holesTable + " WHERE uid = %d AND start >= %d AND start != 1 AND end != 1 ORDER BY start ASC LIMIT 1", dialog_id, max_id)); + cursor = database.queryFinalized(String.format(Locale.US, "SELECT start, end FROM messages_holes WHERE uid = %d AND start >= %d AND start != 1 AND end != 1 ORDER BY start ASC LIMIT 1", dialog_id, max_id)); if (cursor.next()) { holeMessageId = cursor.intValue(0); if (channelId != 0) { @@ -2503,14 +2615,14 @@ public class MessagesStorage { } cursor.dispose(); if (holeMessageId != 0) { - cursor = database.queryFinalized(String.format(Locale.US, "SELECT m.read_state, m.data, m.send_state, m.mid, m.date, r.random_id, m.replydata, m.media, m.ttl FROM messages as m LEFT JOIN randoms as r ON r.mid = m.mid WHERE m.uid = %d AND m.date >= %d AND m.mid > %d AND m.mid <= %d " + imp + "ORDER BY m.date ASC, m.mid ASC LIMIT %d", dialog_id, minDate, messageMaxId, holeMessageId, count_query)); + cursor = database.queryFinalized(String.format(Locale.US, "SELECT m.read_state, m.data, m.send_state, m.mid, m.date, r.random_id, m.replydata, m.media, m.ttl FROM messages as m LEFT JOIN randoms as r ON r.mid = m.mid WHERE m.uid = %d AND m.date >= %d AND m.mid > %d AND m.mid <= %d ORDER BY m.date ASC, m.mid ASC LIMIT %d", dialog_id, minDate, messageMaxId, holeMessageId, count_query)); } else { - cursor = database.queryFinalized(String.format(Locale.US, "SELECT m.read_state, m.data, m.send_state, m.mid, m.date, r.random_id, m.replydata, m.media, m.ttl FROM messages as m LEFT JOIN randoms as r ON r.mid = m.mid WHERE m.uid = %d AND m.date >= %d AND m.mid > %d " + imp + "ORDER BY m.date ASC, m.mid ASC LIMIT %d", dialog_id, minDate, messageMaxId, count_query)); + cursor = database.queryFinalized(String.format(Locale.US, "SELECT m.read_state, m.data, m.send_state, m.mid, m.date, r.random_id, m.replydata, m.media, m.ttl FROM messages as m LEFT JOIN randoms as r ON r.mid = m.mid WHERE m.uid = %d AND m.date >= %d AND m.mid > %d ORDER BY m.date ASC, m.mid ASC LIMIT %d", dialog_id, minDate, messageMaxId, count_query)); } } else if (minDate != 0) { if (messageMaxId != 0) { long holeMessageId = 0; - cursor = database.queryFinalized(String.format(Locale.US, "SELECT end FROM " + holesTable + " WHERE uid = %d AND end <= %d ORDER BY end DESC LIMIT 1", dialog_id, max_id)); + cursor = database.queryFinalized(String.format(Locale.US, "SELECT end FROM messages_holes WHERE uid = %d AND end <= %d ORDER BY end DESC LIMIT 1", dialog_id, max_id)); if (cursor.next()) { holeMessageId = cursor.intValue(0); if (channelId != 0) { @@ -2519,12 +2631,12 @@ public class MessagesStorage { } cursor.dispose(); if (holeMessageId != 0) { - cursor = database.queryFinalized(String.format(Locale.US, "SELECT m.read_state, m.data, m.send_state, m.mid, m.date, r.random_id, m.replydata, m.media, m.ttl FROM messages as m LEFT JOIN randoms as r ON r.mid = m.mid WHERE m.uid = %d AND m.date <= %d AND m.mid < %d AND (m.mid >= %d OR m.mid < 0) " + imp + "ORDER BY m.date DESC, m.mid DESC LIMIT %d", dialog_id, minDate, messageMaxId, holeMessageId, count_query)); + cursor = database.queryFinalized(String.format(Locale.US, "SELECT m.read_state, m.data, m.send_state, m.mid, m.date, r.random_id, m.replydata, m.media, m.ttl FROM messages as m LEFT JOIN randoms as r ON r.mid = m.mid WHERE m.uid = %d AND m.date <= %d AND m.mid < %d AND (m.mid >= %d OR m.mid < 0) ORDER BY m.date DESC, m.mid DESC LIMIT %d", dialog_id, minDate, messageMaxId, holeMessageId, count_query)); } else { - cursor = database.queryFinalized(String.format(Locale.US, "SELECT m.read_state, m.data, m.send_state, m.mid, m.date, r.random_id, m.replydata, m.media, m.ttl FROM messages as m LEFT JOIN randoms as r ON r.mid = m.mid WHERE m.uid = %d AND m.date <= %d AND m.mid < %d " + imp + "ORDER BY m.date DESC, m.mid DESC LIMIT %d", dialog_id, minDate, messageMaxId, count_query)); + cursor = database.queryFinalized(String.format(Locale.US, "SELECT m.read_state, m.data, m.send_state, m.mid, m.date, r.random_id, m.replydata, m.media, m.ttl FROM messages as m LEFT JOIN randoms as r ON r.mid = m.mid WHERE m.uid = %d AND m.date <= %d AND m.mid < %d ORDER BY m.date DESC, m.mid DESC LIMIT %d", dialog_id, minDate, messageMaxId, count_query)); } } else { - cursor = database.queryFinalized(String.format(Locale.US, "SELECT m.read_state, m.data, m.send_state, m.mid, m.date, r.random_id, m.replydata, m.media, m.ttl FROM messages as m LEFT JOIN randoms as r ON r.mid = m.mid WHERE m.uid = %d AND m.date <= %d " + imp + "ORDER BY m.date DESC, m.mid DESC LIMIT %d,%d", dialog_id, minDate, offset_query, count_query)); + cursor = database.queryFinalized(String.format(Locale.US, "SELECT m.read_state, m.data, m.send_state, m.mid, m.date, r.random_id, m.replydata, m.media, m.ttl FROM messages as m LEFT JOIN randoms as r ON r.mid = m.mid WHERE m.uid = %d AND m.date <= %d ORDER BY m.date DESC, m.mid DESC LIMIT %d,%d", dialog_id, minDate, offset_query, count_query)); } } else { cursor = database.queryFinalized(String.format(Locale.US, "SELECT max(mid) FROM messages WHERE uid = %d AND mid > 0", dialog_id)); @@ -2534,7 +2646,7 @@ public class MessagesStorage { cursor.dispose(); long holeMessageId = 0; - cursor = database.queryFinalized(String.format(Locale.US, "SELECT max(end) FROM " + holesTable + " WHERE uid = %d", dialog_id)); + cursor = database.queryFinalized(String.format(Locale.US, "SELECT max(end) FROM messages_holes WHERE uid = %d", dialog_id)); if (cursor.next()) { holeMessageId = cursor.intValue(0); if (channelId != 0) { @@ -2543,9 +2655,9 @@ public class MessagesStorage { } cursor.dispose(); if (holeMessageId != 0) { - cursor = database.queryFinalized(String.format(Locale.US, "SELECT m.read_state, m.data, m.send_state, m.mid, m.date, r.random_id, m.replydata, m.media, m.ttl FROM messages as m LEFT JOIN randoms as r ON r.mid = m.mid WHERE m.uid = %d AND (m.mid >= %d OR m.mid < 0) " + imp + "ORDER BY m.date DESC, m.mid DESC LIMIT %d,%d", dialog_id, holeMessageId, offset_query, count_query)); + cursor = database.queryFinalized(String.format(Locale.US, "SELECT m.read_state, m.data, m.send_state, m.mid, m.date, r.random_id, m.replydata, m.media, m.ttl FROM messages as m LEFT JOIN randoms as r ON r.mid = m.mid WHERE m.uid = %d AND (m.mid >= %d OR m.mid < 0) ORDER BY m.date DESC, m.mid DESC LIMIT %d,%d", dialog_id, holeMessageId, offset_query, count_query)); } else { - cursor = database.queryFinalized(String.format(Locale.US, "SELECT m.read_state, m.data, m.send_state, m.mid, m.date, r.random_id, m.replydata, m.media, m.ttl FROM messages as m LEFT JOIN randoms as r ON r.mid = m.mid WHERE m.uid = %d " + imp + "ORDER BY m.date DESC, m.mid DESC LIMIT %d,%d", dialog_id, offset_query, count_query)); + cursor = database.queryFinalized(String.format(Locale.US, "SELECT m.read_state, m.data, m.send_state, m.mid, m.date, r.random_id, m.replydata, m.media, m.ttl FROM messages as m LEFT JOIN randoms as r ON r.mid = m.mid WHERE m.uid = %d ORDER BY m.date DESC, m.mid DESC LIMIT %d,%d", dialog_id, offset_query, count_query)); } } } else { @@ -2717,22 +2829,6 @@ public class MessagesStorage { res.messages.clear(); } - if (important == 2 && !res.messages.isEmpty()) { - if (max_id != 0) { - cursor = database.queryFinalized(String.format(Locale.US, "SELECT start, end, count FROM channel_group WHERE uid = %d AND ((start >= %d AND end <= %d) OR (start = %d))", dialog_id, res.messages.get(res.messages.size() - 1).id, res.messages.get(0).id, res.messages.get(0).id)); - } else { - cursor = database.queryFinalized(String.format(Locale.US, "SELECT start, end, count FROM channel_group WHERE uid = %d AND start >= %d", dialog_id, res.messages.get(res.messages.size() - 1).id)); - } - while (cursor.next()) { - TLRPC.TL_messageGroup group = new TLRPC.TL_messageGroup(); - group.min_id = cursor.intValue(0); - group.max_id = cursor.intValue(1); - group.count = cursor.intValue(2); - res.collapsed.add(group); - } - cursor.dispose(); - } - if (!replyMessages.isEmpty()) { if (!replyMessageOwners.isEmpty()) { cursor = database.queryFinalized(String.format(Locale.US, "SELECT data, mid, date FROM messages WHERE mid IN(%s)", TextUtils.join(",", replyMessages))); @@ -2790,10 +2886,9 @@ public class MessagesStorage { res.messages.clear(); res.chats.clear(); res.users.clear(); - res.collapsed.clear(); FileLog.e("tmessages", e); } finally { - MessagesController.getInstance().processLoadedMessages(res, dialog_id, count_query, max_id, true, classGuid, min_unread_id, last_message_id, count_unread, max_unread_date, load_type, important, isEnd, loadIndex, queryFromServer); + MessagesController.getInstance().processLoadedMessages(res, dialog_id, count_query, max_id, true, classGuid, min_unread_id, last_message_id, count_unread, max_unread_date, load_type, isChannel, isEnd, loadIndex, queryFromServer); } } }); @@ -2862,7 +2957,7 @@ public class MessagesStorage { if (file instanceof TLRPC.TL_messageMediaDocument) { result.add(((TLRPC.TL_messageMediaDocument) file).document); } else if (file instanceof TLRPC.TL_messageMediaPhoto) { - result.add(((TLRPC.TL_messageMediaDocument) file).photo); + result.add(((TLRPC.TL_messageMediaPhoto) file).photo); } } } @@ -2940,7 +3035,7 @@ public class MessagesStorage { state = database.executeFast("UPDATE enc_chats SET seq_in = ?, seq_out = ?, use_count = ? WHERE uid = ?"); state.bindInteger(1, chat.seq_in); state.bindInteger(2, chat.seq_out); - state.bindInteger(3, (int)chat.key_use_count_in << 16 | chat.key_use_count_out); + state.bindInteger(3, (int) chat.key_use_count_in << 16 | chat.key_use_count_out); state.bindInteger(4, chat.id); state.step(); } catch (Exception e) { @@ -3091,6 +3186,31 @@ public class MessagesStorage { return result[0]; } + public boolean hasAuthMessage(final int date) { + final Semaphore semaphore = new Semaphore(0); + final boolean result[] = new boolean[1]; + storageQueue.postRunnable(new Runnable() { + @Override + public void run() { + try { + SQLiteCursor cursor = database.queryFinalized(String.format(Locale.US, "SELECT mid FROM messages WHERE uid = 777000 AND date = %d AND mid < 0 LIMIT 1", date)); + result[0] = cursor.next(); + cursor.dispose(); + } catch (Exception e) { + FileLog.e("tmessages", e); + } finally { + semaphore.release(); + } + } + }); + try { + semaphore.acquire(); + } catch (Exception e) { + FileLog.e("tmessages", e); + } + return result[0]; + } + public void getEncryptedChat(final int chat_id, final Semaphore semaphore, final ArrayList result) { if (semaphore == null || result == null) { return; @@ -3119,7 +3239,7 @@ public class MessagesStorage { }); } - public void putEncryptedChat(final TLRPC.EncryptedChat chat, final TLRPC.User user, final TLRPC.Dialog dialog) { + public void putEncryptedChat(final TLRPC.EncryptedChat chat, final TLRPC.User user, final TLRPC.TL_dialog dialog) { if (chat == null) { return; } @@ -3181,9 +3301,9 @@ public class MessagesStorage { 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.bindInteger(7, dialog.top_not_important_message); - state.bindInteger(8, dialog.unread_not_important_count); + state.bindInteger(6, dialog.read_outbox_max_id); + state.bindInteger(7, 0); + state.bindInteger(8, 0); state.bindInteger(9, dialog.pts); state.bindInteger(10, 0); state.step(); @@ -3235,14 +3355,14 @@ public class MessagesStorage { oldUser.flags |= 8; } else { oldUser.username = null; - oldUser.flags = oldUser.flags &~ 8; + oldUser.flags = oldUser.flags & ~8; } if (user.photo != null) { oldUser.photo = user.photo; oldUser.flags |= 32; } else { oldUser.photo = null; - oldUser.flags = oldUser.flags &~ 32; + oldUser.flags = oldUser.flags & ~32; } user = oldUser; } @@ -3304,7 +3424,7 @@ public class MessagesStorage { oldChat.flags |= 64; } else { oldChat.username = null; - oldChat.flags = oldChat.flags &~ 64; + oldChat.flags = oldChat.flags & ~64; } chat = oldChat; } @@ -3313,6 +3433,7 @@ public class MessagesStorage { FileLog.e("tmessages", e); } } + cursor.dispose(); } state.requery(); NativeByteBuffer data = new NativeByteBuffer(chat.getObjectSize()); @@ -3401,8 +3522,8 @@ public class MessagesStorage { chat.seq_in = cursor.intValue(6); chat.seq_out = cursor.intValue(7); int use_count = cursor.intValue(8); - chat.key_use_count_in = (short)(use_count >> 16); - chat.key_use_count_out = (short)(use_count); + chat.key_use_count_in = (short) (use_count >> 16); + chat.key_use_count_out = (short) (use_count); chat.exchange_id = cursor.longValue(9); chat.key_create_date = cursor.intValue(10); chat.future_key_fingerprint = cursor.longValue(11); @@ -3532,8 +3653,8 @@ public class MessagesStorage { private int getMessageMediaType(TLRPC.Message message) { if (message instanceof TLRPC.TL_message_secret && ( message.media instanceof TLRPC.TL_messageMediaPhoto && message.ttl > 0 && message.ttl <= 60 || - MessageObject.isVoiceMessage(message) || - MessageObject.isVideoMessage(message))) { + MessageObject.isVoiceMessage(message) || + MessageObject.isVideoMessage(message))) { return 1; } else if (message.media instanceof TLRPC.TL_messageMediaPhoto || MessageObject.isVideoMessage(message)) { return 0; @@ -3643,12 +3764,10 @@ public class MessagesStorage { } database.executeFast("DELETE FROM messages WHERE uid = " + did).stepThis().dispose(); - database.executeFast("DELETE FROM channel_group WHERE uid = " + did).stepThis().dispose(); database.executeFast("DELETE FROM bot_keyboard WHERE uid = " + did).stepThis().dispose(); database.executeFast("DELETE FROM media_counts_v2 WHERE uid = " + did).stepThis().dispose(); database.executeFast("DELETE FROM media_v2 WHERE uid = " + did).stepThis().dispose(); database.executeFast("DELETE FROM messages_holes WHERE uid = " + did).stepThis().dispose(); - database.executeFast("DELETE FROM messages_imp_holes WHERE uid = " + did).stepThis().dispose(); database.executeFast("DELETE FROM media_holes_v2 WHERE uid = " + did).stepThis().dispose(); BotQuery.clearBotKeyboard(did, null); @@ -3656,15 +3775,15 @@ public class MessagesStorage { dialogs.chats.addAll(difference.chats); dialogs.users.addAll(difference.users); dialogs.messages.addAll(difference.messages); - TLRPC.TL_dialogChannel dialog = new TLRPC.TL_dialogChannel(); + TLRPC.TL_dialog dialog = new TLRPC.TL_dialog(); dialog.id = did; + dialog.flags = 1; dialog.peer = new TLRPC.TL_peerChannel(); dialog.peer.channel_id = channel_id; - dialog.top_not_important_message = difference.top_message; - dialog.top_message = difference.top_important_message; + dialog.top_message = difference.top_message; dialog.read_inbox_max_id = difference.read_inbox_max_id; - dialog.unread_not_important_count = difference.unread_count; - dialog.unread_count = difference.unread_important_count; + dialog.read_outbox_max_id = difference.read_outbox_max_id; + dialog.unread_count = difference.unread_count; dialog.notify_settings = null; dialog.pts = difference.pts; dialogs.dialogs.add(dialog); @@ -3727,15 +3846,34 @@ public class MessagesStorage { return message.reply_markup != null && !(message.reply_markup instanceof TLRPC.TL_replyInlineMarkup) && (!message.reply_markup.selective || message.mentioned); } - private void putMessagesInternal(final ArrayList messages, final boolean withTransaction, final boolean doNotUpdateDialogDate, final int downloadMask) { + private void putMessagesInternal(final ArrayList messages, final boolean withTransaction, final boolean doNotUpdateDialogDate, final int downloadMask, boolean ifNoLastMessage) { try { + if (ifNoLastMessage) { + TLRPC.Message lastMessage = messages.get(0); + if (lastMessage.dialog_id == 0) { + if (lastMessage.to_id.user_id != 0) { + lastMessage.dialog_id = lastMessage.to_id.user_id; + } else if (lastMessage.to_id.chat_id != 0) { + lastMessage.dialog_id = -lastMessage.to_id.chat_id; + } else { + lastMessage.dialog_id = -lastMessage.to_id.channel_id; + } + } + int lastMid = -1; + SQLiteCursor cursor = database.queryFinalized("SELECT last_mid FROM dialogs WHERE did = " + lastMessage.dialog_id); + if (cursor.next()) { + lastMid = cursor.intValue(0); + } + cursor.dispose(); + if (lastMid != 0) { + return; + } + } if (withTransaction) { database.beginTransaction(); } HashMap messagesMap = new HashMap<>(); - HashMap messagesMapNotImportant = new HashMap<>(); HashMap messagesCounts = new HashMap<>(); - HashMap messagesCountsNotImportant = new HashMap<>(); HashMap> mediaCounts = null; HashMap botKeyboards = new HashMap<>(); @@ -3745,7 +3883,6 @@ public class MessagesStorage { StringBuilder messageIds = new StringBuilder(); HashMap dialogsReadMax = new HashMap<>(); HashMap messagesIdsMap = new HashMap<>(); - HashMap messagesIdsMapNotImportant = new HashMap<>(); SQLitePreparedStatement state = database.executeFast("REPLACE INTO messages VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?)"); SQLitePreparedStatement state2 = null; @@ -3770,7 +3907,7 @@ public class MessagesStorage { messageId |= ((long) message.to_id.channel_id) << 32; } - if ((message.to_id.channel_id == 0 && MessageObject.isUnread(message) || MessageObject.isContentUnread(message)) && !MessageObject.isOut(message)) { + if (MessageObject.isUnread(message) && !MessageObject.isOut(message)) { Integer currentMaxId = dialogsReadMax.get(message.dialog_id); if (currentMaxId == null) { SQLiteCursor cursor = database.queryFinalized("SELECT inbox_max FROM dialogs WHERE did = " + message.dialog_id); @@ -3787,11 +3924,7 @@ public class MessagesStorage { messageIds.append(","); } messageIds.append(messageId); - if (message.to_id.channel_id == 0 || MessageObject.isMegagroup(message) || MessageObject.isImportant(message)) { - messagesIdsMap.put(messageId, message.dialog_id); - } else if (message.to_id.channel_id != 0) { - messagesIdsMapNotImportant.put(messageId, message.dialog_id); - } + messagesIdsMap.put(messageId, message.dialog_id); } } if (SharedMediaQuery.canAddMessageToMedia(message)) { @@ -3850,7 +3983,6 @@ public class MessagesStorage { SQLiteCursor cursor = database.queryFinalized("SELECT mid FROM messages WHERE mid IN(" + messageIds.toString() + ")"); while (cursor.next()) { messagesIdsMap.remove(cursor.longValue(0)); - messagesIdsMapNotImportant.remove(cursor.longValue(0)); } cursor.dispose(); for (Long dialog_id : messagesIdsMap.values()) { @@ -3861,14 +3993,6 @@ public class MessagesStorage { count++; messagesCounts.put(dialog_id, count); } - for (Long dialog_id : messagesIdsMapNotImportant.values()) { - Integer count = messagesCountsNotImportant.get(dialog_id); - if (count == null) { - count = 0; - } - count++; - messagesCountsNotImportant.put(dialog_id, count); - } } int downloadMediaMask = 0; @@ -3894,17 +4018,9 @@ public class MessagesStorage { } if (updateDialog) { - TLRPC.Message lastMessage; - if (message.to_id.channel_id == 0 || MessageObject.isMegagroup(message) || MessageObject.isImportant(message)) { - lastMessage = messagesMap.get(message.dialog_id); - if (lastMessage == null || message.date > lastMessage.date || message.id > 0 && lastMessage.id > 0 && message.id > lastMessage.id || message.id < 0 && lastMessage.id < 0 && message.id < lastMessage.id) { - messagesMap.put(message.dialog_id, message); - } - } else if (message.to_id.channel_id != 0) { - lastMessage = messagesMapNotImportant.get(message.dialog_id); - if (lastMessage == null || message.date > lastMessage.date || message.id > 0 && lastMessage.id > 0 && message.id > lastMessage.id || message.id < 0 && lastMessage.id < 0 && message.id < lastMessage.id) { - messagesMapNotImportant.put(message.dialog_id, message); - } + TLRPC.Message lastMessage = messagesMap.get(message.dialog_id); + if (lastMessage == null || message.date > lastMessage.date || message.id > 0 && lastMessage.id > 0 && message.id > lastMessage.id || message.id < 0 && lastMessage.id < 0 && message.id < lastMessage.id) { + messagesMap.put(message.dialog_id, message); } } @@ -3921,7 +4037,7 @@ public class MessagesStorage { } else { state.bindInteger(9, getMessageMediaType(message)); } - state.bindInteger(10, MessageObject.isImportant(message) ? 1 : 0); + state.bindInteger(10, 0); state.step(); if (message.random_id != 0) { @@ -3953,7 +4069,7 @@ public class MessagesStorage { data.reuse(); - if ((message.to_id.channel_id == 0 || MessageObject.isImportant(message)) && message.date >= ConnectionsManager.getInstance().getCurrentTime() - 60 * 60 && downloadMask != 0) { + if ((message.to_id.channel_id == 0 || message.post) && message.date >= ConnectionsManager.getInstance().getCurrentTime() - 60 * 60 && downloadMask != 0) { if (message.media instanceof TLRPC.TL_messageMediaPhoto || message.media instanceof TLRPC.TL_messageMediaDocument) { int type = 0; long id = 0; @@ -4020,7 +4136,6 @@ public class MessagesStorage { state = database.executeFast("REPLACE INTO dialogs VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); HashMap dids = new HashMap<>(); dids.putAll(messagesMap); - dids.putAll(messagesMapNotImportant); for (HashMap.Entry pair : dids.entrySet()) { Long key = pair.getKey(); @@ -4028,34 +4143,26 @@ public class MessagesStorage { continue; } TLRPC.Message message = messagesMap.get(key); - TLRPC.Message messageNotImportant = messagesMapNotImportant.get(key); int channelId = 0; if (message != null) { channelId = message.to_id.channel_id; } - if (messageNotImportant != null) { - channelId = messageNotImportant.to_id.channel_id; - } - SQLiteCursor cursor = database.queryFinalized("SELECT date, unread_count, last_mid_i, unread_count_i, pts, date_i, last_mid, inbox_max FROM dialogs WHERE did = " + key); + SQLiteCursor cursor = database.queryFinalized("SELECT date, unread_count, pts, last_mid, inbox_max, outbox_max FROM dialogs WHERE did = " + key); int dialog_date = 0; int last_mid = 0; int old_unread_count = 0; - int last_mid_i = 0; - int old_unread_count_i = 0; int pts = channelId != 0 ? 1 : 0; - int dialog_date_i = 0; int inbox_max = 0; + int outbox_max = 0; if (cursor.next()) { dialog_date = cursor.intValue(0); old_unread_count = cursor.intValue(1); - last_mid_i = cursor.intValue(2); - old_unread_count_i = cursor.intValue(3); - pts = cursor.intValue(4); - dialog_date_i = cursor.intValue(5); - last_mid = cursor.intValue(6); - inbox_max = cursor.intValue(7); + pts = cursor.intValue(2); + last_mid = cursor.intValue(3); + inbox_max = cursor.intValue(4); + outbox_max = cursor.intValue(5); } else if (channelId != 0) { MessagesController.getInstance().checkChannelInviter(channelId); } @@ -4067,28 +4174,15 @@ public class MessagesStorage { } else { messagesCounts.put(key, unread_count + old_unread_count); } - Integer unread_count_i = messagesCountsNotImportant.get(key); - if (unread_count_i == null) { - unread_count_i = 0; - } else { - messagesCountsNotImportant.put(key, unread_count_i + old_unread_count_i); - } long messageId = message != null ? message.id : last_mid; if (message != null) { if (message.local_id != 0) { messageId = message.local_id; } } - long messageIdNotImportant = messageNotImportant != null ? messageNotImportant.id : last_mid_i; - if (messageNotImportant != null) { - if (messageNotImportant.local_id != 0) { - messageIdNotImportant = messageNotImportant.local_id; - } - } if (channelId != 0) { messageId |= ((long) channelId) << 32; - messageIdNotImportant |= ((long) channelId) << 32; } state.requery(); @@ -4101,15 +4195,11 @@ public class MessagesStorage { state.bindInteger(3, old_unread_count + unread_count); state.bindLong(4, messageId); state.bindInteger(5, inbox_max); - state.bindInteger(6, 0); - state.bindLong(7, messageIdNotImportant); - state.bindInteger(8, unread_count_i + old_unread_count_i); + state.bindInteger(6, outbox_max); + state.bindLong(7, 0); + state.bindInteger(8, 0); state.bindInteger(9, pts); - if (messageNotImportant != null && (!doNotUpdateDialogDate || dialog_date == 0)) { - state.bindInteger(10, messageNotImportant.date); - } else { - state.bindInteger(10, dialog_date_i); - } + state.bindInteger(10, 0); state.step(); } state.dispose(); @@ -4159,6 +4249,10 @@ public class MessagesStorage { } public void putMessages(final ArrayList messages, final boolean withTransaction, boolean useQueue, final boolean doNotUpdateDialogDate, final int downloadMask) { + putMessages(messages, withTransaction, useQueue, doNotUpdateDialogDate, downloadMask, false); + } + + public void putMessages(final ArrayList messages, final boolean withTransaction, boolean useQueue, final boolean doNotUpdateDialogDate, final int downloadMask, final boolean ifNoLastMessage) { if (messages.size() == 0) { return; } @@ -4166,11 +4260,11 @@ public class MessagesStorage { storageQueue.postRunnable(new Runnable() { @Override public void run() { - putMessagesInternal(messages, withTransaction, doNotUpdateDialogDate, downloadMask); + putMessagesInternal(messages, withTransaction, doNotUpdateDialogDate, downloadMask, ifNoLastMessage); } }); } else { - putMessagesInternal(messages, withTransaction, doNotUpdateDialogDate, downloadMask); + putMessagesInternal(messages, withTransaction, doNotUpdateDialogDate, downloadMask, ifNoLastMessage); } } @@ -4324,7 +4418,7 @@ public class MessagesStorage { } } - return new long[] {did, newId}; + return new long[]{did, newId}; } else { SQLitePreparedStatement state = null; try { @@ -4377,7 +4471,7 @@ public class MessagesStorage { } } - return new long[] {did, _oldId}; + return new long[]{did, _oldId}; } } @@ -4479,7 +4573,7 @@ public class MessagesStorage { } } - private void markMessagesAsReadInternal(SparseArray inbox, SparseIntArray outbox, HashMap encryptedMessages) { + private void markMessagesAsReadInternal(SparseArray inbox, SparseArray outbox, HashMap encryptedMessages) { try { if (inbox != null) { for (int b = 0; b < inbox.size(); b++) { @@ -4491,13 +4585,13 @@ public class MessagesStorage { if (outbox != null) { for (int b = 0; b < outbox.size(); b++) { int key = outbox.keyAt(b); - int messageId = outbox.get(key); + long messageId = outbox.get(key); database.executeFast(String.format(Locale.US, "UPDATE messages SET read_state = read_state | 1 WHERE uid = %d AND mid > 0 AND mid <= %d AND read_state IN(0,2) AND out = 1", key, messageId)).stepThis().dispose(); } } if (encryptedMessages != null && !encryptedMessages.isEmpty()) { for (HashMap.Entry entry : encryptedMessages.entrySet()) { - long dialog_id = ((long)entry.getKey()) << 32; + long dialog_id = ((long) entry.getKey()) << 32; int max_date = entry.getValue(); 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(); @@ -4528,7 +4622,7 @@ public class MessagesStorage { }); } - public void markMessagesAsRead(final SparseArray inbox, final SparseIntArray outbox, final HashMap encryptedMessages, boolean useQueue) { + public void markMessagesAsRead(final SparseArray inbox, final SparseArray outbox, final HashMap encryptedMessages, boolean useQueue) { if (useQueue) { storageQueue.postRunnable(new Runnable() { @Override @@ -4563,7 +4657,7 @@ public class MessagesStorage { NotificationCenter.getInstance().postNotificationName(NotificationCenter.messagesDeleted, mids, 0); } }); - MessagesStorage.getInstance().updateDialogsWithReadMessagesInternal(mids, null); + MessagesStorage.getInstance().updateDialogsWithReadMessagesInternal(mids, null, null); MessagesStorage.getInstance().markMessagesAsDeletedInternal(mids, 0); MessagesStorage.getInstance().updateDialogsWithDeletedMessagesInternal(mids, 0); } @@ -4699,22 +4793,17 @@ public class MessagesStorage { ArrayList usersToLoad = new ArrayList<>(); ArrayList chatsToLoad = new ArrayList<>(); ArrayList encryptedToLoad = new ArrayList<>(); - SQLiteCursor cursor = database.queryFinalized(String.format(Locale.US, "SELECT d.did, d.last_mid, d.unread_count, d.date, m.data, m.read_state, m.mid, m.send_state, m.date, d.last_mid_i, d.unread_count_i, d.pts, d.inbox_max FROM dialogs as d LEFT JOIN messages as m ON d.last_mid = m.mid WHERE d.did IN(%s)", ids)); + SQLiteCursor cursor = database.queryFinalized(String.format(Locale.US, "SELECT d.did, d.last_mid, d.unread_count, d.date, m.data, m.read_state, m.mid, m.send_state, m.date, d.pts, d.inbox_max, d.outbox_max FROM dialogs as d LEFT JOIN messages as m ON d.last_mid = m.mid WHERE d.did IN(%s)", ids)); while (cursor.next()) { - TLRPC.Dialog dialog; - if (channelId == 0) { - dialog = new TLRPC.TL_dialog(); - } else { - dialog = new TLRPC.TL_dialogChannel(); - } + TLRPC.TL_dialog dialog = new TLRPC.TL_dialog(); dialog.id = cursor.longValue(0); dialog.top_message = cursor.intValue(1); - dialog.read_inbox_max_id = cursor.intValue(13); + dialog.read_inbox_max_id = cursor.intValue(10); + dialog.read_outbox_max_id = cursor.intValue(11); dialog.unread_count = cursor.intValue(2); dialog.last_message_date = cursor.intValue(3); - dialog.pts = cursor.intValue(11); - dialog.top_not_important_message = cursor.intValue(9); - dialog.unread_not_important_count = cursor.intValue(10); + dialog.pts = cursor.intValue(9); + dialog.flags = channelId == 0 ? 0 : 1; dialogs.dialogs.add(dialog); @@ -4735,8 +4824,8 @@ public class MessagesStorage { addUsersAndChatsFromMessage(message, usersToLoad, chatsToLoad); } - int lower_id = (int)dialog.id; - int high_id = (int)(dialog.id >> 32); + int lower_id = (int) dialog.id; + int high_id = (int) (dialog.id >> 32); if (lower_id != 0) { if (high_id == 1) { if (!chatsToLoad.contains(lower_id)) { @@ -5026,78 +5115,33 @@ public class MessagesStorage { } } - public void putMessages(final TLRPC.messages_Messages messages, final long dialog_id, final int load_type, final int max_id, final int important, final boolean createDialog) { + public void putMessages(final TLRPC.messages_Messages messages, final long dialog_id, final int load_type, final int max_id, final boolean createDialog) { storageQueue.postRunnable(new Runnable() { @Override public void run() { try { if (messages.messages.isEmpty()) { if (load_type == 0) { - if (important != 2) { - doneHolesInTable("messages_holes", dialog_id, max_id); - doneHolesInMedia(dialog_id, max_id, -1); - } - if (important != 0) { - doneHolesInTable("messages_imp_holes", dialog_id, max_id); - } + doneHolesInTable("messages_holes", dialog_id, max_id); + doneHolesInMedia(dialog_id, max_id, -1); } return; } database.beginTransaction(); - if (!messages.collapsed.isEmpty() && important == 2) { - int maxId, minId; - int count = messages.collapsed.size(); - for (int a = 0; a < count; a++) { - TLRPC.TL_messageGroup group = messages.collapsed.get(a); - if (a < count - 1) { - minId = group.max_id; - maxId = messages.collapsed.get(a + 1).min_id; - closeHolesInTable("messages_holes", dialog_id, minId, maxId); - closeHolesInMedia(dialog_id, minId, maxId, -1); - } - if (a == 0) { - minId = messages.messages.get(messages.messages.size() - 1).id; - maxId = minId > group.min_id ? group.max_id : group.min_id; - closeHolesInTable("messages_holes", dialog_id, minId, maxId); - closeHolesInMedia(dialog_id, minId, maxId, -1); - } - if (a == count - 1) { - maxId = messages.messages.get(0).id; - minId = maxId < group.max_id ? group.min_id : group.max_id; - closeHolesInTable("messages_holes", dialog_id, minId, maxId); - closeHolesInMedia(dialog_id, minId, maxId, -1); - } - } - } if (load_type == 0) { int minId = messages.messages.get(messages.messages.size() - 1).id; - if (important != 2 || messages.collapsed.isEmpty()) { - closeHolesInTable("messages_holes", dialog_id, minId, max_id); - closeHolesInMedia(dialog_id, minId, max_id, -1); - } - if (important != 0) { - closeHolesInTable("messages_imp_holes", dialog_id, minId, max_id); - } + closeHolesInTable("messages_holes", dialog_id, minId, max_id); + closeHolesInMedia(dialog_id, minId, max_id, -1); } else if (load_type == 1) { int maxId = messages.messages.get(0).id; - if (important != 2 || messages.collapsed.isEmpty()) { - closeHolesInTable("messages_holes", dialog_id, max_id, maxId); - closeHolesInMedia(dialog_id, max_id, maxId, -1); - } - if (important != 0) { - closeHolesInTable("messages_imp_holes", dialog_id, max_id, maxId); - } + closeHolesInTable("messages_holes", dialog_id, max_id, maxId); + closeHolesInMedia(dialog_id, max_id, maxId, -1); } else if (load_type == 3 || load_type == 2) { int maxId = max_id == 0 ? Integer.MAX_VALUE : messages.messages.get(0).id; int minId = messages.messages.get(messages.messages.size() - 1).id; - if (important != 2 || messages.collapsed.isEmpty()) { - closeHolesInTable("messages_holes", dialog_id, minId, maxId); - closeHolesInMedia(dialog_id, minId, maxId, -1); - } - if (important != 0) { - closeHolesInTable("messages_imp_holes", dialog_id, minId, maxId); - } + closeHolesInTable("messages_holes", dialog_id, minId, maxId); + closeHolesInMedia(dialog_id, minId, maxId, -1); } int count = messages.messages.size(); @@ -5110,11 +5154,8 @@ public class MessagesStorage { SQLitePreparedStatement state2 = database.executeFast("REPLACE INTO media_v2 VALUES(?, ?, ?, ?, ?)"); SQLitePreparedStatement state5 = null; TLRPC.Message botKeyboard = null; - int countBeforeImportant = 0; - int countAfterImportant = 0; int minChannelMessageId = Integer.MAX_VALUE; int maxChannelMessageId = 0; - int lastChannelImportantId = -1; int channelId = 0; for (int a = 0; a < count; a++) { TLRPC.Message message = messages.messages.get(a); @@ -5162,29 +5203,6 @@ public class MessagesStorage { state3.dispose(); } - boolean isImportant = MessageObject.isImportant(message); - if (load_type >= 0 && important == 1) { - if (isImportant) { - minChannelMessageId = Math.min(minChannelMessageId, message.id); - maxChannelMessageId = Math.max(maxChannelMessageId, message.id); - if (lastChannelImportantId == -1) { - countBeforeImportant = countAfterImportant; - } else { - if (countAfterImportant != 0) { - TLRPC.TL_messageGroup group = new TLRPC.TL_messageGroup(); - group.max_id = lastChannelImportantId; - group.min_id = message.id; - group.count = countAfterImportant; - messages.collapsed.add(group); - } - } - countAfterImportant = 0; - lastChannelImportantId = message.id; - } else { - countAfterImportant++; - } - } - fixUnsupportedMedia(message); state.requery(); NativeByteBuffer data = new NativeByteBuffer(message.getObjectSize()); @@ -5202,7 +5220,7 @@ public class MessagesStorage { } else { state.bindInteger(9, 0); } - state.bindInteger(10, isImportant ? 1 : 0); + state.bindInteger(10, 0); state.step(); if (SharedMediaQuery.canAddMessageToMedia(message)) { @@ -5240,58 +5258,6 @@ public class MessagesStorage { BotQuery.putBotKeyboard(dialog_id, botKeyboard); } - if (load_type >= 0 && important != 0) { - /*if ((messages.flags & 1) == 0) { - if (countBeforeImportant != 0) { - if (load_type == 0) { - SQLiteCursor cursor = database.queryFinalized(String.format(Locale.US, "SELECT start, count FROM channel_group WHERE uid = %d AND start <= %d ORDER BY start ASC LIMIT 1", dialog_id, maxChannelMessageId)); - if (cursor.next()) { - int currentStart = cursor.intValue(0); - int currentCount = cursor.intValue(1); - database.executeFast(String.format(Locale.US, "UPDATE channel_group SET start = %d, count = %d WHERE uid = %d AND start = %d", maxChannelMessageId, cursor.intValue(1) + countBeforeImportant, dialog_id, cursor.intValue(0))).stepThis().dispose(); - } else { - TLRPC.TL_messageGroup group = new TLRPC.TL_messageGroup(); - group.max_id = max_id != 0 ? max_id : Integer.MAX_VALUE; - group.min_id = maxChannelMessageId; - group.count = countBeforeImportant; - messages.collapsed.add(group); - } - cursor.dispose(); - } - } - if (countAfterImportant != 0) { - if (load_type == 0) { - TLRPC.TL_messageGroup group = new TLRPC.TL_messageGroup(); - group.max_id = minChannelMessageId; - group.min_id = 0; - group.count = countBeforeImportant; - messages.collapsed.add(group); - } - } - }*/ - if (!messages.collapsed.isEmpty()) { - state = database.executeFast("REPLACE INTO channel_group VALUES(?, ?, ?, ?)"); - for (int a = 0; a < messages.collapsed.size(); a++) { - TLRPC.TL_messageGroup group = messages.collapsed.get(a); - if (group.min_id > group.max_id) { - int temp = group.min_id; - group.min_id = group.max_id; - group.max_id = temp; - } - state.requery(); - state.bindLong(1, dialog_id); - state.bindInteger(2, group.min_id); - state.bindInteger(3, group.max_id); - state.bindInteger(4, group.count); - state.step(); - } - state.dispose(); - } - if (important == 1) { - messages.collapsed.clear(); - } - } - putUsersInternal(messages.users); putChatsInternal(messages.chats); @@ -5388,30 +5354,22 @@ public class MessagesStorage { ArrayList encryptedToLoad = new ArrayList<>(); ArrayList replyMessages = new ArrayList<>(); HashMap replyMessageOwners = new HashMap<>(); - SQLiteCursor cursor = database.queryFinalized(String.format(Locale.US, "SELECT d.did, d.last_mid, d.unread_count, d.date, m.data, m.read_state, m.mid, m.send_state, s.flags, m.date, d.last_mid_i, d.unread_count_i, d.pts, d.inbox_max, d.date_i, m.replydata FROM dialogs as d LEFT JOIN messages as m ON d.last_mid = m.mid LEFT JOIN dialog_settings as s ON d.did = s.did ORDER BY d.date DESC LIMIT %d,%d", offset, count)); + SQLiteCursor cursor = database.queryFinalized(String.format(Locale.US, "SELECT d.did, d.last_mid, d.unread_count, d.date, m.data, m.read_state, m.mid, m.send_state, s.flags, m.date, d.pts, d.inbox_max, d.outbox_max, m.replydata FROM dialogs as d LEFT JOIN messages as m ON d.last_mid = m.mid LEFT JOIN dialog_settings as s ON d.did = s.did ORDER BY d.date DESC LIMIT %d,%d", offset, count)); while (cursor.next()) { - TLRPC.Dialog dialog; - int pts = cursor.intValue(12); - long id = cursor.longValue(0); - if (pts == 0 || (int) id > 0) { - dialog = new TLRPC.TL_dialog(); - } else { - dialog = new TLRPC.TL_dialogChannel(); - } - dialog.id = id; + TLRPC.TL_dialog dialog = new TLRPC.TL_dialog(); + dialog.id = cursor.longValue(0); dialog.top_message = cursor.intValue(1); dialog.unread_count = cursor.intValue(2); dialog.last_message_date = cursor.intValue(3); - dialog.pts = pts; - dialog.read_inbox_max_id = cursor.intValue(13); - dialog.last_message_date_i = cursor.intValue(14); - dialog.top_not_important_message = cursor.intValue(10); - dialog.unread_not_important_count = cursor.intValue(11); + dialog.pts = cursor.intValue(10); + dialog.flags = dialog.pts == 0 || (int) dialog.id > 0 ? 0 : 1; + dialog.read_inbox_max_id = cursor.intValue(11); + dialog.read_outbox_max_id = cursor.intValue(12); long flags = cursor.longValue(8); - int low_flags = (int)flags; + int low_flags = (int) flags; dialog.notify_settings = new TLRPC.TL_peerNotifySettings(); if ((low_flags & 1) != 0) { - dialog.notify_settings.mute_until = (int)(flags >> 32); + dialog.notify_settings.mute_until = (int) (flags >> 32); if (dialog.notify_settings.mute_until == 0) { dialog.notify_settings.mute_until = Integer.MAX_VALUE; } @@ -5437,8 +5395,8 @@ public class MessagesStorage { try { if (message.reply_to_msg_id != 0 && message.action instanceof TLRPC.TL_messageActionPinMessage) { - if (!cursor.isNull(15)) { - data = cursor.byteBufferValue(15); + if (!cursor.isNull(13)) { + data = cursor.byteBufferValue(13); if (data != null) { message.replyMessage = TLRPC.Message.TLdeserialize(data, data.readInt32(false), false); data.reuse(); @@ -5464,8 +5422,8 @@ public class MessagesStorage { } } - int lower_id = (int)dialog.id; - int high_id = (int)(dialog.id >> 32); + int lower_id = (int) dialog.id; + int high_id = (int) (dialog.id >> 32); if (lower_id != 0) { if (high_id == 1) { if (!chatsToLoad.contains(lower_id)) { @@ -5523,119 +5481,43 @@ public class MessagesStorage { if (!usersToLoad.isEmpty()) { getUsersInternal(TextUtils.join(",", usersToLoad), dialogs.users); } - MessagesController.getInstance().processLoadedDialogs(dialogs, encryptedChats, offset, count, true, false, false); + MessagesController.getInstance().processLoadedDialogs(dialogs, encryptedChats, offset, count, 1, false, false); } catch (Exception e) { dialogs.dialogs.clear(); dialogs.users.clear(); dialogs.chats.clear(); encryptedChats.clear(); FileLog.e("tmessages", e); - MessagesController.getInstance().processLoadedDialogs(dialogs, encryptedChats, 0, 100, true, true, false); + MessagesController.getInstance().processLoadedDialogs(dialogs, encryptedChats, 0, 100, 1, true, false); } } }); } - public static void createFirstHoles(long did, SQLitePreparedStatement state5, SQLitePreparedStatement state6, SQLitePreparedStatement state7, SQLitePreparedStatement state8, ArrayList arrayList) throws Exception { - int impMessageId = 0; - int notImpMessageId = 0; - for (int a = 0; a < arrayList.size(); a++) { - TLRPC.Message message = arrayList.get(a); + public static void createFirstHoles(long did, SQLitePreparedStatement state5, SQLitePreparedStatement state6, int messageId) throws Exception { + state5.requery(); + state5.bindLong(1, did); + state5.bindInteger(2, messageId == 1 ? 1 : 0); + state5.bindInteger(3, messageId); + state5.step(); - if (MessageObject.isImportant(message)) { - state7.requery(); - state7.bindLong(1, did); - state7.bindInteger(2, message.id == 1 ? 1 : 0); - state7.bindInteger(3, message.id); - state7.step(); - impMessageId = Math.max(message.id, impMessageId); - } else { - notImpMessageId = Math.max(message.id, notImpMessageId); - } - } - - if (impMessageId != 0 && notImpMessageId == 0) { - notImpMessageId = impMessageId; - impMessageId = 0; - } - - if (arrayList.size() == 1) { - int messageId = arrayList.get(0).id; - - state5.requery(); - state5.bindLong(1, did); - state5.bindInteger(2, messageId == 1 ? 1 : 0); - state5.bindInteger(3, messageId); - state5.step(); - - for (int b = 0; b < SharedMediaQuery.MEDIA_TYPES_COUNT; b++) { - state6.requery(); - state6.bindLong(1, did); - state6.bindInteger(2, b); - state6.bindInteger(3, messageId == 1 ? 1 : 0); - state6.bindInteger(4, messageId); - state6.step(); - } - } else if (arrayList.size() == 2) { - int firstId = arrayList.get(0).id; - int lastId = arrayList.get(1).id; - if (firstId > lastId) { - int temp = firstId; - firstId = lastId; - lastId = temp; - } - - state5.requery(); - state5.bindLong(1, did); - state5.bindInteger(2, firstId == 1 ? 1 : 0); - state5.bindInteger(3, firstId); - state5.step(); - - state5.requery(); - state5.bindLong(1, did); - state5.bindInteger(2, firstId); - state5.bindInteger(3, lastId); - state5.step(); - - for (int b = 0; b < SharedMediaQuery.MEDIA_TYPES_COUNT; b++) { - state6.requery(); - state6.bindLong(1, did); - state6.bindInteger(2, b); - state6.bindInteger(3, firstId == 1 ? 1 : 0); - state6.bindInteger(4, firstId); - state6.step(); - - state6.requery(); - state6.bindLong(1, did); - state6.bindInteger(2, b); - state6.bindInteger(3, firstId); - state6.bindInteger(4, lastId); - state6.step(); - } - - if (impMessageId != 0 && impMessageId < notImpMessageId) { - state8.requery(); - state8.bindLong(1, did); - state8.bindInteger(2, impMessageId); - state8.bindInteger(3, Integer.MAX_VALUE); - state8.bindInteger(4, notImpMessageId - impMessageId); - state8.step(); - } + for (int b = 0; b < SharedMediaQuery.MEDIA_TYPES_COUNT; b++) { + state6.requery(); + state6.bindLong(1, did); + state6.bindInteger(2, b); + state6.bindInteger(3, messageId == 1 ? 1 : 0); + state6.bindInteger(4, messageId); + state6.step(); } } private void putDialogsInternal(final TLRPC.messages_Dialogs dialogs) { try { database.beginTransaction(); - final HashMap> new_dialogMessage = new HashMap<>(); + final HashMap new_dialogMessage = new HashMap<>(); for (int a = 0; a < dialogs.messages.size(); a++) { TLRPC.Message message = dialogs.messages.get(a); - ArrayList arrayList = new_dialogMessage.get(message.dialog_id); - if (arrayList == null) { - arrayList = new ArrayList<>(); - new_dialogMessage.put(message.dialog_id, arrayList); - } - arrayList.add(message); + new_dialogMessage.put(message.dialog_id, message); } if (!dialogs.dialogs.isEmpty()) { @@ -5645,11 +5527,9 @@ public class MessagesStorage { SQLitePreparedStatement state4 = database.executeFast("REPLACE INTO dialog_settings VALUES(?, ?)"); SQLitePreparedStatement state5 = database.executeFast("REPLACE INTO messages_holes VALUES(?, ?, ?)"); SQLitePreparedStatement state6 = database.executeFast("REPLACE INTO media_holes_v2 VALUES(?, ?, ?, ?)"); - SQLitePreparedStatement state7 = database.executeFast("REPLACE INTO messages_imp_holes VALUES(?, ?, ?)"); - SQLitePreparedStatement state8 = database.executeFast("REPLACE INTO channel_group VALUES(?, ?, ?, ?)"); for (int a = 0; a < dialogs.dialogs.size(); a++) { - TLRPC.Dialog dialog = dialogs.dialogs.get(a); + TLRPC.TL_dialog dialog = dialogs.dialogs.get(a); if (dialog.id == 0) { if (dialog.peer.user_id != 0) { @@ -5661,74 +5541,54 @@ public class MessagesStorage { } } int messageDate = 0; - int messageDateI = 0; - boolean isMegagroup = false; - ArrayList arrayList = new_dialogMessage.get(dialog.id); - if (arrayList != null) { - for (int b = 0; b < arrayList.size(); b++) { - TLRPC.Message message = arrayList.get(b); - if (message.to_id.channel_id == 0 || MessageObject.isImportant(message)) { - messageDate = Math.max(message.date, messageDate); - } else { - messageDateI = Math.max(message.date, messageDateI); - } - isMegagroup = MessageObject.isMegagroup(message); + TLRPC.Message message = new_dialogMessage.get(dialog.id); + if (message != null) { + messageDate = Math.max(message.date, messageDate); - if (isValidKeyboardToSave(message)) { - BotQuery.putBotKeyboard(dialog.id, message); - } - - fixUnsupportedMedia(message); - NativeByteBuffer data = new NativeByteBuffer(message.getObjectSize()); - message.serializeToStream(data); - - long messageId = message.id; - if (message.to_id.channel_id != 0) { - messageId |= ((long) message.to_id.channel_id) << 32; - } - - state.requery(); - state.bindLong(1, messageId); - state.bindLong(2, dialog.id); - state.bindInteger(3, MessageObject.getUnreadFlags(message)); - state.bindInteger(4, message.send_state); - state.bindInteger(5, message.date); - state.bindByteBuffer(6, data); - state.bindInteger(7, (MessageObject.isOut(message) ? 1 : 0)); - state.bindInteger(8, 0); - if ((message.flags & TLRPC.MESSAGE_FLAG_HAS_VIEWS) != 0) { - state.bindInteger(9, message.views); - } else { - state.bindInteger(9, 0); - } - state.bindInteger(10, MessageObject.isImportant(message) ? 1 : 0); - state.step(); - - if (SharedMediaQuery.canAddMessageToMedia(message)) { - state3.requery(); - state3.bindLong(1, messageId); - state3.bindLong(2, dialog.id); - state3.bindInteger(3, message.date); - state3.bindInteger(4, SharedMediaQuery.getMediaType(message)); - state3.bindByteBuffer(5, data); - state3.step(); - } - data.reuse(); + if (isValidKeyboardToSave(message)) { + BotQuery.putBotKeyboard(dialog.id, message); } - createFirstHoles(dialog.id, state5, state6, state7, state8, arrayList); + fixUnsupportedMedia(message); + NativeByteBuffer data = new NativeByteBuffer(message.getObjectSize()); + message.serializeToStream(data); + + long messageId = message.id; + if (message.to_id.channel_id != 0) { + messageId |= ((long) message.to_id.channel_id) << 32; + } + + state.requery(); + state.bindLong(1, messageId); + state.bindLong(2, dialog.id); + state.bindInteger(3, MessageObject.getUnreadFlags(message)); + state.bindInteger(4, message.send_state); + state.bindInteger(5, message.date); + state.bindByteBuffer(6, data); + state.bindInteger(7, (MessageObject.isOut(message) ? 1 : 0)); + state.bindInteger(8, 0); + state.bindInteger(9, (message.flags & TLRPC.MESSAGE_FLAG_HAS_VIEWS) != 0 ? message.views : 0); + state.bindInteger(10, 0); + state.step(); + + if (SharedMediaQuery.canAddMessageToMedia(message)) { + state3.requery(); + state3.bindLong(1, messageId); + state3.bindLong(2, dialog.id); + state3.bindInteger(3, message.date); + state3.bindInteger(4, SharedMediaQuery.getMediaType(message)); + state3.bindByteBuffer(5, data); + state3.step(); + } + data.reuse(); + + createFirstHoles(dialog.id, state5, state6, message.id); } long topMessage = dialog.top_message; - long topMessageI = dialog.top_not_important_message; if (dialog.peer.channel_id != 0) { - if (isMegagroup) { - topMessage = topMessageI = Math.max(topMessage, topMessageI); - messageDate = messageDateI = Math.max(messageDate, messageDateI); - } topMessage |= ((long) dialog.peer.channel_id) << 32; - topMessageI |= ((long) dialog.peer.channel_id) << 32; } state2.requery(); @@ -5737,11 +5597,11 @@ public class MessagesStorage { state2.bindInteger(3, dialog.unread_count); state2.bindLong(4, topMessage); state2.bindInteger(5, dialog.read_inbox_max_id); - state2.bindInteger(6, 0); - state2.bindLong(7, topMessageI); - state2.bindInteger(8, dialog.unread_not_important_count); + state2.bindInteger(6, dialog.read_outbox_max_id); + state2.bindLong(7, 0); + state2.bindInteger(8, 0); state2.bindInteger(9, dialog.pts); - state2.bindInteger(10, messageDateI); + state2.bindInteger(10, 0); state2.step(); if (dialog.notify_settings != null) { @@ -5757,8 +5617,6 @@ public class MessagesStorage { state4.dispose(); state5.dispose(); state6.dispose(); - state7.dispose(); - state8.dispose(); } putUsersInternal(dialogs.users); @@ -5783,15 +5641,19 @@ public class MessagesStorage { }); } - public int getDialogReadInboxMax(final long dialog_id) { + public int getDialogReadMax(final boolean outbox, final long dialog_id) { final Semaphore semaphore = new Semaphore(0); - final Integer[] max = new Integer[] {0}; + final Integer[] max = new Integer[]{0}; MessagesStorage.getInstance().getStorageQueue().postRunnable(new Runnable() { @Override public void run() { SQLiteCursor cursor = null; try { - cursor = database.queryFinalized("SELECT inbox_max FROM dialogs WHERE did = " + dialog_id); + if (outbox) { + cursor = database.queryFinalized("SELECT outbox_max FROM dialogs WHERE did = " + dialog_id); + } else { + cursor = database.queryFinalized("SELECT inbox_max FROM dialogs WHERE did = " + dialog_id); + } if (cursor.next()) { max[0] = cursor.intValue(0); } @@ -5815,7 +5677,7 @@ public class MessagesStorage { public int getChannelPtsSync(final int channelId) { final Semaphore semaphore = new Semaphore(0); - final Integer[] pts = new Integer[] {0}; + final Integer[] pts = new Integer[]{0}; MessagesStorage.getInstance().getStorageQueue().postRunnable(new Runnable() { @Override public void run() { diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/MusicPlayerService.java b/TMessagesProj/src/main/java/org/telegram/messenger/MusicPlayerService.java index e1bbf9513..e161b016c 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/MusicPlayerService.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/MusicPlayerService.java @@ -190,7 +190,11 @@ public class MusicPlayerService extends Service implements NotificationCenter.No metadataEditor.putString(MediaMetadataRetriever.METADATA_KEY_ARTIST, authorName); metadataEditor.putString(MediaMetadataRetriever.METADATA_KEY_TITLE, songName); if (audioInfo != null && audioInfo.getCover() != null) { - metadataEditor.putBitmap(RemoteControlClient.MetadataEditor.BITMAP_KEY_ARTWORK, audioInfo.getCover()); + try { + metadataEditor.putBitmap(RemoteControlClient.MetadataEditor.BITMAP_KEY_ARTWORK, audioInfo.getCover()); + } catch (Throwable e) { + FileLog.e("tmessages", e); + } } metadataEditor.apply(); } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/NativeLoader.java b/TMessagesProj/src/main/java/org/telegram/messenger/NativeLoader.java index b73b083f3..ccd8bc732 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/NativeLoader.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/NativeLoader.java @@ -142,10 +142,12 @@ public class NativeLoader { folder = "x86"; } - if (Build.VERSION.SDK_INT == 8) { - File destFile = new File(context.getApplicationInfo().dataDir + "/lib", LIB_SO_NAME); + + File destFile = getNativeLibraryDir(context); + if (destFile != null) { + destFile = new File(destFile, LIB_SO_NAME); if (destFile.exists()) { - FileLog.d("tmessages", "Load normal lib"); + FileLog.d("tmessages", "load normal lib"); try { System.loadLibrary(LIB_NAME); init(Constants.FILES_PATH, BuildVars.DEBUG_VERSION); @@ -154,31 +156,6 @@ public class NativeLoader { } catch (Error e) { FileLog.e("tmessages", e); } - } else { - try { - System.loadLibrary(LIB_NAME); - init(Constants.FILES_PATH, BuildVars.DEBUG_VERSION); - nativeLoaded = true; - return; - } catch (Error e) { - FileLog.e("tmessages", e); - } - } - } else { - File destFile = getNativeLibraryDir(context); - if (destFile != null) { - destFile = new File(destFile, LIB_SO_NAME); - if (destFile.exists()) { - FileLog.d("tmessages", "load normal lib"); - try { - System.loadLibrary(LIB_NAME); - init(Constants.FILES_PATH, BuildVars.DEBUG_VERSION); - nativeLoaded = true; - return; - } catch (Error e) { - FileLog.e("tmessages", e); - } - } } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/NotificationCenter.java b/TMessagesProj/src/main/java/org/telegram/messenger/NotificationCenter.java index 81028939d..618dec20e 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/NotificationCenter.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/NotificationCenter.java @@ -36,7 +36,7 @@ public class NotificationCenter { public static final int encryptedChatUpdated = totalEvents++; public static final int messagesReadEncrypted = totalEvents++; public static final int encryptedChatCreated = totalEvents++; - public static final int userPhotosLoaded = totalEvents++; + public static final int dialogPhotosLoaded = totalEvents++; public static final int removeAllMessagesFromDialog = totalEvents++; public static final int notificationsSettingsUpdated = totalEvents++; public static final int pushMessagesUpdated = totalEvents++; @@ -73,6 +73,7 @@ public class NotificationCenter { public static final int wasUnableToFindCurrentLocation = totalEvents++; public static final int reloadHints = totalEvents++; public static final int reloadInlineHints = totalEvents++; + public static final int newDraftReceived = totalEvents++; public static final int httpFileDidLoaded = totalEvents++; public static final int httpFileDidFailedLoad = totalEvents++; diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/NotificationDismissReceiver.java b/TMessagesProj/src/main/java/org/telegram/messenger/NotificationDismissReceiver.java new file mode 100644 index 000000000..a32a872f7 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/NotificationDismissReceiver.java @@ -0,0 +1,23 @@ +/* + * This is the source code of Telegram for Android v. 3.x.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-2016. + */ + +package org.telegram.messenger; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; + +public class NotificationDismissReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("Notifications", Context.MODE_PRIVATE); + preferences.edit().putInt("dismissDate", intent.getIntExtra("messageDate", 0)).commit(); + } +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/NotificationsController.java b/TMessagesProj/src/main/java/org/telegram/messenger/NotificationsController.java index 736062cd2..36c54852e 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/NotificationsController.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/NotificationsController.java @@ -18,6 +18,8 @@ import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.graphics.Point; import android.graphics.drawable.BitmapDrawable; import android.media.AudioManager; @@ -956,7 +958,7 @@ public class NotificationsController { } } else { if (ChatObject.isChannel(chat) && !chat.megagroup) { - if (messageObject.isImportant()) { + if (messageObject.messageOwner.post) { if (messageObject.isMediaEmpty()) { if (!shortMessage && messageObject.messageOwner.message != null && messageObject.messageOwner.message.length() != 0) { msg = LocaleController.formatString("NotificationMessageGroupText", R.string.NotificationMessageGroupText, name, chat.title, messageObject.messageOwner.message); @@ -1271,6 +1273,12 @@ public class NotificationsController { ConnectionsManager.getInstance().resumeNetworkMaybe(); MessageObject lastMessageObject = pushMessages.get(0); + SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("Notifications", Context.MODE_PRIVATE); + int dismissDate = preferences.getInt("dismissDate", 0); + if (lastMessageObject.messageOwner.date <= dismissDate) { + dismissNotification(); + return; + } long dialog_id = lastMessageObject.getDialogId(); long override_dialog_id = dialog_id; @@ -1305,7 +1313,6 @@ public class NotificationsController { int priorityOverride; int vibrateOverride; - SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("Notifications", Context.MODE_PRIVATE); 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; @@ -1475,6 +1482,7 @@ public class NotificationsController { int silent = 2; String lastMessage = null; + boolean hasNewMessages = false; if (pushMessages.size() == 1) { MessageObject messageObject = pushMessages.get(0); String message = lastMessage = getStringForMessage(messageObject, false); @@ -1499,7 +1507,7 @@ public class NotificationsController { for (int i = 0; i < count; i++) { MessageObject messageObject = pushMessages.get(i); String message = getStringForMessage(messageObject, false); - if (message == null) { + if (message == null || messageObject.messageOwner.date <= dismissDate) { continue; } if (silent == 2) { @@ -1521,10 +1529,26 @@ public class NotificationsController { mBuilder.setStyle(inboxStyle); } + Intent dismissIntent = new Intent(ApplicationLoader.applicationContext, NotificationDismissReceiver.class); + dismissIntent.putExtra("messageDate", lastMessageObject.messageOwner.date); + mBuilder.setDeleteIntent(PendingIntent.getBroadcast(ApplicationLoader.applicationContext, 1, dismissIntent, PendingIntent.FLAG_UPDATE_CURRENT)); + if (photoPath != null) { BitmapDrawable img = ImageLoader.getInstance().getImageFromMemory(photoPath, null, "50_50"); if (img != null) { mBuilder.setLargeIcon(img.getBitmap()); + } else { + try { + float scaleFactor = 160.0f / AndroidUtilities.dp(50); + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inSampleSize = scaleFactor < 1 ? 1 : (int) scaleFactor; + Bitmap bitmap = BitmapFactory.decodeFile(FileLoader.getPathToAttach(photoPath, true).toString(), options); + if (bitmap != null) { + mBuilder.setLargeIcon(bitmap); + } + } catch (Throwable e) { + //ignore + } } } @@ -1742,6 +1766,7 @@ public class NotificationsController { .setSmallIcon(R.drawable.notification) .setGroup("messages") .setContentText(text) + .setAutoCancel(true) .setColor(0xff2ca5e0) .setGroupSummary(false) .setContentIntent(contentIntent) diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/SecretChatHelper.java b/TMessagesProj/src/main/java/org/telegram/messenger/SecretChatHelper.java index 5cddafc16..a9ad47e6e 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/SecretChatHelper.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/SecretChatHelper.java @@ -88,7 +88,7 @@ public class SecretChatHelper { return localInstance; } - public void cleanUp() { + public void cleanup() { sendingNotifyLayer.clear(); acceptingChats.clear(); secretHolesQueue.clear(); @@ -194,7 +194,7 @@ public class SecretChatHelper { user = usersDict.get(user_id); } newChat.user_id = user_id; - final TLRPC.Dialog dialog = new TLRPC.TL_dialog(); + final TLRPC.TL_dialog dialog = new TLRPC.TL_dialog(); dialog.id = dialog_id; dialog.unread_count = 0; dialog.top_message = 0; @@ -206,18 +206,7 @@ public class SecretChatHelper { MessagesController.getInstance().dialogs_dict.put(dialog.id, dialog); MessagesController.getInstance().dialogs.add(dialog); MessagesController.getInstance().putEncryptedChat(newChat, false); - Collections.sort(MessagesController.getInstance().dialogs, new Comparator() { - @Override - public int compare(TLRPC.Dialog tl_dialog, TLRPC.Dialog tl_dialog2) { - if (tl_dialog.last_message_date == tl_dialog2.last_message_date) { - return 0; - } else if (tl_dialog.last_message_date < tl_dialog2.last_message_date) { - return 1; - } else { - return -1; - } - } - }); + MessagesController.getInstance().sortDialogs(null); NotificationCenter.getInstance().postNotificationName(NotificationCenter.dialogsNeedReload); } }); @@ -1153,7 +1142,7 @@ public class SecretChatHelper { AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { - TLRPC.Dialog dialog = MessagesController.getInstance().dialogs_dict.get(did); + TLRPC.TL_dialog dialog = MessagesController.getInstance().dialogs_dict.get(did); if (dialog != null) { dialog.unread_count = 0; MessagesController.getInstance().dialogMessage.remove(dialog.id); @@ -1813,25 +1802,14 @@ public class SecretChatHelper { chat.seq_out = 1; chat.a_or_b = salt; MessagesController.getInstance().putEncryptedChat(chat, false); - TLRPC.Dialog dialog = new TLRPC.TL_dialog(); + TLRPC.TL_dialog dialog = new TLRPC.TL_dialog(); dialog.id = ((long) chat.id) << 32; dialog.unread_count = 0; dialog.top_message = 0; dialog.last_message_date = ConnectionsManager.getInstance().getCurrentTime(); MessagesController.getInstance().dialogs_dict.put(dialog.id, dialog); MessagesController.getInstance().dialogs.add(dialog); - Collections.sort(MessagesController.getInstance().dialogs, new Comparator() { - @Override - public int compare(TLRPC.Dialog tl_dialog, TLRPC.Dialog tl_dialog2) { - if (tl_dialog.last_message_date == tl_dialog2.last_message_date) { - return 0; - } else if (tl_dialog.last_message_date < tl_dialog2.last_message_date) { - return 1; - } else { - return -1; - } - } - }); + MessagesController.getInstance().sortDialogs(null); MessagesStorage.getInstance().putEncryptedChat(chat, user, dialog); NotificationCenter.getInstance().postNotificationName(NotificationCenter.dialogsNeedReload); NotificationCenter.getInstance().postNotificationName(NotificationCenter.encryptedChatCreated, chat); @@ -1839,7 +1817,7 @@ public class SecretChatHelper { @Override public void run() { if (!delayedEncryptedChatUpdates.isEmpty()) { - MessagesController.getInstance().processUpdateArray(delayedEncryptedChatUpdates, null, null); + MessagesController.getInstance().processUpdateArray(delayedEncryptedChatUpdates, null, null, false); delayedEncryptedChatUpdates.clear(); } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/SendMessagesHelper.java b/TMessagesProj/src/main/java/org/telegram/messenger/SendMessagesHelper.java index da6f09697..9e8fe28c3 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/SendMessagesHelper.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/SendMessagesHelper.java @@ -20,13 +20,13 @@ import android.media.MediaMetadataRetriever; import android.media.MediaPlayer; import android.media.ThumbnailUtils; import android.net.Uri; -import android.os.Build; import android.os.Bundle; import android.provider.MediaStore; import android.webkit.MimeTypeMap; import android.widget.Toast; import org.telegram.messenger.audioinfo.AudioInfo; +import org.telegram.messenger.query.DraftQuery; import org.telegram.messenger.query.SearchQuery; import org.telegram.messenger.query.StickersQuery; import org.telegram.tgnet.ConnectionsManager; @@ -233,7 +233,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter NotificationCenter.getInstance().addObserver(this, NotificationCenter.FileDidFailedLoad); } - public void cleanUp() { + public void cleanup() { delayedMessages.clear(); unsentMessages.clear(); sendingMessages.clear(); @@ -587,7 +587,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter if (unsent) { unsentMessages.put(messageObject.getId(), messageObject); } - sendMessage(messageObject, messageObject.messageOwner.post); + sendMessage(messageObject); return true; } @@ -599,43 +599,43 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter } } - public void processForwardFromMyName(MessageObject messageObject, long did, boolean asAdmin) { + public void processForwardFromMyName(MessageObject messageObject, long did) { if (messageObject == null) { return; } if (messageObject.messageOwner.media != null && !(messageObject.messageOwner.media instanceof TLRPC.TL_messageMediaEmpty) && !(messageObject.messageOwner.media instanceof TLRPC.TL_messageMediaWebPage)) { if (messageObject.messageOwner.media.photo instanceof TLRPC.TL_photo) { - sendMessage((TLRPC.TL_photo) messageObject.messageOwner.media.photo, null, did, messageObject.replyMessageObject, asAdmin, null, null); + sendMessage((TLRPC.TL_photo) messageObject.messageOwner.media.photo, null, did, messageObject.replyMessageObject, null, null); } 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, asAdmin, null, null); + sendMessage((TLRPC.TL_document) messageObject.messageOwner.media.document, null, messageObject.messageOwner.attachPath, did, messageObject.replyMessageObject, null, null); } else if (messageObject.messageOwner.media instanceof TLRPC.TL_messageMediaVenue || messageObject.messageOwner.media instanceof TLRPC.TL_messageMediaGeo) { - sendMessage(messageObject.messageOwner.media, did, messageObject.replyMessageObject, asAdmin, null, null); + sendMessage(messageObject.messageOwner.media, did, messageObject.replyMessageObject, null, null); } else if (messageObject.messageOwner.media.phone_number != null) { TLRPC.User user = new TLRPC.TL_userContact_old2(); user.phone = messageObject.messageOwner.media.phone_number; user.first_name = messageObject.messageOwner.media.first_name; user.last_name = messageObject.messageOwner.media.last_name; user.id = messageObject.messageOwner.media.user_id; - sendMessage(user, did, messageObject.replyMessageObject, asAdmin, null, null); + sendMessage(user, did, messageObject.replyMessageObject, null, null); } else { ArrayList arrayList = new ArrayList<>(); arrayList.add(messageObject); - sendMessage(arrayList, did, asAdmin); + sendMessage(arrayList, did); } } else if (messageObject.messageOwner.message != null) { TLRPC.WebPage webPage = null; if (messageObject.messageOwner.media instanceof TLRPC.TL_messageMediaWebPage) { webPage = messageObject.messageOwner.media.webpage; } - sendMessage(messageObject.messageOwner.message, did, messageObject.replyMessageObject, webPage, true, asAdmin, messageObject.messageOwner.entities, null, null); + sendMessage(messageObject.messageOwner.message, did, messageObject.replyMessageObject, webPage, true, messageObject.messageOwner.entities, null, null); } else { ArrayList arrayList = new ArrayList<>(); arrayList.add(messageObject); - sendMessage(arrayList, did, asAdmin); + sendMessage(arrayList, did); } } - public void sendSticker(TLRPC.Document document, long peer, MessageObject replyingMessageObject, boolean asAdmin) { + public void sendSticker(TLRPC.Document document, long peer, MessageObject replyingMessageObject) { if (document == null) { return; } @@ -679,10 +679,10 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter } } } - SendMessagesHelper.getInstance().sendMessage((TLRPC.TL_document) document, null, null, peer, replyingMessageObject, asAdmin, null, null); + SendMessagesHelper.getInstance().sendMessage((TLRPC.TL_document) document, null, null, peer, replyingMessageObject, null, null); } - public void sendMessage(ArrayList messages, final long peer, boolean asAdmin) { + public void sendMessage(ArrayList messages, final long peer) { if ((int) peer == 0 || messages == null || messages.isEmpty()) { return; } @@ -762,7 +762,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter } newMsg.local_id = newMsg.id = UserConfig.getNewMessageId(); newMsg.out = true; - if (asAdmin && to_id.channel_id != 0 && !isMegagroup) { + if (to_id.channel_id != 0 && !isMegagroup) { newMsg.from_id = isSignature ? UserConfig.getClientUserId() : -to_id.channel_id; newMsg.post = true; } else { @@ -777,9 +777,11 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter ids.add(newMsg.fwd_msg_id); newMsg.date = ConnectionsManager.getInstance().getCurrentTime(); if (inputPeer instanceof TLRPC.TL_inputPeerChannel) { - if (asAdmin && !isMegagroup) { + if (!isMegagroup) { newMsg.views = 1; newMsg.flags |= TLRPC.MESSAGE_FLAG_HAS_VIEWS; + } else { + newMsg.unread = true; } } else { if ((msgObj.messageOwner.flags & TLRPC.MESSAGE_FLAG_HAS_VIEWS) != 0) { @@ -832,10 +834,6 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter req.random_id = randomIds; req.id = ids; - if (asAdmin && req.to_peer.channel_id != 0 && !isMegagroup) { - req.broadcast = true; - } - final ArrayList newMsgObjArr = arr; final ArrayList newMsgArr = objArr; final HashMap messagesByRandomIdsFinal = messagesByRandomIds; @@ -855,6 +853,12 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter a--; } } + Integer value = MessagesController.getInstance().dialogs_read_outbox_max.get(peer); + if (value == null) { + value = MessagesStorage.getInstance().getDialogReadMax(true, peer); + MessagesController.getInstance().dialogs_read_outbox_max.put(peer, value); + } + 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_updateNewChannelMessage) { @@ -869,6 +873,8 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter message.flags |= TLRPC.MESSAGE_FLAG_MEGAGROUP; } } + message.unread = value < message.id; + Long random_id = newMessagesByIds.get(message.id); if (random_id != null) { final TLRPC.Message newMsgObj = messagesByRandomIdsFinal.get(random_id); @@ -996,7 +1002,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter mediaGeo.geo._long = location.getLongitude(); for (HashMap.Entry entry : waitingForLocation.entrySet()) { MessageObject messageObject = entry.getValue(); - SendMessagesHelper.getInstance().sendMessage(mediaGeo, messageObject.getDialogId(), messageObject, false, null, null); + SendMessagesHelper.getInstance().sendMessage(mediaGeo, messageObject.getDialogId(), messageObject, null, null); } } @@ -1062,31 +1068,31 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter return !(messageObject == null || button == null) && waitingForCallback.containsKey(messageObject.getId() + "_" + Utilities.bytesToHex(button.data)); } - public void sendMessage(MessageObject retryMessageObject, boolean asAdmin) { - sendMessage(null, null, null, null, null, null, retryMessageObject.getDialogId(), retryMessageObject.messageOwner.attachPath, null, null, true, asAdmin, retryMessageObject, null, retryMessageObject.messageOwner.reply_markup, retryMessageObject.messageOwner.params); + public void sendMessage(MessageObject retryMessageObject) { + sendMessage(null, null, null, null, null, null, retryMessageObject.getDialogId(), retryMessageObject.messageOwner.attachPath, null, null, true, retryMessageObject, null, retryMessageObject.messageOwner.reply_markup, retryMessageObject.messageOwner.params); } - public void sendMessage(TLRPC.User user, long peer, MessageObject reply_to_msg, boolean asAdmin, TLRPC.ReplyMarkup replyMarkup, HashMap params) { - sendMessage(null, null, null, null, user, null, peer, null, reply_to_msg, null, true, asAdmin, null, null, replyMarkup, params); + public void sendMessage(TLRPC.User user, long peer, MessageObject reply_to_msg, TLRPC.ReplyMarkup replyMarkup, HashMap params) { + sendMessage(null, null, null, null, user, null, peer, null, reply_to_msg, null, true, null, null, replyMarkup, params); } - public void sendMessage(TLRPC.TL_document document, VideoEditedInfo videoEditedInfo, String path, long peer, MessageObject reply_to_msg, boolean asAdmin, TLRPC.ReplyMarkup replyMarkup, HashMap params) { - sendMessage(null, null, null, videoEditedInfo, null, document, peer, path, reply_to_msg, null, true, asAdmin, null, null, replyMarkup, params); + public void sendMessage(TLRPC.TL_document document, VideoEditedInfo videoEditedInfo, String path, long peer, MessageObject reply_to_msg, TLRPC.ReplyMarkup replyMarkup, HashMap params) { + sendMessage(null, null, null, videoEditedInfo, null, document, peer, path, reply_to_msg, null, true, null, null, replyMarkup, params); } - public void sendMessage(String message, long peer, MessageObject reply_to_msg, TLRPC.WebPage webPage, boolean searchLinks, boolean asAdmin, ArrayList entities, TLRPC.ReplyMarkup replyMarkup, HashMap params) { - sendMessage(message, null, null, null, null, null, peer, null, reply_to_msg, webPage, searchLinks, asAdmin, null, entities, replyMarkup, params); + public void sendMessage(String message, long peer, MessageObject reply_to_msg, TLRPC.WebPage webPage, boolean searchLinks, ArrayList entities, TLRPC.ReplyMarkup replyMarkup, HashMap params) { + sendMessage(message, null, null, null, null, null, peer, null, reply_to_msg, webPage, searchLinks, null, entities, replyMarkup, params); } - public void sendMessage(TLRPC.MessageMedia location, long peer, MessageObject reply_to_msg, boolean asAdmin, TLRPC.ReplyMarkup replyMarkup, HashMap params) { - sendMessage(null, location, null, null, null, null, peer, null, reply_to_msg, null, true, asAdmin, null, null, replyMarkup, params); + public void sendMessage(TLRPC.MessageMedia location, long peer, MessageObject reply_to_msg, TLRPC.ReplyMarkup replyMarkup, HashMap params) { + sendMessage(null, location, null, null, null, null, peer, null, reply_to_msg, null, true, null, null, replyMarkup, params); } - public void sendMessage(TLRPC.TL_photo photo, String path, long peer, MessageObject reply_to_msg, boolean asAdmin, TLRPC.ReplyMarkup replyMarkup, HashMap params) { - sendMessage(null, null, photo, null, null, null, peer, path, reply_to_msg, null, true, asAdmin, null, null, replyMarkup, params); + public void sendMessage(TLRPC.TL_photo photo, String path, long peer, MessageObject reply_to_msg, TLRPC.ReplyMarkup replyMarkup, HashMap params) { + sendMessage(null, null, photo, null, null, null, peer, path, reply_to_msg, null, true, null, null, replyMarkup, params); } - private void sendMessage(String message, TLRPC.MessageMedia location, TLRPC.TL_photo photo, VideoEditedInfo videoEditedInfo, TLRPC.User user, TLRPC.TL_document document, long peer, String path, MessageObject reply_to_msg, TLRPC.WebPage webPage, boolean searchLinks, boolean asAdmin, MessageObject retryMessageObject, ArrayList entities, TLRPC.ReplyMarkup replyMarkup, HashMap params) { + private void sendMessage(String message, TLRPC.MessageMedia location, TLRPC.TL_photo photo, VideoEditedInfo videoEditedInfo, TLRPC.User user, TLRPC.TL_document document, long peer, String path, MessageObject reply_to_msg, TLRPC.WebPage webPage, boolean searchLinks, MessageObject retryMessageObject, ArrayList entities, TLRPC.ReplyMarkup replyMarkup, HashMap params) { if (peer == 0) { return; } @@ -1101,6 +1107,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter int type = -1; int lower_id = (int) peer; int high_id = (int) (peer >> 32); + boolean isChannel = false; TLRPC.EncryptedChat encryptedChat = null; TLRPC.InputPeer sendToPeer = lower_id != 0 ? MessagesController.getInputPeer(lower_id) : null; ArrayList sendToPeers = null; @@ -1115,11 +1122,9 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter } return; } - } else if (asAdmin && sendToPeer instanceof TLRPC.TL_inputPeerChannel) { + } else if (sendToPeer instanceof TLRPC.TL_inputPeerChannel) { TLRPC.Chat chat = MessagesController.getInstance().getChat(sendToPeer.channel_id); - if (chat.megagroup) { - asAdmin = false; - } + isChannel = chat != null && !chat.megagroup; } try { @@ -1304,7 +1309,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter } newMsg.local_id = newMsg.id = UserConfig.getNewMessageId(); newMsg.out = true; - if (asAdmin && sendToPeer != null && sendToPeer.channel_id != 0) { + if (isChannel && sendToPeer != null) { newMsg.from_id = -sendToPeer.channel_id; } else { newMsg.from_id = UserConfig.getClientUserId(); @@ -1330,7 +1335,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter newMsg.date = ConnectionsManager.getInstance().getCurrentTime(); newMsg.flags |= TLRPC.MESSAGE_FLAG_HAS_MEDIA; if (sendToPeer instanceof TLRPC.TL_inputPeerChannel) { - if (asAdmin) { + if (isChannel) { newMsg.views = 1; newMsg.flags |= TLRPC.MESSAGE_FLAG_HAS_VIEWS; } @@ -1338,6 +1343,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter if (chat != null) { if (chat.megagroup) { newMsg.flags |= TLRPC.MESSAGE_FLAG_MEGAGROUP; + newMsg.unread = true; } else { newMsg.post = true; if (chat.signatures) { @@ -1466,14 +1472,12 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter } else { TLRPC.TL_messages_sendMessage reqSend = new TLRPC.TL_messages_sendMessage(); reqSend.message = message; + reqSend.clear_draft = retryMessageObject == null; if (newMsg.to_id instanceof TLRPC.TL_peerChannel) { reqSend.silent = ApplicationLoader.applicationContext.getSharedPreferences("Notifications", Activity.MODE_PRIVATE).getBoolean("silent_" + peer, false); } reqSend.peer = sendToPeer; reqSend.random_id = newMsg.random_id; - if (asAdmin && sendToPeer instanceof TLRPC.TL_inputPeerChannel) { - reqSend.broadcast = true; - } if (reply_to_msg != null) { reqSend.flags |= 1; reqSend.reply_to_msg_id = reply_to_msg.getId(); @@ -1486,6 +1490,9 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter reqSend.flags |= 8; } performSendMessageRequest(reqSend, newMsgObj, null); + if (retryMessageObject == null) { + DraftQuery.cleanDraft(peer, false); + } } } else { TLRPC.TL_decryptedMessage reqSend; @@ -1522,6 +1529,9 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter reqSend.media = new TLRPC.TL_decryptedMessageMediaEmpty(); } SecretChatHelper.getInstance().performSendEncryptedRequest(reqSend, newMsgObj.messageOwner, encryptedChat, null, null, newMsgObj); + if (retryMessageObject == null) { + DraftQuery.cleanDraft(peer, false); + } } } else if (type >= 1 && type <= 3 || type >= 5 && type <= 8 || type == 9 && encryptedChat != null) { if (encryptedChat == null) { @@ -1660,6 +1670,9 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter delayedMessage.sendRequest = request; } reqSend = request; + if (retryMessageObject == null) { + DraftQuery.cleanDraft(peer, false); + } } else { TLRPC.TL_messages_sendMedia request = new TLRPC.TL_messages_sendMedia(); request.peer = sendToPeer; @@ -1668,9 +1681,6 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter } request.random_id = newMsg.random_id; request.media = inputMedia; - if (asAdmin && sendToPeer instanceof TLRPC.TL_inputPeerChannel) { - request.broadcast = true; - } if (reply_to_msg != null) { request.flags |= 1; request.reply_to_msg_id = reply_to_msg.getId(); @@ -1971,6 +1981,9 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter } performSendDelayedMessage(delayedMessage); } + if (retryMessageObject == null) { + DraftQuery.cleanDraft(peer, false); + } } } else if (type == 4) { TLRPC.TL_messages_forwardMessages reqSend = new TLRPC.TL_messages_forwardMessages(); @@ -1994,16 +2007,10 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter } else { reqSend.id.add(retryMessageObject.messageOwner.fwd_msg_id); } - if (asAdmin && reqSend.to_peer.channel_id != 0) { - reqSend.broadcast = true; - } performSendMessageRequest(reqSend, newMsgObj, null); } else if (type == 9) { TLRPC.TL_messages_sendInlineBotResult reqSend = new TLRPC.TL_messages_sendInlineBotResult(); reqSend.peer = sendToPeer; - if (asAdmin && sendToPeer instanceof TLRPC.TL_inputPeerChannel) { - reqSend.broadcast = true; - } reqSend.random_id = newMsg.random_id; if (reply_to_msg != null) { reqSend.flags |= 1; @@ -2014,6 +2021,10 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter } reqSend.query_id = Utilities.parseLong(params.get("query_id")); reqSend.id = params.get("id"); + if (retryMessageObject == null) { + reqSend.clear_draft = true; + DraftQuery.cleanDraft(peer, false); + } performSendMessageRequest(reqSend, newMsgObj, null); } } catch (Exception e) { @@ -2239,6 +2250,13 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter } } if (message != null) { + Integer value = MessagesController.getInstance().dialogs_read_outbox_max.get(message.dialog_id); + if (value == null) { + value = MessagesStorage.getInstance().getDialogReadMax(message.out, message.dialog_id); + MessagesController.getInstance().dialogs_read_outbox_max.put(message.dialog_id, value); + } + message.unread = value < message.id; + newMsgObj.id = message.id; updateMediaPaths(msgObj, message, originalPath, false); } else { @@ -2518,7 +2536,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter } } - private static boolean prepareSendingDocumentInternal(String path, String originalPath, Uri uri, String mime, final long dialog_id, final MessageObject reply_to_msg, final boolean asAdmin, String caption) { + private static boolean prepareSendingDocumentInternal(String path, String originalPath, Uri uri, String mime, final long dialog_id, final MessageObject reply_to_msg, String caption) { if ((path == null || path.length() == 0) && uri == null) { return false; } @@ -2678,13 +2696,13 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { - SendMessagesHelper.getInstance().sendMessage(documentFinal, null, pathFinal, dialog_id, reply_to_msg, asAdmin, null, params); + SendMessagesHelper.getInstance().sendMessage(documentFinal, null, pathFinal, dialog_id, reply_to_msg, null, params); } }); return true; } - public static void prepareSendingDocument(String path, String originalPath, Uri uri, String mine, long dialog_id, MessageObject reply_to_msg, boolean asAdmin) { + public static void prepareSendingDocument(String path, String originalPath, Uri uri, String mine, long dialog_id, MessageObject reply_to_msg) { if ((path == null || originalPath == null) && uri == null) { return; } @@ -2696,10 +2714,10 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter } paths.add(path); originalPaths.add(originalPath); - prepareSendingDocuments(paths, originalPaths, uris, mine, dialog_id, reply_to_msg, asAdmin); + prepareSendingDocuments(paths, originalPaths, uris, mine, dialog_id, reply_to_msg); } - public static void prepareSendingAudioDocuments(final ArrayList messageObjects, final long dialog_id, final MessageObject reply_to_msg, final boolean asAdmin) { + public static void prepareSendingAudioDocuments(final ArrayList messageObjects, final long dialog_id, final MessageObject reply_to_msg) { new Thread(new Runnable() { @Override public void run() { @@ -2751,7 +2769,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { - SendMessagesHelper.getInstance().sendMessage(documentFinal, null, messageObject.messageOwner.attachPath, dialog_id, reply_to_msg, asAdmin, null, params); + SendMessagesHelper.getInstance().sendMessage(documentFinal, null, messageObject.messageOwner.attachPath, dialog_id, reply_to_msg, null, params); } }); } @@ -2759,7 +2777,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter }).start(); } - public static void prepareSendingDocuments(final ArrayList paths, final ArrayList originalPaths, final ArrayList uris, final String mime, final long dialog_id, final MessageObject reply_to_msg, final boolean asAdmin) { + public static void prepareSendingDocuments(final ArrayList paths, final ArrayList originalPaths, final ArrayList uris, final String mime, final long dialog_id, final MessageObject reply_to_msg) { if (paths == null && originalPaths == null && uris == null || paths != null && originalPaths != null && paths.size() != originalPaths.size()) { return; } @@ -2769,14 +2787,14 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter boolean error = false; if (paths != null) { for (int a = 0; a < paths.size(); a++) { - if (!prepareSendingDocumentInternal(paths.get(a), originalPaths.get(a), null, mime, dialog_id, reply_to_msg, asAdmin, null)) { + if (!prepareSendingDocumentInternal(paths.get(a), originalPaths.get(a), null, mime, dialog_id, reply_to_msg, null)) { error = true; } } } if (uris != null) { for (int a = 0; a < uris.size(); a++) { - if (!prepareSendingDocumentInternal(null, null, uris.get(a), mime, dialog_id, reply_to_msg, asAdmin, null)) { + if (!prepareSendingDocumentInternal(null, null, uris.get(a), mime, dialog_id, reply_to_msg, null)) { error = true; } } @@ -2798,7 +2816,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter }).start(); } - public static void prepareSendingPhoto(String imageFilePath, Uri imageUri, long dialog_id, MessageObject reply_to_msg, CharSequence caption, boolean asAdmin) { + public static void prepareSendingPhoto(String imageFilePath, Uri imageUri, long dialog_id, MessageObject reply_to_msg, CharSequence caption) { ArrayList paths = null; ArrayList uris = null; ArrayList captions = null; @@ -2814,10 +2832,10 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter captions = new ArrayList<>(); captions.add(caption.toString()); } - prepareSendingPhotos(paths, uris, dialog_id, reply_to_msg, captions, asAdmin); + prepareSendingPhotos(paths, uris, dialog_id, reply_to_msg, captions); } - public static void prepareSendingBotContextResult(final TLRPC.BotInlineResult result, final HashMap params, final long dialog_id, final MessageObject reply_to_msg, final boolean asAdmin) { + public static void prepareSendingBotContextResult(final TLRPC.BotInlineResult result, final HashMap params, final long dialog_id, final MessageObject reply_to_msg) { if (result == null) { return; } @@ -3014,17 +3032,17 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter public void run() { if (finalDocument != null) { finalDocument.caption = result.send_message.caption; - SendMessagesHelper.getInstance().sendMessage(finalDocument, null, finalPathFinal, dialog_id, reply_to_msg, asAdmin, result.send_message.reply_markup, params); + SendMessagesHelper.getInstance().sendMessage(finalDocument, null, finalPathFinal, dialog_id, reply_to_msg, result.send_message.reply_markup, params); } else if (finalPhoto != null) { finalPhoto.caption = result.send_message.caption; - SendMessagesHelper.getInstance().sendMessage(finalPhoto, result.content_url, dialog_id, reply_to_msg, asAdmin, result.send_message.reply_markup, params); + SendMessagesHelper.getInstance().sendMessage(finalPhoto, result.content_url, dialog_id, reply_to_msg, result.send_message.reply_markup, params); } } }); } }).run(); } else if (result.send_message instanceof TLRPC.TL_botInlineMessageText) { - SendMessagesHelper.getInstance().sendMessage(result.send_message.message, dialog_id, reply_to_msg, null, !result.send_message.no_webpage, asAdmin, result.send_message.entities, result.send_message.reply_markup, params); + SendMessagesHelper.getInstance().sendMessage(result.send_message.message, dialog_id, reply_to_msg, null, !result.send_message.no_webpage, result.send_message.entities, result.send_message.reply_markup, params); } else if (result.send_message instanceof TLRPC.TL_botInlineMessageMediaVenue) { TLRPC.TL_messageMediaVenue venue = new TLRPC.TL_messageMediaVenue(); venue.geo = result.send_message.geo; @@ -3032,21 +3050,21 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter venue.title = result.send_message.title; venue.provider = result.send_message.provider; venue.venue_id = result.send_message.venue_id; - SendMessagesHelper.getInstance().sendMessage(venue, dialog_id, reply_to_msg, asAdmin, result.send_message.reply_markup, params); + SendMessagesHelper.getInstance().sendMessage(venue, dialog_id, reply_to_msg, result.send_message.reply_markup, params); } else if (result.send_message instanceof TLRPC.TL_botInlineMessageMediaGeo) { TLRPC.TL_messageMediaGeo location = new TLRPC.TL_messageMediaGeo(); location.geo = result.send_message.geo; - SendMessagesHelper.getInstance().sendMessage(location, dialog_id, reply_to_msg, asAdmin, result.send_message.reply_markup, params); + SendMessagesHelper.getInstance().sendMessage(location, dialog_id, reply_to_msg, result.send_message.reply_markup, params); } else if (result.send_message instanceof TLRPC.TL_botInlineMessageMediaContact) { TLRPC.User user = new TLRPC.TL_user(); user.phone = result.send_message.phone_number; user.first_name = result.send_message.first_name; user.last_name = result.send_message.last_name; - SendMessagesHelper.getInstance().sendMessage(user, dialog_id, reply_to_msg, asAdmin, result.send_message.reply_markup, params); + SendMessagesHelper.getInstance().sendMessage(user, dialog_id, reply_to_msg, result.send_message.reply_markup, params); } } - public static void prepareSendingPhotosSearch(final ArrayList photos, final long dialog_id, final MessageObject reply_to_msg, final boolean asAdmin) { + public static void prepareSendingPhotosSearch(final ArrayList photos, final long dialog_id, final MessageObject reply_to_msg) { if (photos == null || photos.isEmpty()) { return; } @@ -3142,7 +3160,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { - SendMessagesHelper.getInstance().sendMessage(documentFinal, null, pathFinal, dialog_id, reply_to_msg, asAdmin, null, params); + SendMessagesHelper.getInstance().sendMessage(documentFinal, null, pathFinal, dialog_id, reply_to_msg, null, params); } }); } else { @@ -3192,7 +3210,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { - SendMessagesHelper.getInstance().sendMessage(photoFinal, needDownloadHttpFinal ? searchImage.imageUrl : null, dialog_id, reply_to_msg, asAdmin, null, params); + SendMessagesHelper.getInstance().sendMessage(photoFinal, needDownloadHttpFinal ? searchImage.imageUrl : null, dialog_id, reply_to_msg, null, params); } }); } @@ -3216,7 +3234,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter return src; } - public static void prepareSendingText(final String text, final long dialog_id, final boolean asAdmin) { + public static void prepareSendingText(final String text, final long dialog_id) { MessagesStorage.getInstance().getStorageQueue().postRunnable(new Runnable() { @Override public void run() { @@ -3231,7 +3249,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter int count = (int) Math.ceil(textFinal.length() / 4096.0f); for (int a = 0; a < count; a++) { String mess = textFinal.substring(a * 4096, Math.min((a + 1) * 4096, textFinal.length())); - SendMessagesHelper.getInstance().sendMessage(mess, dialog_id, null, null, true, asAdmin, null, null, null); + SendMessagesHelper.getInstance().sendMessage(mess, dialog_id, null, null, true, null, null, null); } } } @@ -3242,7 +3260,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter }); } - public static void prepareSendingPhotos(ArrayList paths, ArrayList uris, final long dialog_id, final MessageObject reply_to_msg, final ArrayList captions, final boolean asAdmin) { + public static void prepareSendingPhotos(ArrayList paths, ArrayList uris, final long dialog_id, final MessageObject reply_to_msg, final ArrayList captions) { if (paths == null && uris == null || paths != null && paths.isEmpty() || uris != null && uris.isEmpty()) { return; } @@ -3340,7 +3358,7 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { - SendMessagesHelper.getInstance().sendMessage(photoFinal, null, dialog_id, reply_to_msg, asAdmin, null, params); + SendMessagesHelper.getInstance().sendMessage(photoFinal, null, dialog_id, reply_to_msg, null, params); } }); } @@ -3348,14 +3366,14 @@ 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, extension, dialog_id, reply_to_msg, asAdmin, sendAsDocumentsCaptions.get(a)); + prepareSendingDocumentInternal(sendAsDocuments.get(a), sendAsDocumentsOriginal.get(a), null, extension, dialog_id, reply_to_msg, sendAsDocumentsCaptions.get(a)); } } } }).start(); } - public static void prepareSendingVideo(final String videoPath, final long estimatedSize, final long duration, final int width, final int height, final VideoEditedInfo videoEditedInfo, final long dialog_id, final MessageObject reply_to_msg, final boolean asAdmin) { + public static void prepareSendingVideo(final String videoPath, final long estimatedSize, final long duration, final int width, final int height, final VideoEditedInfo videoEditedInfo, final long dialog_id, final MessageObject reply_to_msg) { if (videoPath == null || videoPath.length() == 0) { return; } @@ -3415,34 +3433,33 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter document.size = (int) temp.length(); } boolean infoObtained = false; - if (Build.VERSION.SDK_INT >= 14) { - MediaMetadataRetriever mediaMetadataRetriever = null; + + MediaMetadataRetriever mediaMetadataRetriever = null; + try { + mediaMetadataRetriever = new MediaMetadataRetriever(); + mediaMetadataRetriever.setDataSource(videoPath); + String width = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH); + if (width != null) { + attributeVideo.w = Integer.parseInt(width); + } + String height = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT); + if (height != null) { + attributeVideo.h = Integer.parseInt(height); + } + String duration = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); + if (duration != null) { + attributeVideo.duration = (int) Math.ceil(Long.parseLong(duration) / 1000.0f); + } + infoObtained = true; + } catch (Exception e) { + FileLog.e("tmessages", e); + } finally { try { - mediaMetadataRetriever = new MediaMetadataRetriever(); - mediaMetadataRetriever.setDataSource(videoPath); - String width = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH); - if (width != null) { - attributeVideo.w = Integer.parseInt(width); + if (mediaMetadataRetriever != null) { + mediaMetadataRetriever.release(); } - String height = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT); - if (height != null) { - attributeVideo.h = Integer.parseInt(height); - } - String duration = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); - if (duration != null) { - attributeVideo.duration = (int) Math.ceil(Long.parseLong(duration) / 1000.0f); - } - infoObtained = true; } catch (Exception e) { FileLog.e("tmessages", e); - } finally { - try { - if (mediaMetadataRetriever != null) { - mediaMetadataRetriever.release(); - } - } catch (Exception e) { - FileLog.e("tmessages", e); - } } } if (!infoObtained) { @@ -3470,11 +3487,11 @@ public class SendMessagesHelper implements NotificationCenter.NotificationCenter AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { - SendMessagesHelper.getInstance().sendMessage(videoFinal, videoEditedInfo, finalPath, dialog_id, reply_to_msg, asAdmin, null, params); + SendMessagesHelper.getInstance().sendMessage(videoFinal, videoEditedInfo, finalPath, dialog_id, reply_to_msg, null, params); } }); } else { - prepareSendingDocumentInternal(videoPath, videoPath, null, null, dialog_id, reply_to_msg, asAdmin, null); + prepareSendingDocumentInternal(videoPath, videoPath, null, null, dialog_id, reply_to_msg, null); } } }).start(); diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/UserConfig.java b/TMessagesProj/src/main/java/org/telegram/messenger/UserConfig.java index a13000ce9..a698626ee 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/UserConfig.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/UserConfig.java @@ -20,28 +20,27 @@ import java.io.File; public class UserConfig { private static TLRPC.User currentUser; - public static boolean registeredForPush = false; + public static boolean registeredForPush; public static String pushString = ""; public static int lastSendMessageId = -210000; public static int lastLocalId = -210000; public static int lastBroadcastId = -1; public static String contactsHash = ""; - public static String importHash = ""; - public static boolean blockedUsersLoaded = false; + public static boolean blockedUsersLoaded; private final static Object sync = new Object(); - public static boolean saveIncomingPhotos = false; - public static int contactsVersion = 1; + public static boolean saveIncomingPhotos; public static String passcodeHash = ""; public static byte[] passcodeSalt = new byte[0]; - public static boolean appLocked = false; - public static int passcodeType = 0; + public static boolean appLocked; + public static int passcodeType; public static int autoLockIn = 60 * 60; - public static int lastPauseTime = 0; - public static boolean isWaitingForPasscodeEnter = false; + public static int lastPauseTime; + public static boolean isWaitingForPasscodeEnter; public static boolean useFingerprint = true; public static String lastUpdateVersion; public static int lastContactsSyncTime; public static int lastHintsSyncTime; + public static boolean draftsLoaded; public static int migrateOffsetId = -1; public static int migrateOffsetDate = -1; @@ -73,9 +72,7 @@ public class UserConfig { editor.putInt("lastSendMessageId", lastSendMessageId); editor.putInt("lastLocalId", lastLocalId); editor.putString("contactsHash", contactsHash); - editor.putString("importHash", importHash); editor.putBoolean("saveIncomingPhotos", saveIncomingPhotos); - editor.putInt("contactsVersion", contactsVersion); editor.putInt("lastBroadcastId", lastBroadcastId); editor.putBoolean("blockedUsersLoaded", blockedUsersLoaded); editor.putString("passcodeHash1", passcodeHash); @@ -88,6 +85,7 @@ public class UserConfig { editor.putInt("lastContactsSyncTime", lastContactsSyncTime); editor.putBoolean("useFingerprint", useFingerprint); editor.putInt("lastHintsSyncTime", lastHintsSyncTime); + editor.putBoolean("draftsLoaded", draftsLoaded); editor.putInt("migrateOffsetId", migrateOffsetId); if (migrateOffsetId != -1) { @@ -162,9 +160,8 @@ public class UserConfig { lastSendMessageId = data.readInt32(false); lastLocalId = data.readInt32(false); contactsHash = data.readString(false); - importHash = data.readString(false); + data.readString(false); saveIncomingPhotos = data.readBool(false); - contactsVersion = 0; MessagesStorage.lastQtsValue = data.readInt32(false); MessagesStorage.lastSecretVersion = data.readInt32(false); int val = data.readInt32(false); @@ -188,9 +185,7 @@ public class UserConfig { lastSendMessageId = preferences.getInt("lastSendMessageId", -210000); lastLocalId = preferences.getInt("lastLocalId", -210000); contactsHash = preferences.getString("contactsHash", ""); - importHash = preferences.getString("importHash", ""); saveIncomingPhotos = preferences.getBoolean("saveIncomingPhotos", false); - contactsVersion = preferences.getInt("contactsVersion", 0); } if (lastLocalId > -210000) { lastLocalId = -210000; @@ -215,9 +210,7 @@ public class UserConfig { lastSendMessageId = preferences.getInt("lastSendMessageId", -210000); lastLocalId = preferences.getInt("lastLocalId", -210000); contactsHash = preferences.getString("contactsHash", ""); - importHash = preferences.getString("importHash", ""); saveIncomingPhotos = preferences.getBoolean("saveIncomingPhotos", false); - contactsVersion = preferences.getInt("contactsVersion", 0); lastBroadcastId = preferences.getInt("lastBroadcastId", -1); blockedUsersLoaded = preferences.getBoolean("blockedUsersLoaded", false); passcodeHash = preferences.getString("passcodeHash1", ""); @@ -229,6 +222,7 @@ public class UserConfig { lastUpdateVersion = preferences.getString("lastUpdateVersion2", "3.5"); lastContactsSyncTime = preferences.getInt("lastContactsSyncTime", (int) (System.currentTimeMillis() / 1000) - 23 * 60 * 60); lastHintsSyncTime = preferences.getInt("lastHintsSyncTime", (int) (System.currentTimeMillis() / 1000) - 25 * 60 * 60); + draftsLoaded = preferences.getBoolean("draftsLoaded", false); migrateOffsetId = preferences.getInt("migrateOffsetId", 0); if (migrateOffsetId != -1) { @@ -297,9 +291,7 @@ public class UserConfig { currentUser = null; registeredForPush = false; contactsHash = ""; - importHash = ""; lastSendMessageId = -210000; - contactsVersion = 1; lastBroadcastId = -1; saveIncomingPhotos = false; blockedUsersLoaded = false; @@ -316,6 +308,7 @@ public class UserConfig { autoLockIn = 60 * 60; lastPauseTime = 0; useFingerprint = true; + draftsLoaded = true; isWaitingForPasscodeEnter = false; lastUpdateVersion = BuildVars.BUILD_VERSION_STRING; lastContactsSyncTime = (int) (System.currentTimeMillis() / 1000) - 23 * 60 * 60; diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/Utilities.java b/TMessagesProj/src/main/java/org/telegram/messenger/Utilities.java index 15bf1bb2d..b11a5fc2b 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/Utilities.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/Utilities.java @@ -22,7 +22,7 @@ import java.util.regex.Pattern; public class Utilities { - public static Pattern pattern = Pattern.compile("[0-9]+"); + public static Pattern pattern = Pattern.compile("[\\-0-9]+"); public static SecureRandom random = new SecureRandom(); public static volatile DispatchQueue stageQueue = new DispatchQueue("stageQueue"); diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/VideoEditedInfo.java b/TMessagesProj/src/main/java/org/telegram/messenger/VideoEditedInfo.java index 12c8666a7..e3629ec7d 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/VideoEditedInfo.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/VideoEditedInfo.java @@ -25,27 +25,33 @@ public class VideoEditedInfo { return String.format(Locale.US, "-1_%d_%d_%d_%d_%d_%d_%d_%d_%s", startTime, endTime, rotationValue, originalWidth, originalHeight, bitrate, resultWidth, resultHeight, originalPath); } - public void parseString(String string) { + public boolean parseString(String string) { if (string.length() < 6) { - return; + return false; } - String args[] = string.split("_"); - if (args.length >= 10) { - startTime = Long.parseLong(args[1]); - endTime = Long.parseLong(args[2]); - rotationValue = Integer.parseInt(args[3]); - originalWidth = Integer.parseInt(args[4]); - originalHeight = Integer.parseInt(args[5]); - bitrate = Integer.parseInt(args[6]); - resultWidth = Integer.parseInt(args[7]); - resultHeight = Integer.parseInt(args[8]); - for (int a = 9; a < args.length; a++) { - if (originalPath == null) { - originalPath = args[a]; - } else { - originalPath += "_" + args[a]; + try { + String args[] = string.split("_"); + if (args.length >= 10) { + startTime = Long.parseLong(args[1]); + endTime = Long.parseLong(args[2]); + rotationValue = Integer.parseInt(args[3]); + originalWidth = Integer.parseInt(args[4]); + originalHeight = Integer.parseInt(args[5]); + bitrate = Integer.parseInt(args[6]); + resultWidth = Integer.parseInt(args[7]); + resultHeight = Integer.parseInt(args[8]); + for (int a = 9; a < args.length; a++) { + if (originalPath == null) { + originalPath = args[a]; + } else { + originalPath += "_" + args[a]; + } } } + return true; + } catch (Exception e) { + FileLog.e("tmessages", e); } + return false; } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/WearReplyReceiver.java b/TMessagesProj/src/main/java/org/telegram/messenger/WearReplyReceiver.java index 911daf078..6a490d6f1 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/WearReplyReceiver.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/WearReplyReceiver.java @@ -32,7 +32,7 @@ public class WearReplyReceiver extends BroadcastReceiver { if (dialog_id == 0 || max_id == 0) { return; } - SendMessagesHelper.getInstance().sendMessage(text.toString(), dialog_id, null, null, true, false, null, null, null); + SendMessagesHelper.getInstance().sendMessage(text.toString(), dialog_id, null, null, true, null, null, null); MessagesController.getInstance().markDialogAsRead(dialog_id, max_id, max_id, 0, true, false); } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/browser/Browser.java b/TMessagesProj/src/main/java/org/telegram/messenger/browser/Browser.java index 70b7807eb..9a3d56c2d 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/browser/Browser.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/browser/Browser.java @@ -88,11 +88,13 @@ public class Browser { @Override public void onServiceConnected(CustomTabsClient client) { customTabsClient = client; - if (customTabsClient != null) { - try { - customTabsClient.warmup(0); - } catch (Exception e) { - FileLog.e("tmessages", e); + if (MediaController.getInstance().canCustomTabs()) { + if (customTabsClient != null) { + try { + customTabsClient.warmup(0); + } catch (Exception e) { + FileLog.e("tmessages", e); + } } } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/AspectRatioFrameLayout.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/AspectRatioFrameLayout.java new file mode 100755 index 000000000..21cea8bb5 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/AspectRatioFrameLayout.java @@ -0,0 +1,86 @@ +/* + * 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.messenger.exoplayer; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.FrameLayout; + +/** + * A {@link FrameLayout} that resizes itself to match a specified aspect ratio. + */ +public final class AspectRatioFrameLayout extends FrameLayout { + + /** + * The {@link FrameLayout} will not resize itself if the fractional difference between its natural + * aspect ratio and the requested aspect ratio falls below this threshold. + *

+ * This tolerance allows the view to occupy the whole of the screen when the requested aspect + * ratio is very close, but not exactly equal to, the aspect ratio of the screen. This may reduce + * the number of view layers that need to be composited by the underlying system, which can help + * to reduce power consumption. + */ + private static final float MAX_ASPECT_RATIO_DEFORMATION_FRACTION = 0.01f; + + private float videoAspectRatio; + + public AspectRatioFrameLayout(Context context) { + super(context); + } + + public AspectRatioFrameLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + /** + * Set the aspect ratio that this view should satisfy. + * + * @param widthHeightRatio The width to height ratio. + */ + public void setAspectRatio(float widthHeightRatio) { + if (this.videoAspectRatio != widthHeightRatio) { + this.videoAspectRatio = widthHeightRatio; + requestLayout(); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + if (videoAspectRatio == 0) { + // Aspect ratio not set. + return; + } + + int width = getMeasuredWidth(); + int height = getMeasuredHeight(); + float viewAspectRatio = (float) width / height; + float aspectDeformation = videoAspectRatio / viewAspectRatio - 1; + if (Math.abs(aspectDeformation) <= MAX_ASPECT_RATIO_DEFORMATION_FRACTION) { + // We're within the allowed tolerance. + return; + } + + if (aspectDeformation > 0) { + height = (int) (width / videoAspectRatio); + } else { + width = (int) (height * videoAspectRatio); + } + super.onMeasure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)); + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/BehindLiveWindowException.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/BehindLiveWindowException.java new file mode 100755 index 000000000..33ddfbe44 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/BehindLiveWindowException.java @@ -0,0 +1,33 @@ +/* + * 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.messenger.exoplayer; + +import java.io.IOException; + +/** + * Thrown when a live playback falls behind the available media window. + */ +public final class BehindLiveWindowException extends IOException { + + public BehindLiveWindowException() { + super(); + } + + public BehindLiveWindowException(String message) { + super(message); + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/C.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/C.java new file mode 100755 index 000000000..4e270673c --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/C.java @@ -0,0 +1,146 @@ +/* + * 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.messenger.exoplayer; + +import org.telegram.messenger.exoplayer.util.Util; + +import android.media.AudioFormat; +import android.media.MediaCodec; +import android.media.MediaExtractor; + +/** + * Defines constants that are generally useful throughout the library. + */ +public final class C { + + /** + * Represents an unknown microsecond time or duration. + */ + public static final long UNKNOWN_TIME_US = -1L; + + /** + * Represents a microsecond duration whose exact value is unknown, but which should match the + * longest of some other known durations. + */ + public static final long MATCH_LONGEST_US = -2L; + + /** + * The number of microseconds in one second. + */ + public static final long MICROS_PER_SECOND = 1000000L; + + /** + * Represents an unbounded length of data. + */ + public static final int LENGTH_UNBOUNDED = -1; + + /** + * The name of the UTF-8 charset. + */ + public static final String UTF8_NAME = "UTF-8"; + + /** + * @see MediaCodec#CRYPTO_MODE_AES_CTR + */ + @SuppressWarnings("InlinedApi") + public static final int CRYPTO_MODE_AES_CTR = MediaCodec.CRYPTO_MODE_AES_CTR; + + /** + * @see AudioFormat#ENCODING_INVALID + */ + public static final int ENCODING_INVALID = AudioFormat.ENCODING_INVALID; + + /** + * @see AudioFormat#ENCODING_PCM_8BIT + */ + public static final int ENCODING_PCM_8BIT = AudioFormat.ENCODING_PCM_8BIT; + + /** + * @see AudioFormat#ENCODING_PCM_16BIT + */ + public static final int ENCODING_PCM_16BIT = AudioFormat.ENCODING_PCM_16BIT; + + /** + * PCM encoding with 24 bits per sample. + */ + public static final int ENCODING_PCM_24BIT = 0x80000000; + + /** + * PCM encoding with 32 bits per sample. + */ + public static final int ENCODING_PCM_32BIT = 0x40000000; + + /** + * @see AudioFormat#ENCODING_AC3 + */ + @SuppressWarnings("InlinedApi") + public static final int ENCODING_AC3 = AudioFormat.ENCODING_AC3; + + /** + * @see AudioFormat#ENCODING_E_AC3 + */ + @SuppressWarnings("InlinedApi") + public static final int ENCODING_E_AC3 = AudioFormat.ENCODING_E_AC3; + + /** + * @see AudioFormat#ENCODING_DTS + */ + @SuppressWarnings("InlinedApi") + public static final int ENCODING_DTS = AudioFormat.ENCODING_DTS; + + /** + * @see AudioFormat#ENCODING_DTS_HD + */ + @SuppressWarnings("InlinedApi") + public static final int ENCODING_DTS_HD = AudioFormat.ENCODING_DTS_HD; + + /** + * @see AudioFormat#CHANNEL_OUT_7POINT1_SURROUND + */ + @SuppressWarnings({"InlinedApi", "deprecation"}) + public static final int CHANNEL_OUT_7POINT1_SURROUND = Util.SDK_INT < 23 + ? AudioFormat.CHANNEL_OUT_7POINT1 : AudioFormat.CHANNEL_OUT_7POINT1_SURROUND; + + /** + * @see MediaExtractor#SAMPLE_FLAG_SYNC + */ + @SuppressWarnings("InlinedApi") + public static final int SAMPLE_FLAG_SYNC = MediaExtractor.SAMPLE_FLAG_SYNC; + + /** + * @see MediaExtractor#SAMPLE_FLAG_ENCRYPTED + */ + @SuppressWarnings("InlinedApi") + public static final int SAMPLE_FLAG_ENCRYPTED = MediaExtractor.SAMPLE_FLAG_ENCRYPTED; + + /** + * Indicates that a sample should be decoded but not rendered. + */ + public static final int SAMPLE_FLAG_DECODE_ONLY = 0x8000000; + + /** + * A return value for methods where the end of an input was encountered. + */ + public static final int RESULT_END_OF_INPUT = -1; + + /** + * A return value for methods where the length of parsed data exceeds the maximum length allowed. + */ + public static final int RESULT_MAX_LENGTH_EXCEEDED = -2; + + private C() {} + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/CodecCounters.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/CodecCounters.java new file mode 100755 index 000000000..5d138946c --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/CodecCounters.java @@ -0,0 +1,62 @@ +/* + * 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.messenger.exoplayer; + +/** + * Maintains codec event counts, for debugging purposes only. + *

+ * Counters should be written from the playback thread only. Counters may be read from any thread. + * To ensure that the counter values are correctly reflected between threads, users of this class + * should invoke {@link #ensureUpdated()} prior to reading and after writing. + */ +public final class CodecCounters { + + public int codecInitCount; + public int codecReleaseCount; + public int inputBufferCount; + public int outputFormatChangedCount; + public int outputBuffersChangedCount; + public int renderedOutputBufferCount; + public int skippedOutputBufferCount; + public int droppedOutputBufferCount; + public int maxConsecutiveDroppedOutputBufferCount; + + /** + * Should be invoked from the playback thread after the counters have been updated. Should also + * be invoked from any other thread that wishes to read the counters, before reading. These calls + * ensure that counter updates are made visible to the reading threads. + */ + public synchronized void ensureUpdated() { + // Do nothing. The use of synchronized ensures a memory barrier should another thread also + // call this method. + } + + public String getDebugString() { + ensureUpdated(); + StringBuilder builder = new StringBuilder(); + builder.append("cic:").append(codecInitCount); + builder.append(" crc:").append(codecReleaseCount); + builder.append(" ibc:").append(inputBufferCount); + builder.append(" ofc:").append(outputFormatChangedCount); + builder.append(" obc:").append(outputBuffersChangedCount); + builder.append(" ren:").append(renderedOutputBufferCount); + builder.append(" sob:").append(skippedOutputBufferCount); + builder.append(" dob:").append(droppedOutputBufferCount); + builder.append(" mcdob:").append(maxConsecutiveDroppedOutputBufferCount); + return builder.toString(); + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/CryptoInfo.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/CryptoInfo.java new file mode 100755 index 000000000..e20623ac2 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/CryptoInfo.java @@ -0,0 +1,116 @@ +/* + * 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.messenger.exoplayer; + +import org.telegram.messenger.exoplayer.util.Util; + +import android.annotation.TargetApi; +import android.media.MediaExtractor; + +/** + * Compatibility wrapper around {@link android.media.MediaCodec.CryptoInfo}. + */ +public final class CryptoInfo { + + /** + * @see android.media.MediaCodec.CryptoInfo#iv + */ + public byte[] iv; + /** + * @see android.media.MediaCodec.CryptoInfo#key + */ + public byte[] key; + /** + * @see android.media.MediaCodec.CryptoInfo#mode + */ + public int mode; + /** + * @see android.media.MediaCodec.CryptoInfo#numBytesOfClearData + */ + public int[] numBytesOfClearData; + /** + * @see android.media.MediaCodec.CryptoInfo#numBytesOfEncryptedData + */ + public int[] numBytesOfEncryptedData; + /** + * @see android.media.MediaCodec.CryptoInfo#numSubSamples + */ + public int numSubSamples; + + private final android.media.MediaCodec.CryptoInfo frameworkCryptoInfo; + + public CryptoInfo() { + frameworkCryptoInfo = Util.SDK_INT >= 16 ? newFrameworkCryptoInfoV16() : null; + } + + /** + * @see android.media.MediaCodec.CryptoInfo#set(int, int[], int[], byte[], byte[], int) + */ + public void set(int numSubSamples, int[] numBytesOfClearData, int[] numBytesOfEncryptedData, + byte[] key, byte[] iv, int mode) { + this.numSubSamples = numSubSamples; + this.numBytesOfClearData = numBytesOfClearData; + this.numBytesOfEncryptedData = numBytesOfEncryptedData; + this.key = key; + this.iv = iv; + this.mode = mode; + if (Util.SDK_INT >= 16) { + updateFrameworkCryptoInfoV16(); + } + } + + /** + * Equivalent to {@link MediaExtractor#getSampleCryptoInfo(android.media.MediaCodec.CryptoInfo)}. + * + * @param extractor The extractor from which to retrieve the crypto information. + */ + @TargetApi(16) + public void setFromExtractorV16(MediaExtractor extractor) { + extractor.getSampleCryptoInfo(frameworkCryptoInfo); + numSubSamples = frameworkCryptoInfo.numSubSamples; + numBytesOfClearData = frameworkCryptoInfo.numBytesOfClearData; + numBytesOfEncryptedData = frameworkCryptoInfo.numBytesOfEncryptedData; + key = frameworkCryptoInfo.key; + iv = frameworkCryptoInfo.iv; + mode = frameworkCryptoInfo.mode; + } + + /** + * Returns an equivalent {@link android.media.MediaCodec.CryptoInfo} instance. + *

+ * Successive calls to this method on a single {@link CryptoInfo} will return the same instance. + * Changes to the {@link CryptoInfo} will be reflected in the returned object. The return object + * should not be modified directly. + * + * @return The equivalent {@link android.media.MediaCodec.CryptoInfo} instance. + */ + @TargetApi(16) + public android.media.MediaCodec.CryptoInfo getFrameworkCryptoInfoV16() { + return frameworkCryptoInfo; + } + + @TargetApi(16) + private android.media.MediaCodec.CryptoInfo newFrameworkCryptoInfoV16() { + return new android.media.MediaCodec.CryptoInfo(); + } + + @TargetApi(16) + private void updateFrameworkCryptoInfoV16() { + frameworkCryptoInfo.set(numSubSamples, numBytesOfClearData, numBytesOfEncryptedData, key, iv, + mode); + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/DecoderInfo.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/DecoderInfo.java new file mode 100755 index 000000000..4e682242c --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/DecoderInfo.java @@ -0,0 +1,69 @@ +/* + * 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.messenger.exoplayer; + +import org.telegram.messenger.exoplayer.util.Util; + +import android.annotation.TargetApi; +import android.media.MediaCodecInfo.CodecCapabilities; + +/** + * Contains information about a media decoder. + */ +@TargetApi(16) +public final class DecoderInfo { + + /** + * The name of the decoder. + *

+ * May be passed to {@link android.media.MediaCodec#createByCodecName(String)} to create an + * instance of the decoder. + */ + public final String name; + + /** + * {@link CodecCapabilities} for this decoder. + */ + public final CodecCapabilities capabilities; + + /** + * Whether the decoder supports seamless resolution switches. + * + * @see android.media.MediaCodecInfo.CodecCapabilities#isFeatureSupported(String) + * @see android.media.MediaCodecInfo.CodecCapabilities#FEATURE_AdaptivePlayback + */ + public final boolean adaptive; + + /** + * @param name The name of the decoder. + * @param capabilities {@link CodecCapabilities} of the decoder. + */ + /* package */ DecoderInfo(String name, CodecCapabilities capabilities) { + this.name = name; + this.capabilities = capabilities; + this.adaptive = isAdaptive(capabilities); + } + + private static boolean isAdaptive(CodecCapabilities capabilities) { + return capabilities != null && Util.SDK_INT >= 19 && isAdaptiveV19(capabilities); + } + + @TargetApi(19) + private static boolean isAdaptiveV19(CodecCapabilities capabilities) { + return capabilities.isFeatureSupported(CodecCapabilities.FEATURE_AdaptivePlayback); + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/DefaultLoadControl.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/DefaultLoadControl.java new file mode 100755 index 000000000..43c4268d0 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/DefaultLoadControl.java @@ -0,0 +1,278 @@ +/* + * 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.messenger.exoplayer; + +import org.telegram.messenger.exoplayer.upstream.Allocator; +import org.telegram.messenger.exoplayer.upstream.NetworkLock; + +import android.os.Handler; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +/** + * A {@link LoadControl} implementation that allows loads to continue in a sequence that prevents + * any loader from getting too far ahead or behind any of the other loaders. + *

+ * Loads are scheduled so as to fill the available buffer space as rapidly as possible. Once the + * duration of buffered media and the buffer utilization both exceed respective thresholds, the + * control switches to a draining state during which no loads are permitted to start. During + * draining periods, resources such as the device radio have an opportunity to switch into low + * power modes. The control reverts back to the loading state when either the duration of buffered + * media or the buffer utilization fall below respective thresholds. + *

+ * This implementation of {@link LoadControl} integrates with {@link NetworkLock}, by registering + * itself as a task with priority {@link NetworkLock#STREAMING_PRIORITY} during loading periods, + * and unregistering itself during draining periods. + */ +public final class DefaultLoadControl implements LoadControl { + + /** + * Interface definition for a callback to be notified of {@link DefaultLoadControl} events. + */ + public interface EventListener { + + /** + * Invoked when the control transitions from a loading to a draining state, or vice versa. + * + * @param loading Whether the control is now in a loading state. + */ + void onLoadingChanged(boolean loading); + + } + + public static final int DEFAULT_LOW_WATERMARK_MS = 15000; + public static final int DEFAULT_HIGH_WATERMARK_MS = 30000; + public static final float DEFAULT_LOW_BUFFER_LOAD = 0.2f; + public static final float DEFAULT_HIGH_BUFFER_LOAD = 0.8f; + + private static final int ABOVE_HIGH_WATERMARK = 0; + private static final int BETWEEN_WATERMARKS = 1; + private static final int BELOW_LOW_WATERMARK = 2; + + private final Allocator allocator; + private final List loaders; + private final HashMap loaderStates; + private final Handler eventHandler; + private final EventListener eventListener; + + private final long lowWatermarkUs; + private final long highWatermarkUs; + private final float lowBufferLoad; + private final float highBufferLoad; + + private int targetBufferSize; + private long maxLoadStartPositionUs; + private int bufferState; + private boolean fillingBuffers; + private boolean streamingPrioritySet; + + /** + * Constructs a new instance, using the {@code DEFAULT_*} constants defined in this class. + * + * @param allocator The {@link Allocator} used by the loader. + */ + public DefaultLoadControl(Allocator allocator) { + this(allocator, null, null); + } + + /** + * Constructs a new instance, using the {@code DEFAULT_*} constants defined in this class. + * + * @param allocator The {@link Allocator} used by the loader. + * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be + * null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + */ + public DefaultLoadControl(Allocator allocator, Handler eventHandler, + EventListener eventListener) { + this(allocator, eventHandler, eventListener, DEFAULT_LOW_WATERMARK_MS, + DEFAULT_HIGH_WATERMARK_MS, DEFAULT_LOW_BUFFER_LOAD, DEFAULT_HIGH_BUFFER_LOAD); + } + + /** + * Constructs a new instance. + * + * @param allocator The {@link Allocator} used by the loader. + * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be + * null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + * @param lowWatermarkMs The minimum duration of media that can be buffered for the control to + * be in the draining state. If less media is buffered, then the control will transition to + * the filling state. + * @param highWatermarkMs The minimum duration of media that can be buffered for the control to + * transition from filling to draining. + * @param lowBufferLoad The minimum fraction of the buffer that must be utilized for the control + * to be in the draining state. If the utilization is lower, then the control will transition + * to the filling state. + * @param highBufferLoad The minimum fraction of the buffer that must be utilized for the control + * to transition from the loading state to the draining state. + */ + public DefaultLoadControl(Allocator allocator, Handler eventHandler, EventListener eventListener, + int lowWatermarkMs, int highWatermarkMs, float lowBufferLoad, float highBufferLoad) { + this.allocator = allocator; + this.eventHandler = eventHandler; + this.eventListener = eventListener; + this.loaders = new ArrayList<>(); + this.loaderStates = new HashMap<>(); + this.lowWatermarkUs = lowWatermarkMs * 1000L; + this.highWatermarkUs = highWatermarkMs * 1000L; + this.lowBufferLoad = lowBufferLoad; + this.highBufferLoad = highBufferLoad; + } + + @Override + public void register(Object loader, int bufferSizeContribution) { + loaders.add(loader); + loaderStates.put(loader, new LoaderState(bufferSizeContribution)); + targetBufferSize += bufferSizeContribution; + } + + @Override + public void unregister(Object loader) { + loaders.remove(loader); + LoaderState state = loaderStates.remove(loader); + targetBufferSize -= state.bufferSizeContribution; + updateControlState(); + } + + @Override + public void trimAllocator() { + allocator.trim(targetBufferSize); + } + + @Override + public Allocator getAllocator() { + return allocator; + } + + @Override + public boolean update(Object loader, long playbackPositionUs, long nextLoadPositionUs, + boolean loading) { + // Update the loader state. + int loaderBufferState = getLoaderBufferState(playbackPositionUs, nextLoadPositionUs); + LoaderState loaderState = loaderStates.get(loader); + boolean loaderStateChanged = loaderState.bufferState != loaderBufferState + || loaderState.nextLoadPositionUs != nextLoadPositionUs || loaderState.loading != loading; + if (loaderStateChanged) { + loaderState.bufferState = loaderBufferState; + loaderState.nextLoadPositionUs = nextLoadPositionUs; + loaderState.loading = loading; + } + + // Update the buffer state. + int currentBufferSize = allocator.getTotalBytesAllocated(); + int bufferState = getBufferState(currentBufferSize); + boolean bufferStateChanged = this.bufferState != bufferState; + if (bufferStateChanged) { + this.bufferState = bufferState; + } + + // If either of the individual states have changed, update the shared control state. + if (loaderStateChanged || bufferStateChanged) { + updateControlState(); + } + + return currentBufferSize < targetBufferSize && nextLoadPositionUs != -1 + && nextLoadPositionUs <= maxLoadStartPositionUs; + } + + private int getLoaderBufferState(long playbackPositionUs, long nextLoadPositionUs) { + if (nextLoadPositionUs == -1) { + return ABOVE_HIGH_WATERMARK; + } else { + long timeUntilNextLoadPosition = nextLoadPositionUs - playbackPositionUs; + return timeUntilNextLoadPosition > highWatermarkUs ? ABOVE_HIGH_WATERMARK : + timeUntilNextLoadPosition < lowWatermarkUs ? BELOW_LOW_WATERMARK : + BETWEEN_WATERMARKS; + } + } + + private int getBufferState(int currentBufferSize) { + float bufferLoad = (float) currentBufferSize / targetBufferSize; + return bufferLoad > highBufferLoad ? ABOVE_HIGH_WATERMARK + : bufferLoad < lowBufferLoad ? BELOW_LOW_WATERMARK + : BETWEEN_WATERMARKS; + } + + private void updateControlState() { + boolean loading = false; + boolean haveNextLoadPosition = false; + int highestState = bufferState; + for (int i = 0; i < loaders.size(); i++) { + LoaderState loaderState = loaderStates.get(loaders.get(i)); + loading |= loaderState.loading; + haveNextLoadPosition |= loaderState.nextLoadPositionUs != -1; + highestState = Math.max(highestState, loaderState.bufferState); + } + + fillingBuffers = !loaders.isEmpty() && (loading || haveNextLoadPosition) + && (highestState == BELOW_LOW_WATERMARK + || (highestState == BETWEEN_WATERMARKS && fillingBuffers)); + if (fillingBuffers && !streamingPrioritySet) { + NetworkLock.instance.add(NetworkLock.STREAMING_PRIORITY); + streamingPrioritySet = true; + notifyLoadingChanged(true); + } else if (!fillingBuffers && streamingPrioritySet && !loading) { + NetworkLock.instance.remove(NetworkLock.STREAMING_PRIORITY); + streamingPrioritySet = false; + notifyLoadingChanged(false); + } + + maxLoadStartPositionUs = -1; + if (fillingBuffers) { + for (int i = 0; i < loaders.size(); i++) { + Object loader = loaders.get(i); + LoaderState loaderState = loaderStates.get(loader); + long loaderTime = loaderState.nextLoadPositionUs; + if (loaderTime != -1 + && (maxLoadStartPositionUs == -1 || loaderTime < maxLoadStartPositionUs)) { + maxLoadStartPositionUs = loaderTime; + } + } + } + } + + private void notifyLoadingChanged(final boolean loading) { + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onLoadingChanged(loading); + } + }); + } + } + + private static class LoaderState { + + public final int bufferSizeContribution; + + public int bufferState; + public boolean loading; + public long nextLoadPositionUs; + + public LoaderState(int bufferSizeContribution) { + this.bufferSizeContribution = bufferSizeContribution; + bufferState = ABOVE_HIGH_WATERMARK; + loading = false; + nextLoadPositionUs = -1; + } + + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/DummyTrackRenderer.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/DummyTrackRenderer.java new file mode 100755 index 000000000..9afe11f0e --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/DummyTrackRenderer.java @@ -0,0 +1,77 @@ +/* + * 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.messenger.exoplayer; + +/** + * A {@link TrackRenderer} that does nothing. + *

+ * This renderer returns 0 from {@link #getTrackCount()} in order to request that it should be + * ignored. {@link IllegalStateException} is thrown from all other methods documented to indicate + * that they should not be invoked unless the renderer is prepared. + */ +public final class DummyTrackRenderer extends TrackRenderer { + + @Override + protected boolean doPrepare(long positionUs) throws ExoPlaybackException { + return true; + } + + @Override + protected int getTrackCount() { + return 0; + } + + @Override + protected MediaFormat getFormat(int track) { + throw new IllegalStateException(); + } + + @Override + protected boolean isEnded() { + throw new IllegalStateException(); + } + + @Override + protected boolean isReady() { + throw new IllegalStateException(); + } + + @Override + protected void seekTo(long positionUs) { + throw new IllegalStateException(); + } + + @Override + protected void doSomeWork(long positionUs, long elapsedRealtimeUs) { + throw new IllegalStateException(); + } + + @Override + protected void maybeThrowError() { + throw new IllegalStateException(); + } + + @Override + protected long getDurationUs() { + throw new IllegalStateException(); + } + + @Override + protected long getBufferedPositionUs() { + throw new IllegalStateException(); + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/ExoPlaybackException.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/ExoPlaybackException.java new file mode 100755 index 000000000..9a1bd5cf7 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/ExoPlaybackException.java @@ -0,0 +1,51 @@ +/* + * 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.messenger.exoplayer; + +/** + * Thrown when a non-recoverable playback failure occurs. + *

+ * Where possible, the cause returned by {@link #getCause()} will indicate the reason for failure. + */ +public final class ExoPlaybackException extends Exception { + + /** + * True if the cause (i.e. the {@link Throwable} returned by {@link #getCause()}) was only caught + * by a fail-safe at the top level of the player. False otherwise. + */ + public final boolean caughtAtTopLevel; + + public ExoPlaybackException(String message) { + super(message); + caughtAtTopLevel = false; + } + + public ExoPlaybackException(Throwable cause) { + super(cause); + caughtAtTopLevel = false; + } + + public ExoPlaybackException(String message, Throwable cause) { + super(message, cause); + caughtAtTopLevel = false; + } + + /* package */ ExoPlaybackException(Throwable cause, boolean caughtAtTopLevel) { + super(cause); + this.caughtAtTopLevel = caughtAtTopLevel; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/ExoPlayer.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/ExoPlayer.java new file mode 100755 index 000000000..d9887f3fe --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/ExoPlayer.java @@ -0,0 +1,415 @@ +/* + * 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.messenger.exoplayer; + +import android.os.Looper; + +/** + * An extensible media player exposing traditional high-level media player functionality, such as + * the ability to prepare, play, pause and seek. + * + *

Topics covered here are: + *

    + *
  1. Assumptions and player composition + *
  2. Threading model + *
  3. Player state + *
+ * + * + *

Assumptions and player construction

+ * + *

The implementation is designed to make no assumptions about (and hence impose no restrictions + * on) the type of the media being played, how and where it is stored, or how it is rendered. + * Rather than implementing the loading and rendering of media directly, {@link ExoPlayer} instead + * delegates this work to one or more {@link TrackRenderer}s, which are injected when the player + * is prepared. Hence {@link ExoPlayer} is capable of loading and playing any media for which a + * {@link TrackRenderer} implementation can be provided. + * + *

{@link MediaCodecAudioTrackRenderer} and {@link MediaCodecVideoTrackRenderer} can be used for + * the common cases of rendering audio and video. These components in turn require an + * upstream {@link SampleSource} to be injected through their constructors, where upstream + * is defined to denote a component that is closer to the source of the media. This pattern of + * upstream dependency injection is actively encouraged, since it means that the functionality of + * the player is built up through the composition of components that can easily be exchanged for + * alternate implementations. For example a {@link SampleSource} implementation may require a + * further upstream data loading component to be injected through its constructor, with different + * implementations enabling the loading of data from various sources. + * + * + *

Threading model

+ * + *

The figure below shows the {@link ExoPlayer} threading model.

+ *

MediaPlayer state diagram

+ * + *
    + *
  • It is recommended that instances are created and accessed from a single application thread. + * An application's main thread is ideal. Accessing an instance from multiple threads is + * discouraged, however if an application does wish to do this then it may do so provided that it + * ensures accesses are synchronized. + *
  • + *
  • Registered {@link Listener}s are invoked on the thread that created the {@link ExoPlayer} + * instance.
  • + *
  • An internal playback thread is responsible for managing playback and invoking the + * {@link TrackRenderer}s in order to load and play the media.
  • + *
  • {@link TrackRenderer} implementations (or any upstream components that they depend on) may + * use additional background threads (e.g. to load data). These are implementation specific.
  • + *
+ * + * + *

Player state

+ * + *

The components of an {@link ExoPlayer}'s state can be divided into two distinct groups. State + * accessed by {@link #getSelectedTrack(int)} and {@link #getPlayWhenReady()} is only ever + * changed by invoking the player's methods, and are never changed as a result of operations that + * have been performed asynchronously by the playback thread. In contrast, the playback state + * accessed by {@link #getPlaybackState()} is only ever changed as a result of operations + * completing on the playback thread, as illustrated below.

+ *

ExoPlayer state

+ * + *

The possible playback state transitions are shown below. Transitions can be triggered either + * by changes in the state of the {@link TrackRenderer}s being used, or as a result of + * {@link #prepare(TrackRenderer[])}, {@link #stop()} or {@link #release()} being invoked.

+ *

ExoPlayer playback state transitions

+ */ +public interface ExoPlayer { + + /** + * A factory for instantiating ExoPlayer instances. + */ + public static final class Factory { + + /** + * The default minimum duration of data that must be buffered for playback to start or resume + * following a user action such as a seek. + */ + public static final int DEFAULT_MIN_BUFFER_MS = 2500; + + /** + * The default minimum duration of data that must be buffered for playback to resume + * after a player invoked rebuffer (i.e. a rebuffer that occurs due to buffer depletion, and + * not due to a user action such as starting playback or seeking). + */ + public static final int DEFAULT_MIN_REBUFFER_MS = 5000; + + private Factory() {} + + /** + * Obtains an {@link ExoPlayer} instance. + *

+ * Must be invoked from a thread that has an associated {@link Looper}. + * + * @param rendererCount The number of {@link TrackRenderer}s that will be passed to + * {@link #prepare(TrackRenderer[])}. + * @param minBufferMs A minimum duration of data that must be buffered for playback to start + * or resume following a user action such as a seek. + * @param minRebufferMs A minimum duration of data that must be buffered for playback to resume + * after a player invoked rebuffer (i.e. a rebuffer that occurs due to buffer depletion, and + * not due to a user action such as starting playback or seeking). + */ + public static ExoPlayer newInstance(int rendererCount, int minBufferMs, int minRebufferMs) { + return new ExoPlayerImpl(rendererCount, minBufferMs, minRebufferMs); + } + + /** + * Obtains an {@link ExoPlayer} instance. + *

+ * Must be invoked from a thread that has an associated {@link Looper}. + * + * @param rendererCount The number of {@link TrackRenderer}s that will be passed to + * {@link #prepare(TrackRenderer[])}. + */ + public static ExoPlayer newInstance(int rendererCount) { + return new ExoPlayerImpl(rendererCount, DEFAULT_MIN_BUFFER_MS, DEFAULT_MIN_REBUFFER_MS); + } + + } + + /** + * Interface definition for a callback to be notified of changes in player state. + */ + public interface Listener { + /** + * Invoked when the value returned from either {@link ExoPlayer#getPlayWhenReady()} or + * {@link ExoPlayer#getPlaybackState()} changes. + * + * @param playWhenReady Whether playback will proceed when ready. + * @param playbackState One of the {@code STATE} constants defined in the {@link ExoPlayer} + * interface. + */ + void onPlayerStateChanged(boolean playWhenReady, int playbackState); + /** + * Invoked when the current value of {@link ExoPlayer#getPlayWhenReady()} has been reflected + * by the internal playback thread. + *

+ * An invocation of this method will shortly follow any call to + * {@link ExoPlayer#setPlayWhenReady(boolean)} that changes the state. If multiple calls are + * made in rapid succession, then this method will be invoked only once, after the final state + * has been reflected. + */ + void onPlayWhenReadyCommitted(); + /** + * Invoked when an error occurs. The playback state will transition to + * {@link ExoPlayer#STATE_IDLE} immediately after this method is invoked. The player instance + * can still be used, and {@link ExoPlayer#release()} must still be called on the player should + * it no longer be required. + * + * @param error The error. + */ + void onPlayerError(ExoPlaybackException error); + } + + /** + * A component of an {@link ExoPlayer} that can receive messages on the playback thread. + *

+ * Messages can be delivered to a component via {@link ExoPlayer#sendMessage} and + * {@link ExoPlayer#blockingSendMessage}. + */ + public interface ExoPlayerComponent { + + /** + * Handles a message delivered to the component. Invoked on the playback thread. + * + * @param messageType An integer identifying the type of message. + * @param message The message object. + * @throws ExoPlaybackException If an error occurred whilst handling the message. + */ + void handleMessage(int messageType, Object message) throws ExoPlaybackException; + + } + + /** + * The player is neither prepared or being prepared. + */ + public static final int STATE_IDLE = 1; + /** + * The player is being prepared. + */ + public static final int STATE_PREPARING = 2; + /** + * The player is prepared but not able to immediately play from the current position. The cause + * is {@link TrackRenderer} specific, but this state typically occurs when more data needs + * to be buffered for playback to start. + */ + public static final int STATE_BUFFERING = 3; + /** + * The player is prepared and able to immediately play from the current position. The player will + * be playing if {@link #getPlayWhenReady()} returns true, and paused otherwise. + */ + public static final int STATE_READY = 4; + /** + * The player has finished playing the media. + */ + public static final int STATE_ENDED = 5; + + /** + * A value that can be passed as the second argument to {@link #setSelectedTrack(int, int)} to + * disable the renderer. + */ + public static final int TRACK_DISABLED = -1; + /** + * A value that can be passed as the second argument to {@link #setSelectedTrack(int, int)} to + * select the default track. + */ + public static final int TRACK_DEFAULT = 0; + + /** + * Represents an unknown time or duration. + */ + public static final long UNKNOWN_TIME = -1; + + /** + * Gets the {@link Looper} associated with the playback thread. + * + * @return The {@link Looper} associated with the playback thread. + */ + public Looper getPlaybackLooper(); + + /** + * Register a listener to receive events from the player. The listener's methods will be invoked + * on the thread that was used to construct the player. + * + * @param listener The listener to register. + */ + public void addListener(Listener listener); + + /** + * Unregister a listener. The listener will no longer receive events from the player. + * + * @param listener The listener to unregister. + */ + public void removeListener(Listener listener); + + /** + * Returns the current state of the player. + * + * @return One of the {@code STATE} constants defined in this interface. + */ + public int getPlaybackState(); + + /** + * Prepares the player for playback. + * + * @param renderers The {@link TrackRenderer}s to use. The number of renderers must match the + * value that was passed to the {@link ExoPlayer.Factory#newInstance} method. + */ + public void prepare(TrackRenderer... renderers); + + /** + * Returns the number of tracks exposed by the specified renderer. + * + * @param rendererIndex The index of the renderer. + * @return The number of tracks. + */ + public int getTrackCount(int rendererIndex); + + /** + * Returns the format of a track. + * + * @param rendererIndex The index of the renderer. + * @param trackIndex The index of the track. + * @return The format of the track. + */ + public MediaFormat getTrackFormat(int rendererIndex, int trackIndex); + + /** + * Selects a track for the specified renderer. + * + * @param rendererIndex The index of the renderer. + * @param trackIndex The index of the track. A negative value or a value greater than or equal to + * the renderer's track count will disable the renderer. + */ + public void setSelectedTrack(int rendererIndex, int trackIndex); + + /** + * Returns the index of the currently selected track for the specified renderer. + * + * @param rendererIndex The index of the renderer. + * @return The selected track. A negative value or a value greater than or equal to the renderer's + * track count indicates that the renderer is disabled. + */ + public int getSelectedTrack(int rendererIndex); + + /** + * Sets whether playback should proceed when {@link #getPlaybackState()} == {@link #STATE_READY}. + * If the player is already in this state, then this method can be used to pause and resume + * playback. + * + * @param playWhenReady Whether playback should proceed when ready. + */ + public void setPlayWhenReady(boolean playWhenReady); + + /** + * Whether playback will proceed when {@link #getPlaybackState()} == {@link #STATE_READY}. + * + * @return Whether playback will proceed when ready. + */ + public boolean getPlayWhenReady(); + + /** + * Whether the current value of {@link ExoPlayer#getPlayWhenReady()} has been reflected by the + * internal playback thread. + * + * @return True if the current value has been reflected. False otherwise. + */ + public boolean isPlayWhenReadyCommitted(); + + /** + * Seeks to a position specified in milliseconds. + * + * @param positionMs The seek position. + */ + public void seekTo(long positionMs); + + /** + * Stops playback. Use {@code setPlayWhenReady(false)} rather than this method if the intention + * is to pause playback. + *

+ * Calling this method will cause the playback state to transition to + * {@link ExoPlayer#STATE_IDLE}. The player instance can still be used, and + * {@link ExoPlayer#release()} must still be called on the player if it's no longer required. + *

+ * Calling this method does not reset the playback position. If this player instance will be used + * to play another video from its start, then {@code seekTo(0)} should be called after stopping + * the player and before preparing it for the next video. + */ + public void stop(); + + /** + * Releases the player. This method must be called when the player is no longer required. + *

+ * The player must not be used after calling this method. + */ + public void release(); + + /** + * Sends a message to a specified component. The message is delivered to the component on the + * playback thread. If the component throws a {@link ExoPlaybackException}, then it is + * propagated out of the player as an error. + * + * @param target The target to which the message should be delivered. + * @param messageType An integer that can be used to identify the type of the message. + * @param message The message object. + */ + public void sendMessage(ExoPlayerComponent target, int messageType, Object message); + + /** + * Blocking variant of {@link #sendMessage(ExoPlayerComponent, int, Object)} that does not return + * until after the message has been delivered. + * + * @param target The target to which the message should be delivered. + * @param messageType An integer that can be used to identify the type of the message. + * @param message The message object. + */ + public void blockingSendMessage(ExoPlayerComponent target, int messageType, Object message); + + /** + * Gets the duration of the track in milliseconds. + * + * @return The duration of the track in milliseconds, or {@link ExoPlayer#UNKNOWN_TIME} if the + * duration is not known. + */ + public long getDuration(); + + /** + * Gets the current playback position in milliseconds. + * + * @return The current playback position in milliseconds. + */ + public long getCurrentPosition(); + + /** + * Gets an estimate of the absolute position in milliseconds up to which data is buffered. + * + * @return An estimate of the absolute position in milliseconds up to which data is buffered, + * or {@link ExoPlayer#UNKNOWN_TIME} if no estimate is available. + */ + public long getBufferedPosition(); + + /** + * Gets an estimate of the percentage into the media up to which data is buffered. + * + * @return An estimate of the percentage into the media up to which data is buffered. 0 if the + * duration of the media is not known or if no estimate is available. + */ + public int getBufferedPercentage(); + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/ExoPlayerImpl.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/ExoPlayerImpl.java new file mode 100755 index 000000000..3e4a19009 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/ExoPlayerImpl.java @@ -0,0 +1,230 @@ +/* + * 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.messenger.exoplayer; + +import android.annotation.SuppressLint; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.util.Log; + +import java.util.Arrays; +import java.util.concurrent.CopyOnWriteArraySet; + +/** + * Concrete implementation of {@link ExoPlayer}. + */ +/* package */ final class ExoPlayerImpl implements ExoPlayer { + + private static final String TAG = "ExoPlayerImpl"; + + private final Handler eventHandler; + private final ExoPlayerImplInternal internalPlayer; + private final CopyOnWriteArraySet listeners; + private final MediaFormat[][] trackFormats; + private final int[] selectedTrackIndices; + + private boolean playWhenReady; + private int playbackState; + private int pendingPlayWhenReadyAcks; + + /** + * Constructs an instance. Must be invoked from a thread that has an associated {@link Looper}. + * + * @param rendererCount The number of {@link TrackRenderer}s that will be passed to + * {@link #prepare(TrackRenderer[])}. + * @param minBufferMs A minimum duration of data that must be buffered for playback to start + * or resume following a user action such as a seek. + * @param minRebufferMs A minimum duration of data that must be buffered for playback to resume + * after a player invoked rebuffer (i.e. a rebuffer that occurs due to buffer depletion, and + * not due to a user action such as starting playback or seeking). + */ + @SuppressLint("HandlerLeak") + public ExoPlayerImpl(int rendererCount, int minBufferMs, int minRebufferMs) { + Log.i(TAG, "Init " + ExoPlayerLibraryInfo.VERSION); + this.playWhenReady = false; + this.playbackState = STATE_IDLE; + this.listeners = new CopyOnWriteArraySet<>(); + this.trackFormats = new MediaFormat[rendererCount][]; + this.selectedTrackIndices = new int[rendererCount]; + eventHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + ExoPlayerImpl.this.handleEvent(msg); + } + }; + internalPlayer = new ExoPlayerImplInternal(eventHandler, playWhenReady, selectedTrackIndices, + minBufferMs, minRebufferMs); + } + + @Override + public Looper getPlaybackLooper() { + return internalPlayer.getPlaybackLooper(); + } + + @Override + public void addListener(Listener listener) { + listeners.add(listener); + } + + @Override + public void removeListener(Listener listener) { + listeners.remove(listener); + } + + @Override + public int getPlaybackState() { + return playbackState; + } + + @Override + public void prepare(TrackRenderer... renderers) { + Arrays.fill(trackFormats, null); + internalPlayer.prepare(renderers); + } + + @Override + public int getTrackCount(int rendererIndex) { + return trackFormats[rendererIndex] != null ? trackFormats[rendererIndex].length : 0; + } + + @Override + public MediaFormat getTrackFormat(int rendererIndex, int trackIndex) { + return trackFormats[rendererIndex][trackIndex]; + } + + @Override + public void setSelectedTrack(int rendererIndex, int trackIndex) { + if (selectedTrackIndices[rendererIndex] != trackIndex) { + selectedTrackIndices[rendererIndex] = trackIndex; + internalPlayer.setRendererSelectedTrack(rendererIndex, trackIndex); + } + } + + @Override + public int getSelectedTrack(int rendererIndex) { + return selectedTrackIndices[rendererIndex]; + } + + @Override + public void setPlayWhenReady(boolean playWhenReady) { + if (this.playWhenReady != playWhenReady) { + this.playWhenReady = playWhenReady; + pendingPlayWhenReadyAcks++; + internalPlayer.setPlayWhenReady(playWhenReady); + for (Listener listener : listeners) { + listener.onPlayerStateChanged(playWhenReady, playbackState); + } + } + } + + @Override + public boolean getPlayWhenReady() { + return playWhenReady; + } + + @Override + public boolean isPlayWhenReadyCommitted() { + return pendingPlayWhenReadyAcks == 0; + } + + @Override + public void seekTo(long positionMs) { + internalPlayer.seekTo(positionMs); + } + + @Override + public void stop() { + internalPlayer.stop(); + } + + @Override + public void release() { + internalPlayer.release(); + eventHandler.removeCallbacksAndMessages(null); + } + + @Override + public void sendMessage(ExoPlayerComponent target, int messageType, Object message) { + internalPlayer.sendMessage(target, messageType, message); + } + + @Override + public void blockingSendMessage(ExoPlayerComponent target, int messageType, Object message) { + internalPlayer.blockingSendMessage(target, messageType, message); + } + + @Override + public long getDuration() { + return internalPlayer.getDuration(); + } + + @Override + public long getCurrentPosition() { + return internalPlayer.getCurrentPosition(); + } + + @Override + public long getBufferedPosition() { + return internalPlayer.getBufferedPosition(); + } + + @Override + public int getBufferedPercentage() { + long bufferedPosition = getBufferedPosition(); + long duration = getDuration(); + return bufferedPosition == ExoPlayer.UNKNOWN_TIME || duration == ExoPlayer.UNKNOWN_TIME ? 0 + : (int) (duration == 0 ? 100 : (bufferedPosition * 100) / duration); + } + + // Not private so it can be called from an inner class without going through a thunk method. + /* package */ void handleEvent(Message msg) { + switch (msg.what) { + case ExoPlayerImplInternal.MSG_PREPARED: { + System.arraycopy(msg.obj, 0, trackFormats, 0, trackFormats.length); + playbackState = msg.arg1; + for (Listener listener : listeners) { + listener.onPlayerStateChanged(playWhenReady, playbackState); + } + break; + } + case ExoPlayerImplInternal.MSG_STATE_CHANGED: { + playbackState = msg.arg1; + for (Listener listener : listeners) { + listener.onPlayerStateChanged(playWhenReady, playbackState); + } + break; + } + case ExoPlayerImplInternal.MSG_SET_PLAY_WHEN_READY_ACK: { + pendingPlayWhenReadyAcks--; + if (pendingPlayWhenReadyAcks == 0) { + for (Listener listener : listeners) { + listener.onPlayWhenReadyCommitted(); + } + } + break; + } + case ExoPlayerImplInternal.MSG_ERROR: { + ExoPlaybackException exception = (ExoPlaybackException) msg.obj; + for (Listener listener : listeners) { + listener.onPlayerError(exception); + } + break; + } + } + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/ExoPlayerImplInternal.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/ExoPlayerImplInternal.java new file mode 100755 index 000000000..8f93543bc --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/ExoPlayerImplInternal.java @@ -0,0 +1,668 @@ +/* + * 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.messenger.exoplayer; + +import org.telegram.messenger.exoplayer.ExoPlayer.ExoPlayerComponent; +import org.telegram.messenger.exoplayer.util.Assertions; +import org.telegram.messenger.exoplayer.util.PriorityHandlerThread; +import org.telegram.messenger.exoplayer.util.TraceUtil; +import org.telegram.messenger.exoplayer.util.Util; + +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; +import android.os.Process; +import android.os.SystemClock; +import android.util.Log; +import android.util.Pair; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Implements the internal behavior of {@link ExoPlayerImpl}. + */ +/* package */ final class ExoPlayerImplInternal implements Handler.Callback { + + private static final String TAG = "ExoPlayerImplInternal"; + + // External messages + public static final int MSG_PREPARED = 1; + public static final int MSG_STATE_CHANGED = 2; + public static final int MSG_SET_PLAY_WHEN_READY_ACK = 3; + public static final int MSG_ERROR = 4; + + // Internal messages + private static final int MSG_PREPARE = 1; + private static final int MSG_INCREMENTAL_PREPARE = 2; + private static final int MSG_SET_PLAY_WHEN_READY = 3; + private static final int MSG_STOP = 4; + private static final int MSG_RELEASE = 5; + private static final int MSG_SEEK_TO = 6; + private static final int MSG_DO_SOME_WORK = 7; + private static final int MSG_SET_RENDERER_SELECTED_TRACK = 8; + private static final int MSG_CUSTOM = 9; + + private static final int PREPARE_INTERVAL_MS = 10; + private static final int RENDERING_INTERVAL_MS = 10; + private static final int IDLE_INTERVAL_MS = 1000; + + private final Handler handler; + private final HandlerThread internalPlaybackThread; + private final Handler eventHandler; + private final StandaloneMediaClock standaloneMediaClock; + private final AtomicInteger pendingSeekCount; + private final List enabledRenderers; + private final MediaFormat[][] trackFormats; + private final int[] selectedTrackIndices; + private final long minBufferUs; + private final long minRebufferUs; + + private TrackRenderer[] renderers; + private TrackRenderer rendererMediaClockSource; + private MediaClock rendererMediaClock; + + private boolean released; + private boolean playWhenReady; + private boolean rebuffering; + private int state; + private int customMessagesSent = 0; + private int customMessagesProcessed = 0; + private long lastSeekPositionMs; + private long elapsedRealtimeUs; + + private volatile long durationUs; + private volatile long positionUs; + private volatile long bufferedPositionUs; + + public ExoPlayerImplInternal(Handler eventHandler, boolean playWhenReady, + int[] selectedTrackIndices, int minBufferMs, int minRebufferMs) { + this.eventHandler = eventHandler; + this.playWhenReady = playWhenReady; + this.minBufferUs = minBufferMs * 1000L; + this.minRebufferUs = minRebufferMs * 1000L; + this.selectedTrackIndices = Arrays.copyOf(selectedTrackIndices, selectedTrackIndices.length); + this.state = ExoPlayer.STATE_IDLE; + this.durationUs = TrackRenderer.UNKNOWN_TIME_US; + this.bufferedPositionUs = TrackRenderer.UNKNOWN_TIME_US; + + standaloneMediaClock = new StandaloneMediaClock(); + pendingSeekCount = new AtomicInteger(); + enabledRenderers = new ArrayList<>(selectedTrackIndices.length); + trackFormats = new MediaFormat[selectedTrackIndices.length][]; + // Note: The documentation for Process.THREAD_PRIORITY_AUDIO that states "Applications can + // not normally change to this priority" is incorrect. + internalPlaybackThread = new PriorityHandlerThread("ExoPlayerImplInternal:Handler", + Process.THREAD_PRIORITY_AUDIO); + internalPlaybackThread.start(); + handler = new Handler(internalPlaybackThread.getLooper(), this); + } + + public Looper getPlaybackLooper() { + return internalPlaybackThread.getLooper(); + } + + public long getCurrentPosition() { + return pendingSeekCount.get() > 0 ? lastSeekPositionMs : (positionUs / 1000); + } + + public long getBufferedPosition() { + return bufferedPositionUs == TrackRenderer.UNKNOWN_TIME_US ? ExoPlayer.UNKNOWN_TIME + : bufferedPositionUs / 1000; + } + + public long getDuration() { + return durationUs == TrackRenderer.UNKNOWN_TIME_US ? ExoPlayer.UNKNOWN_TIME + : durationUs / 1000; + } + + public void prepare(TrackRenderer... renderers) { + handler.obtainMessage(MSG_PREPARE, renderers).sendToTarget(); + } + + public void setPlayWhenReady(boolean playWhenReady) { + handler.obtainMessage(MSG_SET_PLAY_WHEN_READY, playWhenReady ? 1 : 0, 0).sendToTarget(); + } + + public void seekTo(long positionMs) { + lastSeekPositionMs = positionMs; + pendingSeekCount.incrementAndGet(); + handler.obtainMessage(MSG_SEEK_TO, Util.getTopInt(positionMs), + Util.getBottomInt(positionMs)).sendToTarget(); + } + + public void stop() { + handler.sendEmptyMessage(MSG_STOP); + } + + public void setRendererSelectedTrack(int rendererIndex, int trackIndex) { + handler.obtainMessage(MSG_SET_RENDERER_SELECTED_TRACK, rendererIndex, trackIndex) + .sendToTarget(); + } + + public void sendMessage(ExoPlayerComponent target, int messageType, Object message) { + customMessagesSent++; + handler.obtainMessage(MSG_CUSTOM, messageType, 0, Pair.create(target, message)).sendToTarget(); + } + + public synchronized void blockingSendMessage(ExoPlayerComponent target, int messageType, + Object message) { + if (released) { + Log.w(TAG, "Sent message(" + messageType + ") after release. Message ignored."); + return; + } + int messageNumber = customMessagesSent++; + handler.obtainMessage(MSG_CUSTOM, messageType, 0, Pair.create(target, message)).sendToTarget(); + while (customMessagesProcessed <= messageNumber) { + try { + wait(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + + public synchronized void release() { + if (released) { + return; + } + handler.sendEmptyMessage(MSG_RELEASE); + while (!released) { + try { + wait(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + internalPlaybackThread.quit(); + } + + @Override + public boolean handleMessage(Message msg) { + try { + switch (msg.what) { + case MSG_PREPARE: { + prepareInternal((TrackRenderer[]) msg.obj); + return true; + } + case MSG_INCREMENTAL_PREPARE: { + incrementalPrepareInternal(); + return true; + } + case MSG_SET_PLAY_WHEN_READY: { + setPlayWhenReadyInternal(msg.arg1 != 0); + return true; + } + case MSG_DO_SOME_WORK: { + doSomeWork(); + return true; + } + case MSG_SEEK_TO: { + seekToInternal(Util.getLong(msg.arg1, msg.arg2)); + return true; + } + case MSG_STOP: { + stopInternal(); + return true; + } + case MSG_RELEASE: { + releaseInternal(); + return true; + } + case MSG_CUSTOM: { + sendMessageInternal(msg.arg1, msg.obj); + return true; + } + case MSG_SET_RENDERER_SELECTED_TRACK: { + setRendererSelectedTrackInternal(msg.arg1, msg.arg2); + return true; + } + default: + return false; + } + } catch (ExoPlaybackException e) { + Log.e(TAG, "Internal track renderer error.", e); + eventHandler.obtainMessage(MSG_ERROR, e).sendToTarget(); + stopInternal(); + return true; + } catch (RuntimeException e) { + Log.e(TAG, "Internal runtime error.", e); + eventHandler.obtainMessage(MSG_ERROR, new ExoPlaybackException(e, true)).sendToTarget(); + stopInternal(); + return true; + } + } + + private void setState(int state) { + if (this.state != state) { + this.state = state; + eventHandler.obtainMessage(MSG_STATE_CHANGED, state, 0).sendToTarget(); + } + } + + private void prepareInternal(TrackRenderer[] renderers) throws ExoPlaybackException { + resetInternal(); + this.renderers = renderers; + Arrays.fill(trackFormats, null); + setState(ExoPlayer.STATE_PREPARING); + incrementalPrepareInternal(); + } + + private void incrementalPrepareInternal() throws ExoPlaybackException { + long operationStartTimeMs = SystemClock.elapsedRealtime(); + boolean prepared = true; + for (int rendererIndex = 0; rendererIndex < renderers.length; rendererIndex++) { + TrackRenderer renderer = renderers[rendererIndex]; + if (renderer.getState() == TrackRenderer.STATE_UNPREPARED) { + int state = renderer.prepare(positionUs); + if (state == TrackRenderer.STATE_UNPREPARED) { + renderer.maybeThrowError(); + prepared = false; + } + } + } + + if (!prepared) { + // We're still waiting for some sources to be prepared. + scheduleNextOperation(MSG_INCREMENTAL_PREPARE, operationStartTimeMs, PREPARE_INTERVAL_MS); + return; + } + + long durationUs = 0; + boolean allRenderersEnded = true; + boolean allRenderersReadyOrEnded = true; + for (int rendererIndex = 0; rendererIndex < renderers.length; rendererIndex++) { + TrackRenderer renderer = renderers[rendererIndex]; + int rendererTrackCount = renderer.getTrackCount(); + MediaFormat[] rendererTrackFormats = new MediaFormat[rendererTrackCount]; + for (int trackIndex = 0; trackIndex < rendererTrackCount; trackIndex++) { + rendererTrackFormats[trackIndex] = renderer.getFormat(trackIndex); + } + trackFormats[rendererIndex] = rendererTrackFormats; + if (rendererTrackCount > 0) { + if (durationUs == TrackRenderer.UNKNOWN_TIME_US) { + // We've already encountered a track for which the duration is unknown, so the media + // duration is unknown regardless of the duration of this track. + } else { + long trackDurationUs = renderer.getDurationUs(); + if (trackDurationUs == TrackRenderer.UNKNOWN_TIME_US) { + durationUs = TrackRenderer.UNKNOWN_TIME_US; + } else if (trackDurationUs == TrackRenderer.MATCH_LONGEST_US) { + // Do nothing. + } else { + durationUs = Math.max(durationUs, trackDurationUs); + } + } + int trackIndex = selectedTrackIndices[rendererIndex]; + if (0 <= trackIndex && trackIndex < rendererTrackFormats.length) { + enableRenderer(renderer, trackIndex, false); + allRenderersEnded = allRenderersEnded && renderer.isEnded(); + allRenderersReadyOrEnded = allRenderersReadyOrEnded && rendererReadyOrEnded(renderer); + } + } + } + this.durationUs = durationUs; + + if (allRenderersEnded + && (durationUs == TrackRenderer.UNKNOWN_TIME_US || durationUs <= positionUs)) { + // We don't expect this case, but handle it anyway. + state = ExoPlayer.STATE_ENDED; + } else { + state = allRenderersReadyOrEnded ? ExoPlayer.STATE_READY : ExoPlayer.STATE_BUFFERING; + } + + // Fire an event indicating that the player has been prepared, passing the initial state and + // renderer track information. + eventHandler.obtainMessage(MSG_PREPARED, state, 0, trackFormats).sendToTarget(); + + // Start the renderers if required, and schedule the first piece of work. + if (playWhenReady && state == ExoPlayer.STATE_READY) { + startRenderers(); + } + handler.sendEmptyMessage(MSG_DO_SOME_WORK); + } + + private void enableRenderer(TrackRenderer renderer, int trackIndex, boolean joining) + throws ExoPlaybackException { + renderer.enable(trackIndex, positionUs, joining); + enabledRenderers.add(renderer); + MediaClock mediaClock = renderer.getMediaClock(); + if (mediaClock != null) { + Assertions.checkState(rendererMediaClock == null); + rendererMediaClock = mediaClock; + rendererMediaClockSource = renderer; + } + } + + private boolean rendererReadyOrEnded(TrackRenderer renderer) { + if (renderer.isEnded()) { + return true; + } + if (!renderer.isReady()) { + return false; + } + if (state == ExoPlayer.STATE_READY) { + return true; + } + long rendererDurationUs = renderer.getDurationUs(); + long rendererBufferedPositionUs = renderer.getBufferedPositionUs(); + long minBufferDurationUs = rebuffering ? minRebufferUs : minBufferUs; + return minBufferDurationUs <= 0 + || rendererBufferedPositionUs == TrackRenderer.UNKNOWN_TIME_US + || rendererBufferedPositionUs == TrackRenderer.END_OF_TRACK_US + || rendererBufferedPositionUs >= positionUs + minBufferDurationUs + || (rendererDurationUs != TrackRenderer.UNKNOWN_TIME_US + && rendererDurationUs != TrackRenderer.MATCH_LONGEST_US + && rendererBufferedPositionUs >= rendererDurationUs); + } + + private void setPlayWhenReadyInternal(boolean playWhenReady) throws ExoPlaybackException { + try { + rebuffering = false; + this.playWhenReady = playWhenReady; + if (!playWhenReady) { + stopRenderers(); + updatePositionUs(); + } else { + if (state == ExoPlayer.STATE_READY) { + startRenderers(); + handler.sendEmptyMessage(MSG_DO_SOME_WORK); + } else if (state == ExoPlayer.STATE_BUFFERING) { + handler.sendEmptyMessage(MSG_DO_SOME_WORK); + } + } + } finally { + eventHandler.obtainMessage(MSG_SET_PLAY_WHEN_READY_ACK).sendToTarget(); + } + } + + private void startRenderers() throws ExoPlaybackException { + rebuffering = false; + standaloneMediaClock.start(); + for (int i = 0; i < enabledRenderers.size(); i++) { + enabledRenderers.get(i).start(); + } + } + + private void stopRenderers() throws ExoPlaybackException { + standaloneMediaClock.stop(); + for (int i = 0; i < enabledRenderers.size(); i++) { + ensureStopped(enabledRenderers.get(i)); + } + } + + private void updatePositionUs() { + if (rendererMediaClock != null && enabledRenderers.contains(rendererMediaClockSource) + && !rendererMediaClockSource.isEnded()) { + positionUs = rendererMediaClock.getPositionUs(); + standaloneMediaClock.setPositionUs(positionUs); + } else { + positionUs = standaloneMediaClock.getPositionUs(); + } + elapsedRealtimeUs = SystemClock.elapsedRealtime() * 1000; + } + + private void doSomeWork() throws ExoPlaybackException { + TraceUtil.beginSection("doSomeWork"); + long operationStartTimeMs = SystemClock.elapsedRealtime(); + long bufferedPositionUs = durationUs != TrackRenderer.UNKNOWN_TIME_US ? durationUs + : Long.MAX_VALUE; + boolean allRenderersEnded = true; + boolean allRenderersReadyOrEnded = true; + updatePositionUs(); + for (int i = 0; i < enabledRenderers.size(); i++) { + TrackRenderer renderer = enabledRenderers.get(i); + // TODO: Each renderer should return the maximum delay before which it wishes to be + // invoked again. The minimum of these values should then be used as the delay before the next + // invocation of this method. + renderer.doSomeWork(positionUs, elapsedRealtimeUs); + allRenderersEnded = allRenderersEnded && renderer.isEnded(); + + // Determine whether the renderer is ready (or ended). If it's not, throw an error that's + // preventing the renderer from making progress, if such an error exists. + boolean rendererReadyOrEnded = rendererReadyOrEnded(renderer); + if (!rendererReadyOrEnded) { + renderer.maybeThrowError(); + } + allRenderersReadyOrEnded = allRenderersReadyOrEnded && rendererReadyOrEnded; + + if (bufferedPositionUs == TrackRenderer.UNKNOWN_TIME_US) { + // We've already encountered a track for which the buffered position is unknown. Hence the + // media buffer position unknown regardless of the buffered position of this track. + } else { + long rendererDurationUs = renderer.getDurationUs(); + long rendererBufferedPositionUs = renderer.getBufferedPositionUs(); + if (rendererBufferedPositionUs == TrackRenderer.UNKNOWN_TIME_US) { + bufferedPositionUs = TrackRenderer.UNKNOWN_TIME_US; + } else if (rendererBufferedPositionUs == TrackRenderer.END_OF_TRACK_US + || (rendererDurationUs != TrackRenderer.UNKNOWN_TIME_US + && rendererDurationUs != TrackRenderer.MATCH_LONGEST_US + && rendererBufferedPositionUs >= rendererDurationUs)) { + // This track is fully buffered. + } else { + bufferedPositionUs = Math.min(bufferedPositionUs, rendererBufferedPositionUs); + } + } + } + this.bufferedPositionUs = bufferedPositionUs; + + if (allRenderersEnded + && (durationUs == TrackRenderer.UNKNOWN_TIME_US || durationUs <= positionUs)) { + setState(ExoPlayer.STATE_ENDED); + stopRenderers(); + } else if (state == ExoPlayer.STATE_BUFFERING && allRenderersReadyOrEnded) { + setState(ExoPlayer.STATE_READY); + if (playWhenReady) { + startRenderers(); + } + } else if (state == ExoPlayer.STATE_READY && !allRenderersReadyOrEnded) { + rebuffering = playWhenReady; + setState(ExoPlayer.STATE_BUFFERING); + stopRenderers(); + } + + handler.removeMessages(MSG_DO_SOME_WORK); + if ((playWhenReady && state == ExoPlayer.STATE_READY) || state == ExoPlayer.STATE_BUFFERING) { + scheduleNextOperation(MSG_DO_SOME_WORK, operationStartTimeMs, RENDERING_INTERVAL_MS); + } else if (!enabledRenderers.isEmpty()) { + scheduleNextOperation(MSG_DO_SOME_WORK, operationStartTimeMs, IDLE_INTERVAL_MS); + } + + TraceUtil.endSection(); + } + + private void scheduleNextOperation(int operationType, long thisOperationStartTimeMs, + long intervalMs) { + long nextOperationStartTimeMs = thisOperationStartTimeMs + intervalMs; + long nextOperationDelayMs = nextOperationStartTimeMs - SystemClock.elapsedRealtime(); + if (nextOperationDelayMs <= 0) { + handler.sendEmptyMessage(operationType); + } else { + handler.sendEmptyMessageDelayed(operationType, nextOperationDelayMs); + } + } + + private void seekToInternal(long positionMs) throws ExoPlaybackException { + try { + if (positionMs == (positionUs / 1000)) { + // Seek is to the current position. Do nothing. + return; + } + + rebuffering = false; + positionUs = positionMs * 1000; + standaloneMediaClock.stop(); + standaloneMediaClock.setPositionUs(positionUs); + if (state == ExoPlayer.STATE_IDLE || state == ExoPlayer.STATE_PREPARING) { + return; + } + for (int i = 0; i < enabledRenderers.size(); i++) { + TrackRenderer renderer = enabledRenderers.get(i); + ensureStopped(renderer); + renderer.seekTo(positionUs); + } + setState(ExoPlayer.STATE_BUFFERING); + handler.sendEmptyMessage(MSG_DO_SOME_WORK); + } finally { + pendingSeekCount.decrementAndGet(); + } + } + + private void stopInternal() { + resetInternal(); + setState(ExoPlayer.STATE_IDLE); + } + + private void releaseInternal() { + resetInternal(); + setState(ExoPlayer.STATE_IDLE); + synchronized (this) { + released = true; + notifyAll(); + } + } + + private void resetInternal() { + handler.removeMessages(MSG_DO_SOME_WORK); + handler.removeMessages(MSG_INCREMENTAL_PREPARE); + rebuffering = false; + standaloneMediaClock.stop(); + if (renderers == null) { + return; + } + for (int i = 0; i < renderers.length; i++) { + TrackRenderer renderer = renderers[i]; + stopAndDisable(renderer); + release(renderer); + } + renderers = null; + rendererMediaClock = null; + rendererMediaClockSource = null; + enabledRenderers.clear(); + } + + private void stopAndDisable(TrackRenderer renderer) { + try { + ensureDisabled(renderer); + } catch (ExoPlaybackException e) { + // There's nothing we can do. + Log.e(TAG, "Stop failed.", e); + } catch (RuntimeException e) { + // Ditto. + Log.e(TAG, "Stop failed.", e); + } + } + + private void release(TrackRenderer renderer) { + try { + renderer.release(); + } catch (ExoPlaybackException e) { + // There's nothing we can do. + Log.e(TAG, "Release failed.", e); + } catch (RuntimeException e) { + // Ditto. + Log.e(TAG, "Release failed.", e); + } + } + + private void sendMessageInternal(int what, Object obj) + throws ExoPlaybackException { + try { + @SuppressWarnings("unchecked") + Pair targetAndMessage = (Pair) obj; + targetAndMessage.first.handleMessage(what, targetAndMessage.second); + if (state != ExoPlayer.STATE_IDLE && state != ExoPlayer.STATE_PREPARING) { + // The message may have caused something to change that now requires us to do work. + handler.sendEmptyMessage(MSG_DO_SOME_WORK); + } + } finally { + synchronized (this) { + customMessagesProcessed++; + notifyAll(); + } + } + } + + private void setRendererSelectedTrackInternal(int rendererIndex, int trackIndex) + throws ExoPlaybackException { + if (selectedTrackIndices[rendererIndex] == trackIndex) { + return; + } + + selectedTrackIndices[rendererIndex] = trackIndex; + if (state == ExoPlayer.STATE_IDLE || state == ExoPlayer.STATE_PREPARING) { + return; + } + + TrackRenderer renderer = renderers[rendererIndex]; + int rendererState = renderer.getState(); + if (rendererState == TrackRenderer.STATE_UNPREPARED + || rendererState == TrackRenderer.STATE_RELEASED + || renderer.getTrackCount() == 0) { + return; + } + + boolean isEnabled = rendererState == TrackRenderer.STATE_ENABLED + || rendererState == TrackRenderer.STATE_STARTED; + boolean shouldEnable = 0 <= trackIndex && trackIndex < trackFormats[rendererIndex].length; + + if (isEnabled) { + // The renderer is currently enabled. We need to disable it, so that we can either re-enable + // it with the newly selected track (if shouldEnable is true) or because we want to leave it + // disabled (if shouldEnable is false). + if (!shouldEnable && renderer == rendererMediaClockSource) { + // We've been using rendererMediaClockSource to advance the current position, but it's being + // disabled and won't be re-enabled. Sync standaloneMediaClock so that it can take over + // timing responsibilities. + standaloneMediaClock.setPositionUs(rendererMediaClock.getPositionUs()); + } + ensureDisabled(renderer); + enabledRenderers.remove(renderer); + } + + if (shouldEnable) { + // Re-enable the renderer with the newly selected track. + boolean playing = playWhenReady && state == ExoPlayer.STATE_READY; + // Consider as joining if the renderer was previously disabled, but not when switching tracks. + boolean joining = !isEnabled && playing; + enableRenderer(renderer, trackIndex, joining); + if (playing) { + renderer.start(); + } + handler.sendEmptyMessage(MSG_DO_SOME_WORK); + } + } + + private void ensureStopped(TrackRenderer renderer) throws ExoPlaybackException { + if (renderer.getState() == TrackRenderer.STATE_STARTED) { + renderer.stop(); + } + } + + private void ensureDisabled(TrackRenderer renderer) throws ExoPlaybackException { + ensureStopped(renderer); + if (renderer.getState() == TrackRenderer.STATE_ENABLED) { + renderer.disable(); + if (renderer == rendererMediaClockSource) { + rendererMediaClock = null; + rendererMediaClockSource = null; + } + } + } +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/ExoPlayerLibraryInfo.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/ExoPlayerLibraryInfo.java new file mode 100755 index 000000000..91845dad0 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/ExoPlayerLibraryInfo.java @@ -0,0 +1,51 @@ +/* + * 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.messenger.exoplayer; + +/** + * Information about the ExoPlayer library. + */ +public final class ExoPlayerLibraryInfo { + + /** + * The version of the library, expressed as a string. + */ + public static final String VERSION = "1.5.8"; + + /** + * The version of the library, expressed as an integer. + *

+ * Three digits are used for each component of {@link #VERSION}. For example "1.2.3" has the + * corresponding integer version 1002003 (001-002-003), and "123.45.6" has the corresponding + * integer version 123045006 (123-045-006). + */ + public static final int VERSION_INT = 1005008; + + /** + * Whether the library was compiled with {@link org.telegram.messenger.exoplayer.util.Assertions} + * checks enabled. + */ + public static final boolean ASSERTIONS_ENABLED = true; + + /** + * Whether the library was compiled with {@link org.telegram.messenger.exoplayer.util.TraceUtil} + * trace enabled. + */ + public static final boolean TRACE_ENABLED = true; + + private ExoPlayerLibraryInfo() {} + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/FrameworkSampleSource.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/FrameworkSampleSource.java new file mode 100755 index 000000000..4206db6d4 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/FrameworkSampleSource.java @@ -0,0 +1,349 @@ +/* + * 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.messenger.exoplayer; + +import org.telegram.messenger.exoplayer.SampleSource.SampleSourceReader; +import org.telegram.messenger.exoplayer.drm.DrmInitData; +import org.telegram.messenger.exoplayer.drm.DrmInitData.SchemeInitData; +import org.telegram.messenger.exoplayer.extractor.ExtractorSampleSource; +import org.telegram.messenger.exoplayer.extractor.mp4.PsshAtomUtil; +import org.telegram.messenger.exoplayer.util.Assertions; +import org.telegram.messenger.exoplayer.util.MimeTypes; +import org.telegram.messenger.exoplayer.util.Util; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.Context; +import android.media.MediaExtractor; +import android.net.Uri; + +import java.io.FileDescriptor; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Map; +import java.util.UUID; + +/** + * Extracts samples from a stream using Android's {@link MediaExtractor}. + *

+ * Warning - This class is marked as deprecated because there are known device specific issues + * associated with its use, including playbacks not starting, playbacks stuttering and other + * miscellaneous failures. For mp4, m4a, mp3, webm, mkv, mpeg-ts, ogg, wav and aac playbacks it is + * strongly recommended to use {@link ExtractorSampleSource} instead. Where this is not possible + * this class can still be used, but please be aware of the associated risks. Playing container + * formats for which an ExoPlayer extractor does not yet exist (e.g. avi) is a valid use case of + * this class. + *

+ * Over time we hope to enhance {@link ExtractorSampleSource} to support more formats, and hence + * make use of this class unnecessary. + */ +// TODO: This implementation needs to be fixed so that its methods are non-blocking (either +// through use of a background thread, or through changes to the framework's MediaExtractor API). +@Deprecated +@TargetApi(16) +public final class FrameworkSampleSource implements SampleSource, SampleSourceReader { + + private static final int ALLOWED_FLAGS_MASK = C.SAMPLE_FLAG_SYNC | C.SAMPLE_FLAG_ENCRYPTED; + + private static final int TRACK_STATE_DISABLED = 0; + private static final int TRACK_STATE_ENABLED = 1; + private static final int TRACK_STATE_FORMAT_SENT = 2; + + // Parameters for a Uri data source. + private final Context context; + private final Uri uri; + private final Map headers; + + // Parameters for a FileDescriptor data source. + private final FileDescriptor fileDescriptor; + private final long fileDescriptorOffset; + private final long fileDescriptorLength; + + private IOException preparationError; + private MediaExtractor extractor; + private MediaFormat[] trackFormats; + private boolean prepared; + private int remainingReleaseCount; + private int[] trackStates; + private boolean[] pendingDiscontinuities; + + private long lastSeekPositionUs; + private long pendingSeekPositionUs; + + /** + * Instantiates a new sample extractor reading from the specified {@code uri}. + * + * @param context Context for resolving {@code uri}. + * @param uri The content URI from which to extract data. + * @param headers Headers to send with requests for data. + */ + public FrameworkSampleSource(Context context, Uri uri, Map headers) { + Assertions.checkState(Util.SDK_INT >= 16); + this.context = Assertions.checkNotNull(context); + this.uri = Assertions.checkNotNull(uri); + this.headers = headers; + fileDescriptor = null; + fileDescriptorOffset = 0; + fileDescriptorLength = 0; + } + + /** + * Instantiates a new sample extractor reading from the specified seekable {@code fileDescriptor}. + * The caller is responsible for releasing the file descriptor. + * + * @param fileDescriptor File descriptor from which to read. + * @param fileDescriptorOffset The offset in bytes where the data to be extracted starts. + * @param fileDescriptorLength The length in bytes of the data to be extracted. + */ + public FrameworkSampleSource(FileDescriptor fileDescriptor, long fileDescriptorOffset, + long fileDescriptorLength) { + Assertions.checkState(Util.SDK_INT >= 16); + this.fileDescriptor = Assertions.checkNotNull(fileDescriptor); + this.fileDescriptorOffset = fileDescriptorOffset; + this.fileDescriptorLength = fileDescriptorLength; + context = null; + uri = null; + headers = null; + } + + @Override + public SampleSourceReader register() { + remainingReleaseCount++; + return this; + } + + @Override + public boolean prepare(long positionUs) { + if (!prepared) { + if (preparationError != null) { + return false; + } + + extractor = new MediaExtractor(); + try { + if (context != null) { + extractor.setDataSource(context, uri, headers); + } else { + extractor.setDataSource(fileDescriptor, fileDescriptorOffset, fileDescriptorLength); + } + } catch (IOException e) { + preparationError = e; + return false; + } + + trackStates = new int[extractor.getTrackCount()]; + pendingDiscontinuities = new boolean[trackStates.length]; + trackFormats = new MediaFormat[trackStates.length]; + for (int i = 0; i < trackStates.length; i++) { + trackFormats[i] = createMediaFormat(extractor.getTrackFormat(i)); + } + prepared = true; + } + return true; + } + + @Override + public int getTrackCount() { + Assertions.checkState(prepared); + return trackStates.length; + } + + @Override + public MediaFormat getFormat(int track) { + Assertions.checkState(prepared); + return trackFormats[track]; + } + + @Override + public void enable(int track, long positionUs) { + Assertions.checkState(prepared); + Assertions.checkState(trackStates[track] == TRACK_STATE_DISABLED); + trackStates[track] = TRACK_STATE_ENABLED; + extractor.selectTrack(track); + seekToUsInternal(positionUs, positionUs != 0); + } + + @Override + public boolean continueBuffering(int track, long positionUs) { + // MediaExtractor takes care of buffering and blocks until it has samples, so we can always + // return true here. Although note that the blocking behavior is itself as bug, as per the + // TODO further up this file. This method will need to return something else as part of fixing + // the TODO. + return true; + } + + @Override + public long readDiscontinuity(int track) { + if (pendingDiscontinuities[track]) { + pendingDiscontinuities[track] = false; + return lastSeekPositionUs; + } + return NO_DISCONTINUITY; + } + + @Override + public int readData(int track, long positionUs, MediaFormatHolder formatHolder, + SampleHolder sampleHolder) { + Assertions.checkState(prepared); + Assertions.checkState(trackStates[track] != TRACK_STATE_DISABLED); + if (pendingDiscontinuities[track]) { + return NOTHING_READ; + } + if (trackStates[track] != TRACK_STATE_FORMAT_SENT) { + formatHolder.format = trackFormats[track]; + formatHolder.drmInitData = Util.SDK_INT >= 18 ? getDrmInitDataV18() : null; + trackStates[track] = TRACK_STATE_FORMAT_SENT; + return FORMAT_READ; + } + int extractorTrackIndex = extractor.getSampleTrackIndex(); + if (extractorTrackIndex == track) { + if (sampleHolder.data != null) { + int offset = sampleHolder.data.position(); + sampleHolder.size = extractor.readSampleData(sampleHolder.data, offset); + sampleHolder.data.position(offset + sampleHolder.size); + } else { + sampleHolder.size = 0; + } + sampleHolder.timeUs = extractor.getSampleTime(); + sampleHolder.flags = extractor.getSampleFlags() & ALLOWED_FLAGS_MASK; + if (sampleHolder.isEncrypted()) { + sampleHolder.cryptoInfo.setFromExtractorV16(extractor); + } + pendingSeekPositionUs = C.UNKNOWN_TIME_US; + extractor.advance(); + return SAMPLE_READ; + } else { + return extractorTrackIndex < 0 ? END_OF_STREAM : NOTHING_READ; + } + } + + @Override + public void disable(int track) { + Assertions.checkState(prepared); + Assertions.checkState(trackStates[track] != TRACK_STATE_DISABLED); + extractor.unselectTrack(track); + pendingDiscontinuities[track] = false; + trackStates[track] = TRACK_STATE_DISABLED; + } + + @Override + public void maybeThrowError() throws IOException { + if (preparationError != null) { + throw preparationError; + } + } + + @Override + public void seekToUs(long positionUs) { + Assertions.checkState(prepared); + seekToUsInternal(positionUs, false); + } + + @Override + public long getBufferedPositionUs() { + Assertions.checkState(prepared); + long bufferedDurationUs = extractor.getCachedDuration(); + if (bufferedDurationUs == -1) { + return TrackRenderer.UNKNOWN_TIME_US; + } else { + long sampleTime = extractor.getSampleTime(); + return sampleTime == -1 ? TrackRenderer.END_OF_TRACK_US : sampleTime + bufferedDurationUs; + } + } + + @Override + public void release() { + Assertions.checkState(remainingReleaseCount > 0); + if (--remainingReleaseCount == 0 && extractor != null) { + extractor.release(); + extractor = null; + } + } + + @TargetApi(18) + private DrmInitData getDrmInitDataV18() { + // MediaExtractor only supports psshInfo for MP4, so it's ok to hard code the mimeType here. + Map psshInfo = extractor.getPsshInfo(); + if (psshInfo == null || psshInfo.isEmpty()) { + return null; + } + DrmInitData.Mapped drmInitData = new DrmInitData.Mapped(); + for (UUID uuid : psshInfo.keySet()) { + byte[] psshAtom = PsshAtomUtil.buildPsshAtom(uuid, psshInfo.get(uuid)); + drmInitData.put(uuid, new SchemeInitData(MimeTypes.VIDEO_MP4, psshAtom)); + } + return drmInitData; + } + + private void seekToUsInternal(long positionUs, boolean force) { + // Unless forced, avoid duplicate calls to the underlying extractor's seek method in the case + // that there have been no interleaving calls to readSample. + if (force || pendingSeekPositionUs != positionUs) { + lastSeekPositionUs = positionUs; + pendingSeekPositionUs = positionUs; + extractor.seekTo(positionUs, MediaExtractor.SEEK_TO_PREVIOUS_SYNC); + for (int i = 0; i < trackStates.length; ++i) { + if (trackStates[i] != TRACK_STATE_DISABLED) { + pendingDiscontinuities[i] = true; + } + } + } + } + + @SuppressLint("InlinedApi") + private static MediaFormat createMediaFormat(android.media.MediaFormat format) { + String mimeType = format.getString(android.media.MediaFormat.KEY_MIME); + String language = getOptionalStringV16(format, android.media.MediaFormat.KEY_LANGUAGE); + int maxInputSize = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_MAX_INPUT_SIZE); + int width = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_WIDTH); + int height = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_HEIGHT); + int rotationDegrees = getOptionalIntegerV16(format, "rotation-degrees"); + int channelCount = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_CHANNEL_COUNT); + int sampleRate = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_SAMPLE_RATE); + int encoderDelay = getOptionalIntegerV16(format, "encoder-delay"); + int encoderPadding = getOptionalIntegerV16(format, "encoder-padding"); + ArrayList initializationData = new ArrayList<>(); + for (int i = 0; format.containsKey("csd-" + i); i++) { + ByteBuffer buffer = format.getByteBuffer("csd-" + i); + byte[] data = new byte[buffer.limit()]; + buffer.get(data); + initializationData.add(data); + buffer.flip(); + } + long durationUs = format.containsKey(android.media.MediaFormat.KEY_DURATION) + ? format.getLong(android.media.MediaFormat.KEY_DURATION) : C.UNKNOWN_TIME_US; + int pcmEncoding = MimeTypes.AUDIO_RAW.equals(mimeType) ? C.ENCODING_PCM_16BIT + : MediaFormat.NO_VALUE; + MediaFormat mediaFormat = new MediaFormat(null, mimeType, MediaFormat.NO_VALUE, maxInputSize, + durationUs, width, height, rotationDegrees, MediaFormat.NO_VALUE, channelCount, sampleRate, + language, MediaFormat.OFFSET_SAMPLE_RELATIVE, initializationData, false, + MediaFormat.NO_VALUE, MediaFormat.NO_VALUE, pcmEncoding, encoderDelay, encoderPadding); + mediaFormat.setFrameworkFormatV16(format); + return mediaFormat; + } + + @TargetApi(16) + private static final String getOptionalStringV16(android.media.MediaFormat format, String key) { + return format.containsKey(key) ? format.getString(key) : null; + } + + @TargetApi(16) + private static final int getOptionalIntegerV16(android.media.MediaFormat format, String key) { + return format.containsKey(key) ? format.getInteger(key) : MediaFormat.NO_VALUE; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/LoadControl.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/LoadControl.java new file mode 100755 index 000000000..292aa7539 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/LoadControl.java @@ -0,0 +1,75 @@ +/* + * 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.messenger.exoplayer; + +import org.telegram.messenger.exoplayer.upstream.Allocator; + +/** + * Coordinates multiple loaders of time series data. + */ +public interface LoadControl { + + /** + * Registers a loader. + * + * @param loader The loader being registered. + * @param bufferSizeContribution For instances whose {@link Allocator} maintains a pool of memory + * for the purpose of satisfying allocation requests, this is a hint indicating the loader's + * desired contribution to the size of the pool, in bytes. + */ + void register(Object loader, int bufferSizeContribution); + + /** + * Unregisters a loader. + * + * @param loader The loader being unregistered. + */ + void unregister(Object loader); + + /** + * Gets the {@link Allocator} that loaders should use to obtain memory allocations into which + * data can be loaded. + * + * @return The {@link Allocator} to use. + */ + Allocator getAllocator(); + + /** + * Hints to the control that it should consider trimming any unused memory being held in order + * to satisfy allocation requests. + *

+ * This method is typically invoked by a recently unregistered loader, once it has released all + * of its allocations back to the {@link Allocator}. + */ + void trimAllocator(); + + /** + * Invoked by a loader to update the control with its current state. + *

+ * This method must be called by a registered loader whenever its state changes. This is true + * even if the registered loader does not itself wish to start its next load (since the state of + * the loader will still affect whether other registered loaders are allowed to proceed). + * + * @param loader The loader invoking the update. + * @param playbackPositionUs The loader's playback position. + * @param nextLoadPositionUs The loader's next load position. -1 if finished, failed, or if the + * next load position is not yet known. + * @param loading Whether the loader is currently loading data. + * @return True if the loader is allowed to start its next load. False otherwise. + */ + boolean update(Object loader, long playbackPositionUs, long nextLoadPositionUs, boolean loading); + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/IntEvaluator.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/MediaClock.java old mode 100644 new mode 100755 similarity index 61% rename from TMessagesProj/src/main/java/org/telegram/messenger/Animation/IntEvaluator.java rename to TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/MediaClock.java index 2b43e0773..127650a2b --- a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/IntEvaluator.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/MediaClock.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2010 The Android Open Source Project + * 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. @@ -13,12 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package org.telegram.messenger.exoplayer; -package org.telegram.messenger.Animation; +/** + * Tracks the progression of media time. + */ +public interface MediaClock { -public class IntEvaluator implements TypeEvaluator { - public Integer evaluate(float fraction, Integer startValue, Integer endValue) { - int startInt = startValue; - return (int)(startInt + fraction * (endValue - startInt)); - } -} \ No newline at end of file + /** + * @return The current media position in microseconds. + */ + long getPositionUs(); + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/MediaCodecAudioTrackRenderer.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/MediaCodecAudioTrackRenderer.java new file mode 100755 index 000000000..84e678094 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/MediaCodecAudioTrackRenderer.java @@ -0,0 +1,486 @@ +/* + * 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.messenger.exoplayer; + +import org.telegram.messenger.exoplayer.MediaCodecUtil.DecoderQueryException; +import org.telegram.messenger.exoplayer.audio.AudioCapabilities; +import org.telegram.messenger.exoplayer.audio.AudioTrack; +import org.telegram.messenger.exoplayer.drm.DrmSessionManager; +import org.telegram.messenger.exoplayer.util.MimeTypes; + +import android.annotation.TargetApi; +import android.media.AudioManager; +import android.media.MediaCodec; +import android.media.PlaybackParams; +import android.media.audiofx.Virtualizer; +import android.os.Handler; +import android.os.SystemClock; + +import java.nio.ByteBuffer; + +/** + * Decodes and renders audio using {@link MediaCodec} and {@link android.media.AudioTrack}. + */ +@TargetApi(16) +public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer implements MediaClock { + + /** + * Interface definition for a callback to be notified of {@link MediaCodecAudioTrackRenderer} + * events. + */ + public interface EventListener extends MediaCodecTrackRenderer.EventListener { + + /** + * Invoked when an {@link AudioTrack} fails to initialize. + * + * @param e The corresponding exception. + */ + void onAudioTrackInitializationError(AudioTrack.InitializationException e); + + /** + * Invoked when an {@link AudioTrack} write fails. + * + * @param e The corresponding exception. + */ + void onAudioTrackWriteError(AudioTrack.WriteException e); + + /** + * Invoked when an {@link AudioTrack} underrun occurs. + * + * @param bufferSize The size of the {@link AudioTrack}'s buffer, in bytes. + * @param bufferSizeMs The size of the {@link AudioTrack}'s buffer, in milliseconds, if it is + * configured for PCM output. -1 if it is configured for passthrough output, as the buffered + * media can have a variable bitrate so the duration may be unknown. + * @param elapsedSinceLastFeedMs The time since the {@link AudioTrack} was last fed data. + */ + void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs); + + } + + /** + * The type of a message that can be passed to an instance of this class via + * {@link ExoPlayer#sendMessage} or {@link ExoPlayer#blockingSendMessage}. The message object + * should be a {@link Float} with 0 being silence and 1 being unity gain. + */ + public static final int MSG_SET_VOLUME = 1; + + /** + * The type of a message that can be passed to an instance of this class via + * {@link ExoPlayer#sendMessage} or {@link ExoPlayer#blockingSendMessage}. The message object + * should be a {@link android.media.PlaybackParams}, which will be used to configure the + * underlying {@link android.media.AudioTrack}. The message object should not be modified by the + * caller after it has been passed + */ + public static final int MSG_SET_PLAYBACK_PARAMS = 2; + + private final EventListener eventListener; + private final AudioTrack audioTrack; + + private boolean passthroughEnabled; + private android.media.MediaFormat passthroughMediaFormat; + private int pcmEncoding; + private int audioSessionId; + private long currentPositionUs; + private boolean allowPositionDiscontinuity; + + private boolean audioTrackHasData; + private long lastFeedElapsedRealtimeMs; + + /** + * @param source The upstream source from which the renderer obtains samples. + * @param mediaCodecSelector A decoder selector. + */ + public MediaCodecAudioTrackRenderer(SampleSource source, MediaCodecSelector mediaCodecSelector) { + this(source, mediaCodecSelector, null, true); + } + + /** + * @param source The upstream source from which the renderer obtains samples. + * @param mediaCodecSelector A decoder selector. + * @param drmSessionManager For use with encrypted content. May be null if support for encrypted + * content is not required. + * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions. + * For example a media file may start with a short clear region so as to allow playback to + * begin in parallel with key acquisition. This parameter specifies whether the renderer is + * permitted to play clear regions of encrypted media files before {@code drmSessionManager} + * has obtained the keys necessary to decrypt encrypted regions of the media. + */ + public MediaCodecAudioTrackRenderer(SampleSource source, MediaCodecSelector mediaCodecSelector, + DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys) { + this(source, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, null, null); + } + + /** + * @param source The upstream source from which the renderer obtains samples. + * @param mediaCodecSelector A decoder selector. + * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be + * null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + */ + public MediaCodecAudioTrackRenderer(SampleSource source, MediaCodecSelector mediaCodecSelector, + Handler eventHandler, EventListener eventListener) { + this(source, mediaCodecSelector, null, true, eventHandler, eventListener); + } + + /** + * @param source The upstream source from which the renderer obtains samples. + * @param mediaCodecSelector A decoder selector. + * @param drmSessionManager For use with encrypted content. May be null if support for encrypted + * content is not required. + * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions. + * For example a media file may start with a short clear region so as to allow playback to + * begin in parallel with key acquisition. This parameter specifies whether the renderer is + * permitted to play clear regions of encrypted media files before {@code drmSessionManager} + * has obtained the keys necessary to decrypt encrypted regions of the media. + * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be + * null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + */ + public MediaCodecAudioTrackRenderer(SampleSource source, MediaCodecSelector mediaCodecSelector, + DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, + Handler eventHandler, EventListener eventListener) { + this(source, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, eventHandler, + eventListener, null, AudioManager.STREAM_MUSIC); + } + + /** + * @param source The upstream source from which the renderer obtains samples. + * @param mediaCodecSelector A decoder selector. + * @param drmSessionManager For use with encrypted content. May be null if support for encrypted + * content is not required. + * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions. + * For example a media file may start with a short clear region so as to allow playback to + * begin in parallel with key acquisition. This parameter specifies whether the renderer is + * permitted to play clear regions of encrypted media files before {@code drmSessionManager} + * has obtained the keys necessary to decrypt encrypted regions of the media. + * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be + * null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + * @param audioCapabilities The audio capabilities for playback on this device. May be null if the + * default capabilities (no encoded audio passthrough support) should be assumed. + * @param streamType The type of audio stream for the {@link AudioTrack}. + */ + public MediaCodecAudioTrackRenderer(SampleSource source, MediaCodecSelector mediaCodecSelector, + DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, + Handler eventHandler, EventListener eventListener, AudioCapabilities audioCapabilities, + int streamType) { + this (new SampleSource[] {source}, mediaCodecSelector, drmSessionManager, + playClearSamplesWithoutKeys, eventHandler, eventListener, audioCapabilities, streamType); + } + + /** + * @param sources The upstream sources from which the renderer obtains samples. + * @param mediaCodecSelector A decoder selector. + * @param drmSessionManager For use with encrypted content. May be null if support for encrypted + * content is not required. + * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions. + * For example a media file may start with a short clear region so as to allow playback to + * begin in parallel with key acquisition. This parameter specifies whether the renderer is + * permitted to play clear regions of encrypted media files before {@code drmSessionManager} + * has obtained the keys necessary to decrypt encrypted regions of the media. + * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be + * null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + * @param audioCapabilities The audio capabilities for playback on this device. May be null if the + * default capabilities (no encoded audio passthrough support) should be assumed. + * @param streamType The type of audio stream for the {@link AudioTrack}. + */ + public MediaCodecAudioTrackRenderer(SampleSource[] sources, MediaCodecSelector mediaCodecSelector, + DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, + Handler eventHandler, EventListener eventListener, AudioCapabilities audioCapabilities, + int streamType) { + super(sources, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, eventHandler, + eventListener); + this.eventListener = eventListener; + this.audioSessionId = AudioTrack.SESSION_ID_NOT_SET; + this.audioTrack = new AudioTrack(audioCapabilities, streamType); + } + + @Override + protected boolean handlesTrack(MediaCodecSelector mediaCodecSelector, MediaFormat mediaFormat) + throws DecoderQueryException { + String mimeType = mediaFormat.mimeType; + return MimeTypes.isAudio(mimeType) && (MimeTypes.AUDIO_UNKNOWN.equals(mimeType) + || (allowPassthrough(mimeType) && mediaCodecSelector.getPassthroughDecoderInfo() != null) + || mediaCodecSelector.getDecoderInfo(mimeType, false) != null); + } + + @Override + protected DecoderInfo getDecoderInfo(MediaCodecSelector mediaCodecSelector, String mimeType, + boolean requiresSecureDecoder) throws DecoderQueryException { + if (allowPassthrough(mimeType)) { + DecoderInfo passthroughDecoderInfo = mediaCodecSelector.getPassthroughDecoderInfo(); + if (passthroughDecoderInfo != null) { + passthroughEnabled = true; + return passthroughDecoderInfo; + } + } + passthroughEnabled = false; + return super.getDecoderInfo(mediaCodecSelector, mimeType, requiresSecureDecoder); + } + + /** + * Returns whether encoded audio passthrough should be used for playing back the input format. + * This implementation returns true if the {@link AudioTrack}'s audio capabilities indicate that + * passthrough is supported. + * + * @param mimeType The type of input media. + * @return True if passthrough playback should be used. False otherwise. + */ + protected boolean allowPassthrough(String mimeType) { + return audioTrack.isPassthroughSupported(mimeType); + } + + @Override + protected void configureCodec(MediaCodec codec, boolean codecIsAdaptive, + android.media.MediaFormat format, android.media.MediaCrypto crypto) { + String mimeType = format.getString(android.media.MediaFormat.KEY_MIME); + if (passthroughEnabled) { + // Override the MIME type used to configure the codec if we are using a passthrough decoder. + format.setString(android.media.MediaFormat.KEY_MIME, MimeTypes.AUDIO_RAW); + codec.configure(format, null, crypto, 0); + format.setString(android.media.MediaFormat.KEY_MIME, mimeType); + passthroughMediaFormat = format; + } else { + codec.configure(format, null, crypto, 0); + passthroughMediaFormat = null; + } + } + + @Override + protected MediaClock getMediaClock() { + return this; + } + + @Override + protected void onInputFormatChanged(MediaFormatHolder holder) throws ExoPlaybackException { + super.onInputFormatChanged(holder); + // If the input format is anything other than PCM then we assume that the audio decoder will + // output 16-bit PCM. + pcmEncoding = MimeTypes.AUDIO_RAW.equals(holder.format.mimeType) ? holder.format.pcmEncoding + : C.ENCODING_PCM_16BIT; + } + + @Override + protected void onOutputFormatChanged(MediaCodec codec, android.media.MediaFormat outputFormat) { + boolean passthrough = passthroughMediaFormat != null; + String mimeType = passthrough + ? passthroughMediaFormat.getString(android.media.MediaFormat.KEY_MIME) + : MimeTypes.AUDIO_RAW; + android.media.MediaFormat format = passthrough ? passthroughMediaFormat : outputFormat; + int channelCount = format.getInteger(android.media.MediaFormat.KEY_CHANNEL_COUNT); + int sampleRate = format.getInteger(android.media.MediaFormat.KEY_SAMPLE_RATE); + audioTrack.configure(mimeType, channelCount, sampleRate, pcmEncoding); + } + + /** + * Invoked when the audio session id becomes known. Once the id is known it will not change + * (and hence this method will not be invoked again) unless the renderer is disabled and then + * subsequently re-enabled. + *

+ * The default implementation is a no-op. One reason for overriding this method would be to + * instantiate and enable a {@link Virtualizer} in order to spatialize the audio channels. For + * this use case, any {@link Virtualizer} instances should be released in {@link #onDisabled()} + * (if not before). + * + * @param audioSessionId The audio session id. + */ + protected void onAudioSessionId(int audioSessionId) { + // Do nothing. + } + + @Override + protected void onStarted() { + super.onStarted(); + audioTrack.play(); + } + + @Override + protected void onStopped() { + audioTrack.pause(); + super.onStopped(); + } + + @Override + protected boolean isEnded() { + return super.isEnded() && !audioTrack.hasPendingData(); + } + + @Override + protected boolean isReady() { + return audioTrack.hasPendingData() || super.isReady(); + } + + @Override + public long getPositionUs() { + long newCurrentPositionUs = audioTrack.getCurrentPositionUs(isEnded()); + if (newCurrentPositionUs != AudioTrack.CURRENT_POSITION_NOT_SET) { + currentPositionUs = allowPositionDiscontinuity ? newCurrentPositionUs + : Math.max(currentPositionUs, newCurrentPositionUs); + allowPositionDiscontinuity = false; + } + return currentPositionUs; + } + + @Override + protected void onDisabled() throws ExoPlaybackException { + audioSessionId = AudioTrack.SESSION_ID_NOT_SET; + try { + audioTrack.release(); + } finally { + super.onDisabled(); + } + } + + @Override + protected void onDiscontinuity(long positionUs) throws ExoPlaybackException { + super.onDiscontinuity(positionUs); + audioTrack.reset(); + currentPositionUs = positionUs; + allowPositionDiscontinuity = true; + } + + @Override + protected boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs, MediaCodec codec, + ByteBuffer buffer, MediaCodec.BufferInfo bufferInfo, int bufferIndex, boolean shouldSkip) + throws ExoPlaybackException { + if (passthroughEnabled && (bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) { + // Discard output buffers from the passthrough (raw) decoder containing codec specific data. + codec.releaseOutputBuffer(bufferIndex, false); + return true; + } + + if (shouldSkip) { + codec.releaseOutputBuffer(bufferIndex, false); + codecCounters.skippedOutputBufferCount++; + audioTrack.handleDiscontinuity(); + return true; + } + + if (!audioTrack.isInitialized()) { + // Initialize the AudioTrack now. + try { + if (audioSessionId != AudioTrack.SESSION_ID_NOT_SET) { + audioTrack.initialize(audioSessionId); + } else { + audioSessionId = audioTrack.initialize(); + onAudioSessionId(audioSessionId); + } + audioTrackHasData = false; + } catch (AudioTrack.InitializationException e) { + notifyAudioTrackInitializationError(e); + throw new ExoPlaybackException(e); + } + if (getState() == TrackRenderer.STATE_STARTED) { + audioTrack.play(); + } + } else { + // Check for AudioTrack underrun. + boolean audioTrackHadData = audioTrackHasData; + audioTrackHasData = audioTrack.hasPendingData(); + if (audioTrackHadData && !audioTrackHasData && getState() == TrackRenderer.STATE_STARTED) { + long elapsedSinceLastFeedMs = SystemClock.elapsedRealtime() - lastFeedElapsedRealtimeMs; + long bufferSizeUs = audioTrack.getBufferSizeUs(); + long bufferSizeMs = bufferSizeUs == C.UNKNOWN_TIME_US ? -1 : bufferSizeUs / 1000; + notifyAudioTrackUnderrun(audioTrack.getBufferSize(), bufferSizeMs, elapsedSinceLastFeedMs); + } + } + + int handleBufferResult; + try { + handleBufferResult = audioTrack.handleBuffer( + buffer, bufferInfo.offset, bufferInfo.size, bufferInfo.presentationTimeUs); + lastFeedElapsedRealtimeMs = SystemClock.elapsedRealtime(); + } catch (AudioTrack.WriteException e) { + notifyAudioTrackWriteError(e); + throw new ExoPlaybackException(e); + } + + // If we are out of sync, allow currentPositionUs to jump backwards. + if ((handleBufferResult & AudioTrack.RESULT_POSITION_DISCONTINUITY) != 0) { + handleAudioTrackDiscontinuity(); + allowPositionDiscontinuity = true; + } + + // Release the buffer if it was consumed. + if ((handleBufferResult & AudioTrack.RESULT_BUFFER_CONSUMED) != 0) { + codec.releaseOutputBuffer(bufferIndex, false); + codecCounters.renderedOutputBufferCount++; + return true; + } + + return false; + } + + @Override + protected void onOutputStreamEnded() { + audioTrack.handleEndOfStream(); + } + + protected void handleAudioTrackDiscontinuity() { + // Do nothing + } + + @Override + public void handleMessage(int messageType, Object message) throws ExoPlaybackException { + switch (messageType) { + case MSG_SET_VOLUME: + audioTrack.setVolume((Float) message); + break; + case MSG_SET_PLAYBACK_PARAMS: + audioTrack.setPlaybackParams((PlaybackParams) message); + break; + default: + super.handleMessage(messageType, message); + break; + } + } + + private void notifyAudioTrackInitializationError(final AudioTrack.InitializationException e) { + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onAudioTrackInitializationError(e); + } + }); + } + } + + private void notifyAudioTrackWriteError(final AudioTrack.WriteException e) { + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onAudioTrackWriteError(e); + } + }); + } + } + + private void notifyAudioTrackUnderrun(final int bufferSize, final long bufferSizeMs, + final long elapsedSinceLastFeedMs) { + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onAudioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs); + } + }); + } + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/MediaCodecSelector.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/MediaCodecSelector.java new file mode 100755 index 000000000..32b710664 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/MediaCodecSelector.java @@ -0,0 +1,64 @@ +/* + * 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.messenger.exoplayer; + +import org.telegram.messenger.exoplayer.MediaCodecUtil.DecoderQueryException; + +import android.media.MediaCodec; + +/** + * Selector of {@link MediaCodec} instances. + */ +public interface MediaCodecSelector { + + /** + * Default implementation of {@link MediaCodecSelector}. + */ + MediaCodecSelector DEFAULT = new MediaCodecSelector() { + + @Override + public DecoderInfo getDecoderInfo(String mimeType, boolean requiresSecureDecoder) + throws DecoderQueryException { + return MediaCodecUtil.getDecoderInfo(mimeType, requiresSecureDecoder); + } + + @Override + public DecoderInfo getPassthroughDecoderInfo() throws DecoderQueryException { + return MediaCodecUtil.getPassthroughDecoderInfo(); + } + + }; + + /** + * Selects a decoder to instantiate for a given mime type. + * + * @param mimeType The mime type for which a decoder is required. + * @param requiresSecureDecoder Whether a secure decoder is required. + * @return A {@link DecoderInfo} describing the decoder, or null if no suitable decoder exists. + * @throws DecoderQueryException Thrown if there was an error querying decoders. + */ + DecoderInfo getDecoderInfo(String mimeType, boolean requiresSecureDecoder) + throws DecoderQueryException; + + /** + * Selects a decoder to instantiate for audio passthrough. + * + * @return A {@link DecoderInfo} describing the decoder, or null if no suitable decoder exists. + * @throws DecoderQueryException Thrown if there was an error querying decoders. + */ + DecoderInfo getPassthroughDecoderInfo() throws DecoderQueryException; + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/MediaCodecTrackRenderer.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/MediaCodecTrackRenderer.java new file mode 100755 index 000000000..05d995d6e --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/MediaCodecTrackRenderer.java @@ -0,0 +1,1114 @@ +/* + * 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.messenger.exoplayer; + +import org.telegram.messenger.exoplayer.MediaCodecUtil.DecoderQueryException; +import org.telegram.messenger.exoplayer.drm.DrmInitData; +import org.telegram.messenger.exoplayer.drm.DrmSessionManager; +import org.telegram.messenger.exoplayer.util.Assertions; +import org.telegram.messenger.exoplayer.util.NalUnitUtil; +import org.telegram.messenger.exoplayer.util.TraceUtil; +import org.telegram.messenger.exoplayer.util.Util; + +import android.annotation.TargetApi; +import android.media.MediaCodec; +import android.media.MediaCodec.CodecException; +import android.media.MediaCodec.CryptoException; +import android.media.MediaCrypto; +import android.os.Handler; +import android.os.SystemClock; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +/** + * An abstract {@link TrackRenderer} that uses {@link MediaCodec} to decode samples for rendering. + */ +@TargetApi(16) +public abstract class MediaCodecTrackRenderer extends SampleSourceTrackRenderer { + + /** + * Interface definition for a callback to be notified of {@link MediaCodecTrackRenderer} events. + */ + public interface EventListener { + + /** + * Invoked when a decoder fails to initialize. + * + * @param e The corresponding exception. + */ + void onDecoderInitializationError(DecoderInitializationException e); + + /** + * Invoked when a decoder operation raises a {@link CryptoException}. + * + * @param e The corresponding exception. + */ + void onCryptoError(CryptoException e); + + /** + * Invoked when a decoder is successfully created. + * + * @param decoderName The decoder that was configured and created. + * @param elapsedRealtimeMs {@code elapsedRealtime} timestamp of when the initialization + * finished. + * @param initializationDurationMs Amount of time taken to initialize the decoder. + */ + void onDecoderInitialized(String decoderName, long elapsedRealtimeMs, + long initializationDurationMs); + + } + + /** + * Thrown when a failure occurs instantiating a decoder. + */ + public static class DecoderInitializationException extends Exception { + + private static final int CUSTOM_ERROR_CODE_BASE = -50000; + private static final int NO_SUITABLE_DECODER_ERROR = CUSTOM_ERROR_CODE_BASE + 1; + private static final int DECODER_QUERY_ERROR = CUSTOM_ERROR_CODE_BASE + 2; + + /** + * The mime type for which a decoder was being initialized. + */ + public final String mimeType; + + /** + * Whether it was required that the decoder support a secure output path. + */ + public final boolean secureDecoderRequired; + + /** + * The name of the decoder that failed to initialize. Null if no suitable decoder was found. + */ + public final String decoderName; + + /** + * An optional developer-readable diagnostic information string. May be null. + */ + public final String diagnosticInfo; + + public DecoderInitializationException(MediaFormat mediaFormat, Throwable cause, + boolean secureDecoderRequired, int errorCode) { + super("Decoder init failed: [" + errorCode + "], " + mediaFormat, cause); + this.mimeType = mediaFormat.mimeType; + this.secureDecoderRequired = secureDecoderRequired; + this.decoderName = null; + this.diagnosticInfo = buildCustomDiagnosticInfo(errorCode); + } + + public DecoderInitializationException(MediaFormat mediaFormat, Throwable cause, + boolean secureDecoderRequired, String decoderName) { + super("Decoder init failed: " + decoderName + ", " + mediaFormat, cause); + this.mimeType = mediaFormat.mimeType; + this.secureDecoderRequired = secureDecoderRequired; + this.decoderName = decoderName; + this.diagnosticInfo = Util.SDK_INT >= 21 ? getDiagnosticInfoV21(cause) : null; + } + + @TargetApi(21) + private static String getDiagnosticInfoV21(Throwable cause) { + if (cause instanceof CodecException) { + return ((CodecException) cause).getDiagnosticInfo(); + } + return null; + } + + private static String buildCustomDiagnosticInfo(int errorCode) { + String sign = errorCode < 0 ? "neg_" : ""; + return "org.telegram.messenger.exoplayer.MediaCodecTrackRenderer_" + sign + Math.abs(errorCode); + } + + } + + /** + * Value returned by {@link #getSourceState()} when the source is not ready. + */ + protected static final int SOURCE_STATE_NOT_READY = 0; + /** + * Value returned by {@link #getSourceState()} when the source is ready and we're able to read + * from it. + */ + protected static final int SOURCE_STATE_READY = 1; + /** + * Value returned by {@link #getSourceState()} when the source is ready but we might not be able + * to read from it. We transition to this state when an attempt to read a sample fails despite the + * source reporting that samples are available. This can occur when the next sample to be provided + * by the source is for another renderer. + */ + protected static final int SOURCE_STATE_READY_READ_MAY_FAIL = 2; + + /** + * If the {@link MediaCodec} is hotswapped (i.e. replaced during playback), this is the period of + * time during which {@link #isReady()} will report true regardless of whether the new codec has + * output frames that are ready to be rendered. + *

+ * This allows codec hotswapping to be performed seamlessly, without interrupting the playback of + * other renderers, provided the new codec is able to decode some frames within this time period. + */ + private static final long MAX_CODEC_HOTSWAP_TIME_MS = 1000; + + /** + * There is no pending adaptive reconfiguration work. + */ + private static final int RECONFIGURATION_STATE_NONE = 0; + /** + * Codec configuration data needs to be written into the next buffer. + */ + private static final int RECONFIGURATION_STATE_WRITE_PENDING = 1; + /** + * Codec configuration data has been written into the next buffer, but that buffer still needs to + * be returned to the codec. + */ + private static final int RECONFIGURATION_STATE_QUEUE_PENDING = 2; + + /** + * The codec does not need to be re-initialized. + */ + private static final int REINITIALIZATION_STATE_NONE = 0; + /** + * The input format has changed in a way that requires the codec to be re-initialized, but we + * haven't yet signaled an end of stream to the existing codec. We need to do so in order to + * ensure that it outputs any remaining buffers before we release it. + */ + private static final int REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM = 1; + /** + * The input format has changed in a way that requires the codec to be re-initialized, and we've + * signaled an end of stream to the existing codec. We're waiting for the codec to output an end + * of stream signal to indicate that it has output any remaining buffers before we release it. + */ + private static final int REINITIALIZATION_STATE_WAIT_END_OF_STREAM = 2; + + public final CodecCounters codecCounters; + + private final MediaCodecSelector mediaCodecSelector; + private final DrmSessionManager drmSessionManager; + private final boolean playClearSamplesWithoutKeys; + private final SampleHolder sampleHolder; + private final MediaFormatHolder formatHolder; + private final List decodeOnlyPresentationTimestamps; + private final MediaCodec.BufferInfo outputBufferInfo; + private final EventListener eventListener; + private final boolean deviceNeedsAutoFrcWorkaround; + protected final Handler eventHandler; + + private MediaFormat format; + private DrmInitData drmInitData; + private MediaCodec codec; + private boolean codecIsAdaptive; + private boolean codecNeedsDiscardToSpsWorkaround; + private boolean codecNeedsFlushWorkaround; + private boolean codecNeedsEosPropagationWorkaround; + private boolean codecNeedsEosFlushWorkaround; + private boolean codecNeedsMonoChannelCountWorkaround; + private ByteBuffer[] inputBuffers; + private ByteBuffer[] outputBuffers; + private long codecHotswapTimeMs; + private int inputIndex; + private int outputIndex; + private boolean openedDrmSession; + private boolean codecReconfigured; + private int codecReconfigurationState; + private int codecReinitializationState; + private boolean codecReceivedBuffers; + private boolean codecReceivedEos; + + private int sourceState; + private boolean inputStreamEnded; + private boolean outputStreamEnded; + private boolean waitingForKeys; + private boolean waitingForFirstSyncFrame; + + /** + * @param source The upstream source from which the renderer obtains samples. + * @param mediaCodecSelector A decoder selector. + * @param drmSessionManager For use with encrypted media. May be null if support for encrypted + * media is not required. + * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions. + * For example a media file may start with a short clear region so as to allow playback to + * begin in parallel with key acquisition. This parameter specifies whether the renderer is + * permitted to play clear regions of encrypted media files before {@code drmSessionManager} + * has obtained the keys necessary to decrypt encrypted regions of the media. + * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be + * null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + */ + public MediaCodecTrackRenderer(SampleSource source, MediaCodecSelector mediaCodecSelector, + DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, + Handler eventHandler, EventListener eventListener) { + this (new SampleSource[] {source}, mediaCodecSelector, drmSessionManager, + playClearSamplesWithoutKeys, eventHandler, eventListener); + } + + /** + * @param sources The upstream sources from which the renderer obtains samples. + * @param mediaCodecSelector A decoder selector. + * @param drmSessionManager For use with encrypted media. May be null if support for encrypted + * media is not required. + * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions. + * For example a media file may start with a short clear region so as to allow playback to + * begin in parallel with key acquisition. This parameter specifies whether the renderer is + * permitted to play clear regions of encrypted media files before {@code drmSessionManager} + * has obtained the keys necessary to decrypt encrypted regions of the media. + * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be + * null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + */ + public MediaCodecTrackRenderer(SampleSource[] sources, MediaCodecSelector mediaCodecSelector, + DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, + Handler eventHandler, EventListener eventListener) { + super(sources); + Assertions.checkState(Util.SDK_INT >= 16); + this.mediaCodecSelector = Assertions.checkNotNull(mediaCodecSelector); + this.drmSessionManager = drmSessionManager; + this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys; + this.eventHandler = eventHandler; + this.eventListener = eventListener; + deviceNeedsAutoFrcWorkaround = deviceNeedsAutoFrcWorkaround(); + codecCounters = new CodecCounters(); + sampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_DISABLED); + formatHolder = new MediaFormatHolder(); + decodeOnlyPresentationTimestamps = new ArrayList<>(); + outputBufferInfo = new MediaCodec.BufferInfo(); + codecReconfigurationState = RECONFIGURATION_STATE_NONE; + codecReinitializationState = REINITIALIZATION_STATE_NONE; + } + + @Override + protected final boolean handlesTrack(MediaFormat mediaFormat) throws DecoderQueryException { + return handlesTrack(mediaCodecSelector, mediaFormat); + } + + /** + * Returns whether this renderer is capable of handling the provided track. + * + * @param mediaCodecSelector The decoder selector. + * @param mediaFormat The format of the track. + * @return True if the renderer can handle the track, false otherwise. + * @throws DecoderQueryException Thrown if there was an error querying decoders. + */ + protected abstract boolean handlesTrack(MediaCodecSelector mediaCodecSelector, + MediaFormat mediaFormat) throws DecoderQueryException; + + /** + * Returns a {@link DecoderInfo} for a given format. + * + * @param mediaCodecSelector The decoder selector. + * @param mimeType The mime type for which a decoder is required. + * @param requiresSecureDecoder Whether a secure decoder is required. + * @return A {@link DecoderInfo} describing the decoder to instantiate, or null if no suitable + * decoder exists. + * @throws DecoderQueryException Thrown if there was an error querying decoders. + */ + protected DecoderInfo getDecoderInfo(MediaCodecSelector mediaCodecSelector, String mimeType, + boolean requiresSecureDecoder) throws DecoderQueryException { + return mediaCodecSelector.getDecoderInfo(mimeType, requiresSecureDecoder); + } + + /** + * Configures a newly created {@link MediaCodec}. + * + * @param codec The {@link MediaCodec} to configure. + * @param codecIsAdaptive Whether the codec is adaptive. + * @param format The format for which the codec is being configured. + * @param crypto For drm protected playbacks, a {@link MediaCrypto} to use for decryption. + */ + protected abstract void configureCodec(MediaCodec codec, boolean codecIsAdaptive, + android.media.MediaFormat format, MediaCrypto crypto); + + @SuppressWarnings("deprecation") + protected final void maybeInitCodec() throws ExoPlaybackException { + if (!shouldInitCodec()) { + return; + } + + String mimeType = format.mimeType; + MediaCrypto mediaCrypto = null; + boolean requiresSecureDecoder = false; + if (drmInitData != null) { + if (drmSessionManager == null) { + throw new ExoPlaybackException("Media requires a DrmSessionManager"); + } + if (!openedDrmSession) { + drmSessionManager.open(drmInitData); + openedDrmSession = true; + } + int drmSessionState = drmSessionManager.getState(); + if (drmSessionState == DrmSessionManager.STATE_ERROR) { + throw new ExoPlaybackException(drmSessionManager.getError()); + } else if (drmSessionState == DrmSessionManager.STATE_OPENED + || drmSessionState == DrmSessionManager.STATE_OPENED_WITH_KEYS) { + mediaCrypto = drmSessionManager.getMediaCrypto(); + requiresSecureDecoder = drmSessionManager.requiresSecureDecoderComponent(mimeType); + } else { + // The drm session isn't open yet. + return; + } + } + + DecoderInfo decoderInfo = null; + try { + decoderInfo = getDecoderInfo(mediaCodecSelector, mimeType, requiresSecureDecoder); + } catch (DecoderQueryException e) { + notifyAndThrowDecoderInitError(new DecoderInitializationException(format, e, + requiresSecureDecoder, DecoderInitializationException.DECODER_QUERY_ERROR)); + } + + if (decoderInfo == null) { + notifyAndThrowDecoderInitError(new DecoderInitializationException(format, null, + requiresSecureDecoder, DecoderInitializationException.NO_SUITABLE_DECODER_ERROR)); + } + + String codecName = decoderInfo.name; + codecIsAdaptive = decoderInfo.adaptive; + codecNeedsDiscardToSpsWorkaround = codecNeedsDiscardToSpsWorkaround(codecName, format); + codecNeedsFlushWorkaround = codecNeedsFlushWorkaround(codecName); + codecNeedsEosPropagationWorkaround = codecNeedsEosPropagationWorkaround(codecName); + codecNeedsEosFlushWorkaround = codecNeedsEosFlushWorkaround(codecName); + codecNeedsMonoChannelCountWorkaround = codecNeedsMonoChannelCountWorkaround(codecName, format); + try { + long codecInitializingTimestamp = SystemClock.elapsedRealtime(); + TraceUtil.beginSection("createByCodecName(" + codecName + ")"); + codec = MediaCodec.createByCodecName(codecName); + TraceUtil.endSection(); + TraceUtil.beginSection("configureCodec"); + configureCodec(codec, decoderInfo.adaptive, getFrameworkMediaFormat(format), mediaCrypto); + TraceUtil.endSection(); + TraceUtil.beginSection("codec.start()"); + codec.start(); + TraceUtil.endSection(); + long codecInitializedTimestamp = SystemClock.elapsedRealtime(); + notifyDecoderInitialized(codecName, codecInitializedTimestamp, + codecInitializedTimestamp - codecInitializingTimestamp); + inputBuffers = codec.getInputBuffers(); + outputBuffers = codec.getOutputBuffers(); + } catch (Exception e) { + notifyAndThrowDecoderInitError(new DecoderInitializationException(format, e, + requiresSecureDecoder, codecName)); + } + codecHotswapTimeMs = getState() == TrackRenderer.STATE_STARTED ? + SystemClock.elapsedRealtime() : -1; + inputIndex = -1; + outputIndex = -1; + waitingForFirstSyncFrame = true; + codecCounters.codecInitCount++; + } + + private void notifyAndThrowDecoderInitError(DecoderInitializationException e) + throws ExoPlaybackException { + notifyDecoderInitializationError(e); + throw new ExoPlaybackException(e); + } + + protected boolean shouldInitCodec() { + return codec == null && format != null; + } + + protected final boolean codecInitialized() { + return codec != null; + } + + protected final boolean haveFormat() { + return format != null; + } + + @Override + protected void onDisabled() throws ExoPlaybackException { + format = null; + drmInitData = null; + try { + releaseCodec(); + } finally { + try { + if (openedDrmSession) { + drmSessionManager.close(); + openedDrmSession = false; + } + } finally { + super.onDisabled(); + } + } + } + + protected void releaseCodec() { + if (codec != null) { + codecHotswapTimeMs = -1; + inputIndex = -1; + outputIndex = -1; + waitingForKeys = false; + decodeOnlyPresentationTimestamps.clear(); + inputBuffers = null; + outputBuffers = null; + codecReconfigured = false; + codecReceivedBuffers = false; + codecIsAdaptive = false; + codecNeedsDiscardToSpsWorkaround = false; + codecNeedsFlushWorkaround = false; + codecNeedsEosPropagationWorkaround = false; + codecNeedsEosFlushWorkaround = false; + codecNeedsMonoChannelCountWorkaround = false; + codecReceivedEos = false; + codecReconfigurationState = RECONFIGURATION_STATE_NONE; + codecReinitializationState = REINITIALIZATION_STATE_NONE; + codecCounters.codecReleaseCount++; + try { + codec.stop(); + } finally { + try { + codec.release(); + } finally { + codec = null; + } + } + } + } + + @Override + protected void onDiscontinuity(long positionUs) throws ExoPlaybackException { + sourceState = SOURCE_STATE_NOT_READY; + inputStreamEnded = false; + outputStreamEnded = false; + if (codec != null) { + flushCodec(); + } + } + + @Override + protected void onStarted() { + // Do nothing. Overridden to remove throws clause. + } + + @Override + protected void onStopped() { + // Do nothing. Overridden to remove throws clause. + } + + @Override + protected void doSomeWork(long positionUs, long elapsedRealtimeUs, boolean sourceIsReady) + throws ExoPlaybackException { + sourceState = sourceIsReady + ? (sourceState == SOURCE_STATE_NOT_READY ? SOURCE_STATE_READY : sourceState) + : SOURCE_STATE_NOT_READY; + if (format == null) { + readFormat(positionUs); + } + maybeInitCodec(); + if (codec != null) { + TraceUtil.beginSection("drainAndFeed"); + while (drainOutputBuffer(positionUs, elapsedRealtimeUs)) {} + if (feedInputBuffer(positionUs, true)) { + while (feedInputBuffer(positionUs, false)) {} + } + TraceUtil.endSection(); + } + codecCounters.ensureUpdated(); + } + + private void readFormat(long positionUs) throws ExoPlaybackException { + int result = readSource(positionUs, formatHolder, null); + if (result == SampleSource.FORMAT_READ) { + onInputFormatChanged(formatHolder); + } + } + + protected void flushCodec() throws ExoPlaybackException { + codecHotswapTimeMs = -1; + inputIndex = -1; + outputIndex = -1; + waitingForFirstSyncFrame = true; + waitingForKeys = false; + decodeOnlyPresentationTimestamps.clear(); + if (codecNeedsFlushWorkaround || (codecNeedsEosFlushWorkaround && codecReceivedEos)) { + // Workaround framework bugs. See [Internal: b/8347958, b/8578467, b/8543366, b/23361053]. + releaseCodec(); + maybeInitCodec(); + } else if (codecReinitializationState != REINITIALIZATION_STATE_NONE) { + // We're already waiting to release and re-initialize the codec. Since we're now flushing, + // there's no need to wait any longer. + releaseCodec(); + maybeInitCodec(); + } else { + // We can flush and re-use the existing decoder. + codec.flush(); + codecReceivedBuffers = false; + } + if (codecReconfigured && format != null) { + // Any reconfiguration data that we send shortly before the flush may be discarded. We + // avoid this issue by sending reconfiguration data following every flush. + codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING; + } + } + + /** + * @param positionUs The current media time in microseconds, measured at the start of the + * current iteration of the rendering loop. + * @param firstFeed True if this is the first call to this method from the current invocation of + * {@link #doSomeWork(long, long)}. False otherwise. + * @return True if it may be possible to feed more input data. False otherwise. + * @throws ExoPlaybackException If an error occurs feeding the input buffer. + */ + private boolean feedInputBuffer(long positionUs, boolean firstFeed) throws ExoPlaybackException { + if (inputStreamEnded + || codecReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM) { + // The input stream has ended, or we need to re-initialize the codec but are still waiting + // for the existing codec to output any final output buffers. + return false; + } + + if (inputIndex < 0) { + inputIndex = codec.dequeueInputBuffer(0); + if (inputIndex < 0) { + return false; + } + sampleHolder.data = inputBuffers[inputIndex]; + sampleHolder.clearData(); + } + + if (codecReinitializationState == REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM) { + // We need to re-initialize the codec. Send an end of stream signal to the existing codec so + // that it outputs any remaining buffers before we release it. + if (codecNeedsEosPropagationWorkaround) { + // Do nothing. + } else { + codecReceivedEos = true; + codec.queueInputBuffer(inputIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM); + inputIndex = -1; + } + codecReinitializationState = REINITIALIZATION_STATE_WAIT_END_OF_STREAM; + return false; + } + + int result; + if (waitingForKeys) { + // We've already read an encrypted sample into sampleHolder, and are waiting for keys. + result = SampleSource.SAMPLE_READ; + } else { + // For adaptive reconfiguration OMX decoders expect all reconfiguration data to be supplied + // at the start of the buffer that also contains the first frame in the new format. + if (codecReconfigurationState == RECONFIGURATION_STATE_WRITE_PENDING) { + for (int i = 0; i < format.initializationData.size(); i++) { + byte[] data = format.initializationData.get(i); + sampleHolder.data.put(data); + } + codecReconfigurationState = RECONFIGURATION_STATE_QUEUE_PENDING; + } + result = readSource(positionUs, formatHolder, sampleHolder); + if (firstFeed && sourceState == SOURCE_STATE_READY && result == SampleSource.NOTHING_READ) { + sourceState = SOURCE_STATE_READY_READ_MAY_FAIL; + } + } + + if (result == SampleSource.NOTHING_READ) { + return false; + } + if (result == SampleSource.FORMAT_READ) { + if (codecReconfigurationState == RECONFIGURATION_STATE_QUEUE_PENDING) { + // We received two formats in a row. Clear the current buffer of any reconfiguration data + // associated with the first format. + sampleHolder.clearData(); + codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING; + } + onInputFormatChanged(formatHolder); + return true; + } + if (result == SampleSource.END_OF_STREAM) { + if (codecReconfigurationState == RECONFIGURATION_STATE_QUEUE_PENDING) { + // We received a new format immediately before the end of the stream. We need to clear + // the corresponding reconfiguration data from the current buffer, but re-write it into + // a subsequent buffer if there are any (e.g. if the user seeks backwards). + sampleHolder.clearData(); + codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING; + } + inputStreamEnded = true; + if (!codecReceivedBuffers) { + processEndOfStream(); + return false; + } + try { + if (codecNeedsEosPropagationWorkaround) { + // Do nothing. + } else { + codecReceivedEos = true; + codec.queueInputBuffer(inputIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM); + inputIndex = -1; + } + } catch (CryptoException e) { + notifyCryptoError(e); + throw new ExoPlaybackException(e); + } + return false; + } + if (waitingForFirstSyncFrame) { + // TODO: Find out if it's possible to supply samples prior to the first sync + // frame for HE-AAC. + if (!sampleHolder.isSyncFrame()) { + sampleHolder.clearData(); + if (codecReconfigurationState == RECONFIGURATION_STATE_QUEUE_PENDING) { + // The buffer we just cleared contained reconfiguration data. We need to re-write this + // data into a subsequent buffer (if there is one). + codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING; + } + return true; + } + waitingForFirstSyncFrame = false; + } + boolean sampleEncrypted = sampleHolder.isEncrypted(); + waitingForKeys = shouldWaitForKeys(sampleEncrypted); + if (waitingForKeys) { + return false; + } + if (codecNeedsDiscardToSpsWorkaround && !sampleEncrypted) { + NalUnitUtil.discardToSps(sampleHolder.data); + if (sampleHolder.data.position() == 0) { + return true; + } + codecNeedsDiscardToSpsWorkaround = false; + } + try { + int bufferSize = sampleHolder.data.position(); + int adaptiveReconfigurationBytes = bufferSize - sampleHolder.size; + long presentationTimeUs = sampleHolder.timeUs; + if (sampleHolder.isDecodeOnly()) { + decodeOnlyPresentationTimestamps.add(presentationTimeUs); + } + + onQueuedInputBuffer(presentationTimeUs, sampleHolder.data, bufferSize, sampleEncrypted); + + if (sampleEncrypted) { + MediaCodec.CryptoInfo cryptoInfo = getFrameworkCryptoInfo(sampleHolder, + adaptiveReconfigurationBytes); + codec.queueSecureInputBuffer(inputIndex, 0, cryptoInfo, presentationTimeUs, 0); + } else { + codec.queueInputBuffer(inputIndex, 0, bufferSize, presentationTimeUs, 0); + } + inputIndex = -1; + codecReceivedBuffers = true; + codecReconfigurationState = RECONFIGURATION_STATE_NONE; + codecCounters.inputBufferCount++; + } catch (CryptoException e) { + notifyCryptoError(e); + throw new ExoPlaybackException(e); + } + return true; + } + + private static MediaCodec.CryptoInfo getFrameworkCryptoInfo(SampleHolder sampleHolder, + int adaptiveReconfigurationBytes) { + MediaCodec.CryptoInfo cryptoInfo = sampleHolder.cryptoInfo.getFrameworkCryptoInfoV16(); + if (adaptiveReconfigurationBytes == 0) { + return cryptoInfo; + } + // There must be at least one sub-sample, although numBytesOfClearData is permitted to be + // null if it contains no clear data. Instantiate it if needed, and add the reconfiguration + // bytes to the clear byte count of the first sub-sample. + if (cryptoInfo.numBytesOfClearData == null) { + cryptoInfo.numBytesOfClearData = new int[1]; + } + cryptoInfo.numBytesOfClearData[0] += adaptiveReconfigurationBytes; + return cryptoInfo; + } + + private android.media.MediaFormat getFrameworkMediaFormat(MediaFormat format) { + android.media.MediaFormat mediaFormat = format.getFrameworkMediaFormatV16(); + if (deviceNeedsAutoFrcWorkaround) { + mediaFormat.setInteger("auto-frc", 0); + } + return mediaFormat; + } + + private boolean shouldWaitForKeys(boolean sampleEncrypted) throws ExoPlaybackException { + if (!openedDrmSession) { + return false; + } + int drmManagerState = drmSessionManager.getState(); + if (drmManagerState == DrmSessionManager.STATE_ERROR) { + throw new ExoPlaybackException(drmSessionManager.getError()); + } + if (drmManagerState != DrmSessionManager.STATE_OPENED_WITH_KEYS && + (sampleEncrypted || !playClearSamplesWithoutKeys)) { + return true; + } + return false; + } + + /** + * Invoked when a new format is read from the upstream {@link SampleSource}. + * + * @param formatHolder Holds the new format. + * @throws ExoPlaybackException If an error occurs reinitializing the {@link MediaCodec}. + */ + protected void onInputFormatChanged(MediaFormatHolder formatHolder) throws ExoPlaybackException { + MediaFormat oldFormat = format; + format = formatHolder.format; + drmInitData = formatHolder.drmInitData; + if (codec != null && canReconfigureCodec(codec, codecIsAdaptive, oldFormat, format)) { + codecReconfigured = true; + codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING; + } else { + if (codecReceivedBuffers) { + // Signal end of stream and wait for any final output buffers before re-initialization. + codecReinitializationState = REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM; + } else { + // There aren't any final output buffers, so perform re-initialization immediately. + releaseCodec(); + maybeInitCodec(); + } + } + } + + /** + * Invoked when the output format of the {@link MediaCodec} changes. + *

+ * The default implementation is a no-op. + * + * @param codec The {@link MediaCodec} instance. + * @param outputFormat The new output format. + * @throws ExoPlaybackException If an error occurs on output format change. + */ + protected void onOutputFormatChanged(MediaCodec codec, android.media.MediaFormat outputFormat) + throws ExoPlaybackException { + // Do nothing. + } + + /** + * Invoked when the output stream ends, meaning that the last output buffer has been processed + * and the {@link MediaCodec#BUFFER_FLAG_END_OF_STREAM} flag has been propagated through the + * decoder. + *

+ * The default implementation is a no-op. + */ + protected void onOutputStreamEnded() { + // Do nothing. + } + + /** + * Invoked immediately before an input buffer is queued into the codec. + *

+ * The default implementation is a no-op. + * + * @param presentationTimeUs The timestamp associated with the input buffer. + * @param buffer The buffer to be queued. + * @param bufferSize the size of the sample data stored in the buffer. + * @param sampleEncrypted Whether the sample data is encrypted. + */ + protected void onQueuedInputBuffer( + long presentationTimeUs, ByteBuffer buffer, int bufferSize, boolean sampleEncrypted) { + // Do nothing. + } + + /** + * Invoked when an output buffer is successfully processed. + *

+ * The default implementation is a no-op. + * + * @param presentationTimeUs The timestamp associated with the output buffer. + */ + protected void onProcessedOutputBuffer(long presentationTimeUs) { + // Do nothing. + } + + /** + * Determines whether the existing {@link MediaCodec} should be reconfigured for a new format by + * sending codec specific initialization data at the start of the next input buffer. If true is + * returned then the {@link MediaCodec} instance will be reconfigured in this way. If false is + * returned then the instance will be released, and a new instance will be created for the new + * format. + *

+ * The default implementation returns false. + * + * @param codec The existing {@link MediaCodec} instance. + * @param codecIsAdaptive Whether the codec is adaptive. + * @param oldFormat The format for which the existing instance is configured. + * @param newFormat The new format. + * @return True if the existing instance can be reconfigured. False otherwise. + */ + protected boolean canReconfigureCodec(MediaCodec codec, boolean codecIsAdaptive, + MediaFormat oldFormat, MediaFormat newFormat) { + return false; + } + + @Override + protected boolean isEnded() { + return outputStreamEnded; + } + + @Override + protected boolean isReady() { + return format != null && !waitingForKeys + && (sourceState != SOURCE_STATE_NOT_READY || outputIndex >= 0 || isWithinHotswapPeriod()); + } + + /** + * Gets the source state. + * + * @return One of {@link #SOURCE_STATE_NOT_READY}, {@link #SOURCE_STATE_READY} and + * {@link #SOURCE_STATE_READY_READ_MAY_FAIL}. + */ + protected final int getSourceState() { + return sourceState; + } + + private boolean isWithinHotswapPeriod() { + return SystemClock.elapsedRealtime() < codecHotswapTimeMs + MAX_CODEC_HOTSWAP_TIME_MS; + } + + /** + * Returns the maximum time to block whilst waiting for a decoded output buffer. + * + * @return The maximum time to block, in microseconds. + */ + protected long getDequeueOutputBufferTimeoutUs() { + return 0; + } + + /** + * @return True if it may be possible to drain more output data. False otherwise. + * @throws ExoPlaybackException If an error occurs draining the output buffer. + */ + @SuppressWarnings("deprecation") + private boolean drainOutputBuffer(long positionUs, long elapsedRealtimeUs) + throws ExoPlaybackException { + if (outputStreamEnded) { + return false; + } + + if (outputIndex < 0) { + outputIndex = codec.dequeueOutputBuffer(outputBufferInfo, getDequeueOutputBufferTimeoutUs()); + } + + if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { + processOutputFormat(); + return true; + } else if (outputIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { + outputBuffers = codec.getOutputBuffers(); + codecCounters.outputBuffersChangedCount++; + return true; + } else if (outputIndex < 0) { + if (codecNeedsEosPropagationWorkaround && (inputStreamEnded + || codecReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM)) { + processEndOfStream(); + return true; + } + return false; + } + + if ((outputBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { + processEndOfStream(); + return false; + } + + int decodeOnlyIndex = getDecodeOnlyIndex(outputBufferInfo.presentationTimeUs); + if (processOutputBuffer(positionUs, elapsedRealtimeUs, codec, outputBuffers[outputIndex], + outputBufferInfo, outputIndex, decodeOnlyIndex != -1)) { + onProcessedOutputBuffer(outputBufferInfo.presentationTimeUs); + if (decodeOnlyIndex != -1) { + decodeOnlyPresentationTimestamps.remove(decodeOnlyIndex); + } + outputIndex = -1; + return true; + } + + return false; + } + + /** + * Processes a new output format. + * + * @throws ExoPlaybackException If an error occurs processing the output format. + */ + private void processOutputFormat() throws ExoPlaybackException { + android.media.MediaFormat format = codec.getOutputFormat(); + if (codecNeedsMonoChannelCountWorkaround) { + format.setInteger(android.media.MediaFormat.KEY_CHANNEL_COUNT, 1); + } + onOutputFormatChanged(codec, format); + codecCounters.outputFormatChangedCount++; + } + + /** + * Processes the provided output buffer. + * + * @return True if the output buffer was processed (e.g. rendered or discarded) and hence is no + * longer required. False otherwise. + * @throws ExoPlaybackException If an error occurs processing the output buffer. + */ + protected abstract boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs, + MediaCodec codec, ByteBuffer buffer, MediaCodec.BufferInfo bufferInfo, int bufferIndex, + boolean shouldSkip) throws ExoPlaybackException; + + /** + * Processes an end of stream signal. + * + * @throws ExoPlaybackException If an error occurs processing the signal. + */ + private void processEndOfStream() throws ExoPlaybackException { + if (codecReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM) { + // We're waiting to re-initialize the codec, and have now processed all final buffers. + releaseCodec(); + maybeInitCodec(); + } else { + outputStreamEnded = true; + onOutputStreamEnded(); + } + } + + private void notifyDecoderInitializationError(final DecoderInitializationException e) { + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onDecoderInitializationError(e); + } + }); + } + } + + private void notifyCryptoError(final CryptoException e) { + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onCryptoError(e); + } + }); + } + } + + private void notifyDecoderInitialized(final String decoderName, + final long initializedTimestamp, final long initializationDuration) { + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onDecoderInitialized(decoderName, initializedTimestamp, + initializationDuration); + } + }); + } + } + + private int getDecodeOnlyIndex(long presentationTimeUs) { + final int size = decodeOnlyPresentationTimestamps.size(); + for (int i = 0; i < size; i++) { + if (decodeOnlyPresentationTimestamps.get(i).longValue() == presentationTimeUs) { + return i; + } + } + return -1; + } + + /** + * Returns whether the decoder is known to fail when flushed. + *

+ * If true is returned, the renderer will work around the issue by releasing the decoder and + * instantiating a new one rather than flushing the current instance. + * + * @param name The name of the decoder. + * @return True if the decoder is known to fail when flushed. + */ + private static boolean codecNeedsFlushWorkaround(String name) { + return Util.SDK_INT < 18 + || (Util.SDK_INT == 18 + && ("OMX.SEC.avc.dec".equals(name) || "OMX.SEC.avc.dec.secure".equals(name))) + || (Util.SDK_INT == 19 && Util.MODEL.startsWith("SM-G800") + && ("OMX.Exynos.avc.dec".equals(name) || "OMX.Exynos.avc.dec.secure".equals(name))); + } + + /** + * Returns whether the decoder is an H.264/AVC decoder known to fail if NAL units are queued + * before the codec specific data. + *

+ * If true is returned, the renderer will work around the issue by discarding data up to the SPS. + * + * @param name The name of the decoder. + * @param format The format used to configure the decoder. + * @return True if the decoder is known to fail if NAL units are queued before CSD. + */ + private static boolean codecNeedsDiscardToSpsWorkaround(String name, MediaFormat format) { + return Util.SDK_INT < 21 && format.initializationData.isEmpty() + && "OMX.MTK.VIDEO.DECODER.AVC".equals(name); + } + + /** + * Returns whether the decoder is known to handle the propagation of the + * {@link MediaCodec#BUFFER_FLAG_END_OF_STREAM} flag incorrectly on the host device. + *

+ * If true is returned, the renderer will work around the issue by approximating end of stream + * behavior without relying on the flag being propagated through to an output buffer by the + * underlying decoder. + * + * @param name The name of the decoder. + * @return True if the decoder is known to handle {@link MediaCodec#BUFFER_FLAG_END_OF_STREAM} + * propagation incorrectly on the host device. False otherwise. + */ + private static boolean codecNeedsEosPropagationWorkaround(String name) { + return Util.SDK_INT <= 17 && ("OMX.rk.video_decoder.avc".equals(name) + || "OMX.allwinner.video.decoder.avc".equals(name)); + } + + /** + * Returns whether the decoder is known to behave incorrectly if flushed after receiving an input + * buffer with {@link MediaCodec#BUFFER_FLAG_END_OF_STREAM} set. + *

+ * If true is returned, the renderer will work around the issue by instantiating a new decoder + * when this case occurs. + * + * @param name The name of the decoder. + * @return True if the decoder is known to behave incorrectly if flushed after receiving an input + * buffer with {@link MediaCodec#BUFFER_FLAG_END_OF_STREAM} set. False otherwise. + */ + private static boolean codecNeedsEosFlushWorkaround(String name) { + return Util.SDK_INT <= 23 && "OMX.google.vorbis.decoder".equals(name); + } + + /** + * Returns whether the decoder is known to set the number of audio channels in the output format + * to 2 for the given input format, whilst only actually outputting a single channel. + *

+ * If true is returned then we explicitly override the number of channels in the output format, + * setting it to 1. + * + * @param name The decoder name. + * @param format The input format. + * @return True if the device is known to set the number of audio channels in the output format + * to 2 for the given input format, whilst only actually outputting a single channel. False + * otherwise. + */ + private static boolean codecNeedsMonoChannelCountWorkaround(String name, MediaFormat format) { + return Util.SDK_INT <= 18 && format.channelCount == 1 + && "OMX.MTK.AUDIO.DECODER.MP3".equals(name); + } + + /** + * Returns whether the device is known to enable frame-rate conversion logic that negatively + * impacts ExoPlayer. + *

+ * If true is returned then we explicitly disable the feature. + * + * @return True if the device is known to enable frame-rate conversion logic that negatively + * impacts ExoPlayer. False otherwise. + */ + private static boolean deviceNeedsAutoFrcWorkaround() { + // nVidia Shield prior to M tries to adjust the playback rate to better map the frame-rate of + // content to the refresh rate of the display. For example playback of 23.976fps content is + // adjusted to play at 1.001x speed when the output display is 60Hz. Unfortunately the + // implementation causes ExoPlayer's reported playback position to drift out of sync. Captions + // also lose sync [Internal: b/26453592]. + return Util.SDK_INT <= 22 && "foster".equals(Util.DEVICE) && "NVIDIA".equals(Util.MANUFACTURER); + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/MediaCodecUtil.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/MediaCodecUtil.java new file mode 100755 index 000000000..e7de3a090 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/MediaCodecUtil.java @@ -0,0 +1,501 @@ +/* + * 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.messenger.exoplayer; + +import org.telegram.messenger.exoplayer.util.Assertions; +import org.telegram.messenger.exoplayer.util.MimeTypes; +import org.telegram.messenger.exoplayer.util.Util; + +import android.annotation.TargetApi; +import android.media.MediaCodecInfo; +import android.media.MediaCodecInfo.CodecCapabilities; +import android.media.MediaCodecInfo.CodecProfileLevel; +import android.media.MediaCodecList; +import android.text.TextUtils; +import android.util.Log; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * A utility class for querying the available codecs. + */ +@TargetApi(16) +public final class MediaCodecUtil { + + /** + * Thrown when an error occurs querying the device for its underlying media capabilities. + *

+ * Such failures are not expected in normal operation and are normally temporary (e.g. if the + * mediaserver process has crashed and is yet to restart). + */ + public static class DecoderQueryException extends IOException { + + private DecoderQueryException(Throwable cause) { + super("Failed to query underlying media codecs", cause); + } + + } + + private static final String TAG = "MediaCodecUtil"; + private static final DecoderInfo PASSTHROUGH_DECODER_INFO = + new DecoderInfo("OMX.google.raw.decoder", null); + + private static final Map> decoderInfosCache = new HashMap<>(); + + // Lazily initialized. + private static int maxH264DecodableFrameSize = -1; + + private MediaCodecUtil() {} + + /** + * Optional call to warm the codec cache for a given mime type. + *

+ * Calling this method may speed up subsequent calls to {@link #getDecoderInfo(String, boolean)}. + * + * @param mimeType The mime type. + * @param secure Whether the decoder is required to support secure decryption. Always pass false + * unless secure decryption really is required. + */ + public static void warmCodec(String mimeType, boolean secure) { + try { + getDecoderInfos(mimeType, secure); + } catch (DecoderQueryException e) { + // Codec warming is best effort, so we can swallow the exception. + Log.e(TAG, "Codec warming failed", e); + } + } + + /** + * Gets information about a decoder suitable for audio passthrough. + ** + * @return A {@link DecoderInfo} describing the decoder, or null if no suitable decoder exists. + */ + public static DecoderInfo getPassthroughDecoderInfo() { + // TODO: Return null if the raw decoder doesn't exist. + return PASSTHROUGH_DECODER_INFO; + } + + /** + * Get information about the preferred decoder for a given mime type. + * + * @param mimeType The mime type. + * @param secure Whether the decoder is required to support secure decryption. Always pass false + * unless secure decryption really is required. + * @return A {@link DecoderInfo} describing the decoder, or null if no suitable decoder exists. + */ + public static DecoderInfo getDecoderInfo(String mimeType, boolean secure) + throws DecoderQueryException { + List decoderInfos = getDecoderInfos(mimeType, secure); + return decoderInfos.isEmpty() ? null : decoderInfos.get(0); + } + + /** + * Returns all @{link DecoderInfo}s for a given mime type, in the order given by + * {@link MediaCodecList}. + * + * @param mimeType The mime type. + * @param secure Whether the decoders are required to support secure decryption. Always pass false + * unless secure decryption really is required. + * @return A list of all @{link DecoderInfo}s for the given mime type. May be empty if no suitable + * decoders exist. + */ + public static synchronized List getDecoderInfos(String mimeType, boolean secure) + throws DecoderQueryException { + CodecKey key = new CodecKey(mimeType, secure); + List decoderInfos = decoderInfosCache.get(key); + if (decoderInfos != null) { + return decoderInfos; + } + MediaCodecListCompat mediaCodecList = Util.SDK_INT >= 21 + ? new MediaCodecListCompatV21(secure) : new MediaCodecListCompatV16(); + decoderInfos = getDecoderInfosInternal(key, mediaCodecList); + if (secure && decoderInfos.isEmpty() && 21 <= Util.SDK_INT && Util.SDK_INT <= 23) { + // Some devices don't list secure decoders on API level 21 [Internal: b/18678462]. Try the + // legacy path. We also try this path on API levels 22 and 23 as a defensive measure. + mediaCodecList = new MediaCodecListCompatV16(); + decoderInfos = getDecoderInfosInternal(key, mediaCodecList); + if (!decoderInfos.isEmpty()) { + Log.w(TAG, "MediaCodecList API didn't list secure decoder for: " + mimeType + + ". Assuming: " + decoderInfos.get(0).name); + } + } + decoderInfos = Collections.unmodifiableList(decoderInfos); + decoderInfosCache.put(key, decoderInfos); + return decoderInfos; + } + + private static List getDecoderInfosInternal( + CodecKey key, MediaCodecListCompat mediaCodecList) throws DecoderQueryException { + try { + List decoderInfos = new ArrayList<>(); + String mimeType = key.mimeType; + int numberOfCodecs = mediaCodecList.getCodecCount(); + boolean secureDecodersExplicit = mediaCodecList.secureDecodersExplicit(); + // Note: MediaCodecList is sorted by the framework such that the best decoders come first. + for (int i = 0; i < numberOfCodecs; i++) { + MediaCodecInfo codecInfo = mediaCodecList.getCodecInfoAt(i); + String codecName = codecInfo.getName(); + if (isCodecUsableDecoder(codecInfo, codecName, secureDecodersExplicit)) { + for (String supportedType : codecInfo.getSupportedTypes()) { + if (supportedType.equalsIgnoreCase(mimeType)) { + try { + CodecCapabilities capabilities = codecInfo.getCapabilitiesForType(supportedType); + boolean secure = mediaCodecList.isSecurePlaybackSupported(mimeType, capabilities); + if ((secureDecodersExplicit && key.secure == secure) + || (!secureDecodersExplicit && !key.secure)) { + decoderInfos.add(new DecoderInfo(codecName, capabilities)); + } else if (!secureDecodersExplicit && secure) { + decoderInfos.add(new DecoderInfo(codecName + ".secure", capabilities)); + // It only makes sense to have one synthesized secure decoder, return immediately. + return decoderInfos; + } + } catch (Exception e) { + if (Util.SDK_INT <= 23 && !decoderInfos.isEmpty()) { + // Suppress error querying secondary codec capabilities up to API level 23. + Log.e(TAG, "Skipping codec " + codecName + " (failed to query capabilities)"); + } else { + throw e; + } + } + } + } + } + } + return decoderInfos; + } catch (Exception e) { + // If the underlying mediaserver is in a bad state, we may catch an IllegalStateException + // or an IllegalArgumentException here. + throw new DecoderQueryException(e); + } + } + + /** + * Returns whether the specified codec is usable for decoding on the current device. + */ + private static boolean isCodecUsableDecoder(MediaCodecInfo info, String name, + boolean secureDecodersExplicit) { + if (info.isEncoder() || (!secureDecodersExplicit && name.endsWith(".secure"))) { + return false; + } + + // Work around broken audio decoders. + if (Util.SDK_INT < 21 + && ("CIPAACDecoder".equals(name)) + || "CIPMP3Decoder".equals(name) + || "CIPVorbisDecoder".equals(name) + || "AACDecoder".equals(name) + || "MP3Decoder".equals(name)) { + return false; + } + // Work around https://github.com/google/ExoPlayer/issues/398 + if (Util.SDK_INT < 18 && "OMX.SEC.MP3.Decoder".equals(name)) { + return false; + } + // Work around https://github.com/google/ExoPlayer/issues/1528 + if (Util.SDK_INT < 18 && "OMX.MTK.AUDIO.DECODER.AAC".equals(name) + && "a70".equals(Util.DEVICE)) { + return false; + } + + // Work around an issue where creating a particular MP3 decoder on some devices on platform API + // version 16 crashes mediaserver. + if (Util.SDK_INT == 16 + && "OMX.qcom.audio.decoder.mp3".equals(name) + && ("dlxu".equals(Util.DEVICE) // HTC Butterfly + || "protou".equals(Util.DEVICE) // HTC Desire X + || "C6602".equals(Util.DEVICE) // Sony Xperia Z + || "C6603".equals(Util.DEVICE) + || "C6606".equals(Util.DEVICE) + || "C6616".equals(Util.DEVICE) + || "L36h".equals(Util.DEVICE) + || "SO-02E".equals(Util.DEVICE))) { + return false; + } + + // Work around an issue where large timestamps are not propagated correctly. + if (Util.SDK_INT == 16 + && "OMX.qcom.audio.decoder.aac".equals(name) + && ("C1504".equals(Util.DEVICE) // Sony Xperia E + || "C1505".equals(Util.DEVICE) + || "C1604".equals(Util.DEVICE) // Sony Xperia E dual + || "C1605".equals(Util.DEVICE))) { + return false; + } + + // Work around an issue where the VP8 decoder on Samsung Galaxy S3/S4 Mini does not render + // video. + if (Util.SDK_INT <= 19 && Util.DEVICE != null + && (Util.DEVICE.startsWith("d2") || Util.DEVICE.startsWith("serrano")) + && "samsung".equals(Util.MANUFACTURER) && name.equals("OMX.SEC.vp8.dec")) { + return false; + } + + return true; + } + + /** + * Tests whether the device advertises it can decode video of a given type at a specified width + * and height. + *

+ * Must not be called if the device SDK version is less than 21. + * + * @param mimeType The mime type. + * @param secure Whether the decoder is required to support secure decryption. Always pass false + * unless secure decryption really is required. + * @param width Width in pixels. + * @param height Height in pixels. + * @return Whether the decoder advertises support of the given size. + */ + @TargetApi(21) + public static boolean isSizeSupportedV21(String mimeType, boolean secure, int width, + int height) throws DecoderQueryException { + Assertions.checkState(Util.SDK_INT >= 21); + MediaCodecInfo.VideoCapabilities videoCapabilities = getVideoCapabilitiesV21(mimeType, secure); + return videoCapabilities != null && videoCapabilities.isSizeSupported(width, height); + } + + /** + * Tests whether the device advertises it can decode video of a given type at a specified + * width, height, and frame rate. + *

+ * Must not be called if the device SDK version is less than 21. + * + * @param mimeType The mime type. + * @param secure Whether the decoder is required to support secure decryption. Always pass false + * unless secure decryption really is required. + * @param width Width in pixels. + * @param height Height in pixels. + * @param frameRate Frame rate in frames per second. + * @return Whether the decoder advertises support of the given size and frame rate. + */ + @TargetApi(21) + public static boolean isSizeAndRateSupportedV21(String mimeType, boolean secure, + int width, int height, double frameRate) throws DecoderQueryException { + Assertions.checkState(Util.SDK_INT >= 21); + MediaCodecInfo.VideoCapabilities videoCapabilities = getVideoCapabilitiesV21(mimeType, secure); + return videoCapabilities != null + && videoCapabilities.areSizeAndRateSupported(width, height, frameRate); + } + + /** + * @param profile An AVC profile constant from {@link CodecProfileLevel}. + * @param level An AVC profile level from {@link CodecProfileLevel}. + * @return Whether the specified profile is supported at the specified level. + * @deprecated Prefer {@link #getDecoderInfos(String, boolean)} for new code. + */ + @Deprecated + public static boolean isH264ProfileSupported(int profile, int level) + throws DecoderQueryException { + DecoderInfo decoderInfo = getDecoderInfo(MimeTypes.VIDEO_H264, false); + if (decoderInfo == null) { + return false; + } + for (CodecProfileLevel profileLevel : decoderInfo.capabilities.profileLevels) { + if (profileLevel.profile == profile && profileLevel.level >= level) { + return true; + } + } + return false; + } + + /** + * @return the maximum frame size for an H264 stream that can be decoded on the device. + */ + public static int maxH264DecodableFrameSize() throws DecoderQueryException { + if (maxH264DecodableFrameSize == -1) { + int result = 0; + DecoderInfo decoderInfo = getDecoderInfo(MimeTypes.VIDEO_H264, false); + if (decoderInfo != null) { + for (CodecProfileLevel profileLevel : decoderInfo.capabilities.profileLevels) { + result = Math.max(avcLevelToMaxFrameSize(profileLevel.level), result); + } + // We assume support for at least 360p. + result = Math.max(result, 480 * 360); + } + maxH264DecodableFrameSize = result; + } + return maxH264DecodableFrameSize; + } + + @TargetApi(21) + private static MediaCodecInfo.VideoCapabilities getVideoCapabilitiesV21(String mimeType, + boolean secure) throws DecoderQueryException { + DecoderInfo decoderInfo = getDecoderInfo(mimeType, secure); + return decoderInfo == null ? null : decoderInfo.capabilities.getVideoCapabilities(); + } + + /** + * Conversion values taken from ISO 14496-10 Table A-1. + * + * @param avcLevel one of CodecProfileLevel.AVCLevel* constants. + * @return maximum frame size that can be decoded by a decoder with the specified avc level + * (or {@code -1} if the level is not recognized) + */ + private static int avcLevelToMaxFrameSize(int avcLevel) { + switch (avcLevel) { + case CodecProfileLevel.AVCLevel1: return 99 * 16 * 16; + case CodecProfileLevel.AVCLevel1b: return 99 * 16 * 16; + case CodecProfileLevel.AVCLevel12: return 396 * 16 * 16; + case CodecProfileLevel.AVCLevel13: return 396 * 16 * 16; + case CodecProfileLevel.AVCLevel2: return 396 * 16 * 16; + case CodecProfileLevel.AVCLevel21: return 792 * 16 * 16; + case CodecProfileLevel.AVCLevel22: return 1620 * 16 * 16; + case CodecProfileLevel.AVCLevel3: return 1620 * 16 * 16; + case CodecProfileLevel.AVCLevel31: return 3600 * 16 * 16; + case CodecProfileLevel.AVCLevel32: return 5120 * 16 * 16; + case CodecProfileLevel.AVCLevel4: return 8192 * 16 * 16; + case CodecProfileLevel.AVCLevel41: return 8192 * 16 * 16; + case CodecProfileLevel.AVCLevel42: return 8704 * 16 * 16; + case CodecProfileLevel.AVCLevel5: return 22080 * 16 * 16; + case CodecProfileLevel.AVCLevel51: return 36864 * 16 * 16; + default: return -1; + } + } + + private interface MediaCodecListCompat { + + /** + * The number of codecs in the list. + */ + int getCodecCount(); + + /** + * The info at the specified index in the list. + * + * @param index The index. + */ + MediaCodecInfo getCodecInfoAt(int index); + + /** + * @return Returns whether secure decoders are explicitly listed, if present. + */ + boolean secureDecodersExplicit(); + + /** + * Whether secure playback is supported for the given {@link CodecCapabilities}, which should + * have been obtained from a {@link MediaCodecInfo} obtained from this list. + */ + boolean isSecurePlaybackSupported(String mimeType, CodecCapabilities capabilities); + + } + + @TargetApi(21) + private static final class MediaCodecListCompatV21 implements MediaCodecListCompat { + + private final int codecKind; + + private MediaCodecInfo[] mediaCodecInfos; + + public MediaCodecListCompatV21(boolean includeSecure) { + codecKind = includeSecure ? MediaCodecList.ALL_CODECS : MediaCodecList.REGULAR_CODECS; + } + + @Override + public int getCodecCount() { + ensureMediaCodecInfosInitialized(); + return mediaCodecInfos.length; + } + + @Override + public MediaCodecInfo getCodecInfoAt(int index) { + ensureMediaCodecInfosInitialized(); + return mediaCodecInfos[index]; + } + + @Override + public boolean secureDecodersExplicit() { + return true; + } + + @Override + public boolean isSecurePlaybackSupported(String mimeType, CodecCapabilities capabilities) { + return capabilities.isFeatureSupported(CodecCapabilities.FEATURE_SecurePlayback); + } + + private void ensureMediaCodecInfosInitialized() { + if (mediaCodecInfos == null) { + mediaCodecInfos = new MediaCodecList(codecKind).getCodecInfos(); + } + } + + } + + @SuppressWarnings("deprecation") + private static final class MediaCodecListCompatV16 implements MediaCodecListCompat { + + @Override + public int getCodecCount() { + return MediaCodecList.getCodecCount(); + } + + @Override + public MediaCodecInfo getCodecInfoAt(int index) { + return MediaCodecList.getCodecInfoAt(index); + } + + @Override + public boolean secureDecodersExplicit() { + return false; + } + + @Override + public boolean isSecurePlaybackSupported(String mimeType, CodecCapabilities capabilities) { + // Secure decoders weren't explicitly listed prior to API level 21. We assume that a secure + // H264 decoder exists. + return MimeTypes.VIDEO_H264.equals(mimeType); + } + + } + + private static final class CodecKey { + + public final String mimeType; + public final boolean secure; + + public CodecKey(String mimeType, boolean secure) { + this.mimeType = mimeType; + this.secure = secure; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((mimeType == null) ? 0 : mimeType.hashCode()); + result = prime * result + (secure ? 1231 : 1237); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || obj.getClass() != CodecKey.class) { + return false; + } + CodecKey other = (CodecKey) obj; + return TextUtils.equals(mimeType, other.mimeType) && secure == other.secure; + } + + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/MediaCodecVideoTrackRenderer.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/MediaCodecVideoTrackRenderer.java new file mode 100755 index 000000000..cfff4ccff --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/MediaCodecVideoTrackRenderer.java @@ -0,0 +1,611 @@ +/* + * 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.messenger.exoplayer; + +import org.telegram.messenger.exoplayer.MediaCodecUtil.DecoderQueryException; +import org.telegram.messenger.exoplayer.drm.DrmSessionManager; +import org.telegram.messenger.exoplayer.util.MimeTypes; +import org.telegram.messenger.exoplayer.util.TraceUtil; +import org.telegram.messenger.exoplayer.util.Util; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.Context; +import android.media.MediaCodec; +import android.media.MediaCrypto; +import android.os.Handler; +import android.os.SystemClock; +import android.view.Surface; +import android.view.TextureView; + +import java.nio.ByteBuffer; + +/** + * Decodes and renders video using {@link MediaCodec}. + */ +@TargetApi(16) +public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { + + /** + * Interface definition for a callback to be notified of {@link MediaCodecVideoTrackRenderer} + * events. + */ + public interface EventListener extends MediaCodecTrackRenderer.EventListener { + + /** + * Invoked to report the number of frames dropped by the renderer. Dropped frames are reported + * whenever the renderer is stopped having dropped frames, and optionally, whenever the count + * reaches a specified threshold whilst the renderer is started. + * + * @param count The number of dropped frames. + * @param elapsed The duration in milliseconds over which the frames were dropped. This + * duration is timed from when the renderer was started or from when dropped frames were + * last reported (whichever was more recent), and not from when the first of the reported + * drops occurred. + */ + void onDroppedFrames(int count, long elapsed); + + /** + * Invoked each time there's a change in the size of the video being rendered. + * + * @param width The video width in pixels. + * @param height The video height in pixels. + * @param unappliedRotationDegrees For videos that require a rotation, this is the clockwise + * rotation in degrees that the application should apply for the video for it to be rendered + * in the correct orientation. This value will always be zero on API levels 21 and above, + * since the renderer will apply all necessary rotations internally. On earlier API levels + * this is not possible. Applications that use {@link TextureView} can apply the rotation by + * calling {@link TextureView#setTransform}. Applications that do not expect to encounter + * rotated videos can safely ignore this parameter. + * @param pixelWidthHeightRatio The width to height ratio of each pixel. For the normal case + * of square pixels this will be equal to 1.0. Different values are indicative of anamorphic + * content. + */ + void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, + float pixelWidthHeightRatio); + + /** + * Invoked when a frame is rendered to a surface for the first time following that surface + * having been set as the target for the renderer. + * + * @param surface The surface to which a first frame has been rendered. + */ + void onDrawnToSurface(Surface surface); + + } + + // TODO: Use MediaFormat constants if these get exposed through the API. See + // [Internal: b/14127601]. + private static final String KEY_CROP_LEFT = "crop-left"; + private static final String KEY_CROP_RIGHT = "crop-right"; + private static final String KEY_CROP_BOTTOM = "crop-bottom"; + private static final String KEY_CROP_TOP = "crop-top"; + + /** + * The type of a message that can be passed to an instance of this class via + * {@link ExoPlayer#sendMessage} or {@link ExoPlayer#blockingSendMessage}. The message object + * should be the target {@link Surface}, or null. + */ + public static final int MSG_SET_SURFACE = 1; + + private final VideoFrameReleaseTimeHelper frameReleaseTimeHelper; + private final EventListener eventListener; + private final long allowedJoiningTimeUs; + private final int videoScalingMode; + private final int maxDroppedFrameCountToNotify; + + private Surface surface; + private boolean reportedDrawnToSurface; + private boolean renderedFirstFrame; + private long joiningDeadlineUs; + private long droppedFrameAccumulationStartTimeMs; + private int droppedFrameCount; + private int consecutiveDroppedFrameCount; + + private int pendingRotationDegrees; + private float pendingPixelWidthHeightRatio; + private int currentWidth; + private int currentHeight; + private int currentUnappliedRotationDegrees; + private float currentPixelWidthHeightRatio; + private int lastReportedWidth; + private int lastReportedHeight; + private int lastReportedUnappliedRotationDegrees; + private float lastReportedPixelWidthHeightRatio; + + /** + * @param context A context. + * @param source The upstream source from which the renderer obtains samples. + * @param mediaCodecSelector A decoder selector. + * @param videoScalingMode The scaling mode to pass to + * {@link MediaCodec#setVideoScalingMode(int)}. + */ + public MediaCodecVideoTrackRenderer(Context context, SampleSource source, + MediaCodecSelector mediaCodecSelector, int videoScalingMode) { + this(context, source, mediaCodecSelector, videoScalingMode, 0); + } + + /** + * @param context A context. + * @param source The upstream source from which the renderer obtains samples. + * @param mediaCodecSelector A decoder selector. + * @param videoScalingMode The scaling mode to pass to + * {@link MediaCodec#setVideoScalingMode(int)}. + * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer + * can attempt to seamlessly join an ongoing playback. + */ + public MediaCodecVideoTrackRenderer(Context context, SampleSource source, + MediaCodecSelector mediaCodecSelector, int videoScalingMode, long allowedJoiningTimeMs) { + this(context, source, mediaCodecSelector, videoScalingMode, allowedJoiningTimeMs, null, null, + -1); + } + + /** + * @param context A context. + * @param source The upstream source from which the renderer obtains samples. + * @param mediaCodecSelector A decoder selector. + * @param videoScalingMode The scaling mode to pass to + * {@link MediaCodec#setVideoScalingMode(int)}. + * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer + * can attempt to seamlessly join an ongoing playback. + * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be + * null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + * @param maxDroppedFrameCountToNotify The maximum number of frames that can be dropped between + * invocations of {@link EventListener#onDroppedFrames(int, long)}. + */ + public MediaCodecVideoTrackRenderer(Context context, SampleSource source, + MediaCodecSelector mediaCodecSelector, int videoScalingMode, long allowedJoiningTimeMs, + Handler eventHandler, EventListener eventListener, int maxDroppedFrameCountToNotify) { + this(context, source, mediaCodecSelector, videoScalingMode, allowedJoiningTimeMs, null, false, + eventHandler, eventListener, maxDroppedFrameCountToNotify); + } + + /** + * @param context A context. + * @param source The upstream source from which the renderer obtains samples. + * @param mediaCodecSelector A decoder selector. + * @param videoScalingMode The scaling mode to pass to + * {@link MediaCodec#setVideoScalingMode(int)}. + * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer + * can attempt to seamlessly join an ongoing playback. + * @param drmSessionManager For use with encrypted content. May be null if support for encrypted + * content is not required. + * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions. + * For example a media file may start with a short clear region so as to allow playback to + * begin in parallel with key acquisision. This parameter specifies whether the renderer is + * permitted to play clear regions of encrypted media files before {@code drmSessionManager} + * has obtained the keys necessary to decrypt encrypted regions of the media. + * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be + * null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + * @param maxDroppedFrameCountToNotify The maximum number of frames that can be dropped between + * invocations of {@link EventListener#onDroppedFrames(int, long)}. + */ + public MediaCodecVideoTrackRenderer(Context context, SampleSource source, + MediaCodecSelector mediaCodecSelector, int videoScalingMode, long allowedJoiningTimeMs, + DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, + Handler eventHandler, EventListener eventListener, int maxDroppedFrameCountToNotify) { + super(source, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, eventHandler, + eventListener); + this.frameReleaseTimeHelper = new VideoFrameReleaseTimeHelper(context); + this.videoScalingMode = videoScalingMode; + this.allowedJoiningTimeUs = allowedJoiningTimeMs * 1000; + this.eventListener = eventListener; + this.maxDroppedFrameCountToNotify = maxDroppedFrameCountToNotify; + joiningDeadlineUs = -1; + currentWidth = -1; + currentHeight = -1; + currentPixelWidthHeightRatio = -1; + pendingPixelWidthHeightRatio = -1; + lastReportedWidth = -1; + lastReportedHeight = -1; + lastReportedPixelWidthHeightRatio = -1; + } + + @Override + protected boolean handlesTrack(MediaCodecSelector mediaCodecSelector, MediaFormat mediaFormat) + throws DecoderQueryException { + String mimeType = mediaFormat.mimeType; + return MimeTypes.isVideo(mimeType) && (MimeTypes.VIDEO_UNKNOWN.equals(mimeType) + || mediaCodecSelector.getDecoderInfo(mimeType, false) != null); + } + + @Override + protected void onEnabled(int track, long positionUs, boolean joining) + throws ExoPlaybackException { + super.onEnabled(track, positionUs, joining); + if (joining && allowedJoiningTimeUs > 0) { + joiningDeadlineUs = SystemClock.elapsedRealtime() * 1000L + allowedJoiningTimeUs; + } + frameReleaseTimeHelper.enable(); + } + + @Override + protected void onDiscontinuity(long positionUs) throws ExoPlaybackException { + super.onDiscontinuity(positionUs); + renderedFirstFrame = false; + consecutiveDroppedFrameCount = 0; + joiningDeadlineUs = -1; + } + + @Override + protected boolean isReady() { + if (super.isReady() && (renderedFirstFrame || !codecInitialized() + || getSourceState() == SOURCE_STATE_READY_READ_MAY_FAIL)) { + // Ready. If we were joining then we've now joined, so clear the joining deadline. + joiningDeadlineUs = -1; + return true; + } else if (joiningDeadlineUs == -1) { + // Not joining. + return false; + } else if (SystemClock.elapsedRealtime() * 1000 < joiningDeadlineUs) { + // Joining and still within the joining deadline. + return true; + } else { + // The joining deadline has been exceeded. Give up and clear the deadline. + joiningDeadlineUs = -1; + return false; + } + } + + @Override + protected void onStarted() { + super.onStarted(); + droppedFrameCount = 0; + droppedFrameAccumulationStartTimeMs = SystemClock.elapsedRealtime(); + } + + @Override + protected void onStopped() { + joiningDeadlineUs = -1; + maybeNotifyDroppedFrameCount(); + super.onStopped(); + } + + @Override + protected void onDisabled() throws ExoPlaybackException { + currentWidth = -1; + currentHeight = -1; + currentPixelWidthHeightRatio = -1; + pendingPixelWidthHeightRatio = -1; + lastReportedWidth = -1; + lastReportedHeight = -1; + lastReportedPixelWidthHeightRatio = -1; + frameReleaseTimeHelper.disable(); + super.onDisabled(); + } + + @Override + public void handleMessage(int messageType, Object message) throws ExoPlaybackException { + if (messageType == MSG_SET_SURFACE) { + setSurface((Surface) message); + } else { + super.handleMessage(messageType, message); + } + } + + /** + * @param surface The surface to set. + * @throws ExoPlaybackException + */ + private void setSurface(Surface surface) throws ExoPlaybackException { + if (this.surface == surface) { + return; + } + this.surface = surface; + this.reportedDrawnToSurface = false; + int state = getState(); + if (state == TrackRenderer.STATE_ENABLED || state == TrackRenderer.STATE_STARTED) { + releaseCodec(); + maybeInitCodec(); + } + } + + @Override + protected boolean shouldInitCodec() { + return super.shouldInitCodec() && surface != null && surface.isValid(); + } + + // Override configureCodec to provide the surface. + @Override + protected void configureCodec(MediaCodec codec, boolean codecIsAdaptive, + android.media.MediaFormat format, MediaCrypto crypto) { + maybeSetMaxInputSize(format, codecIsAdaptive); + codec.configure(format, surface, crypto, 0); + } + + @Override + protected void onInputFormatChanged(MediaFormatHolder holder) throws ExoPlaybackException { + super.onInputFormatChanged(holder); + pendingPixelWidthHeightRatio = holder.format.pixelWidthHeightRatio == MediaFormat.NO_VALUE ? 1 + : holder.format.pixelWidthHeightRatio; + pendingRotationDegrees = holder.format.rotationDegrees == MediaFormat.NO_VALUE ? 0 + : holder.format.rotationDegrees; + } + + /** + * @return True if the first frame has been rendered (playback has not necessarily begun). + */ + protected final boolean haveRenderedFirstFrame() { + return renderedFirstFrame; + } + + @Override + protected void onOutputFormatChanged(MediaCodec codec, android.media.MediaFormat outputFormat) { + boolean hasCrop = outputFormat.containsKey(KEY_CROP_RIGHT) + && outputFormat.containsKey(KEY_CROP_LEFT) && outputFormat.containsKey(KEY_CROP_BOTTOM) + && outputFormat.containsKey(KEY_CROP_TOP); + currentWidth = hasCrop + ? outputFormat.getInteger(KEY_CROP_RIGHT) - outputFormat.getInteger(KEY_CROP_LEFT) + 1 + : outputFormat.getInteger(android.media.MediaFormat.KEY_WIDTH); + currentHeight = hasCrop + ? outputFormat.getInteger(KEY_CROP_BOTTOM) - outputFormat.getInteger(KEY_CROP_TOP) + 1 + : outputFormat.getInteger(android.media.MediaFormat.KEY_HEIGHT); + currentPixelWidthHeightRatio = pendingPixelWidthHeightRatio; + if (Util.SDK_INT >= 21) { + // On API level 21 and above the decoder applies the rotation when rendering to the surface. + // Hence currentUnappliedRotation should always be 0. For 90 and 270 degree rotations, we need + // to flip the width, height and pixel aspect ratio to reflect the rotation that was applied. + if (pendingRotationDegrees == 90 || pendingRotationDegrees == 270) { + int rotatedHeight = currentWidth; + currentWidth = currentHeight; + currentHeight = rotatedHeight; + currentPixelWidthHeightRatio = 1 / currentPixelWidthHeightRatio; + } + } else { + // On API level 20 and below the decoder does not apply the rotation. + currentUnappliedRotationDegrees = pendingRotationDegrees; + } + // Must be applied each time the output format changes. + codec.setVideoScalingMode(videoScalingMode); + } + + @Override + protected boolean canReconfigureCodec(MediaCodec codec, boolean codecIsAdaptive, + MediaFormat oldFormat, MediaFormat newFormat) { + return newFormat.mimeType.equals(oldFormat.mimeType) + && (codecIsAdaptive + || (oldFormat.width == newFormat.width && oldFormat.height == newFormat.height)); + } + + @Override + protected boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs, MediaCodec codec, + ByteBuffer buffer, MediaCodec.BufferInfo bufferInfo, int bufferIndex, boolean shouldSkip) { + if (shouldSkip) { + skipOutputBuffer(codec, bufferIndex); + consecutiveDroppedFrameCount = 0; + return true; + } + + if (!renderedFirstFrame) { + if (Util.SDK_INT >= 21) { + renderOutputBufferV21(codec, bufferIndex, System.nanoTime()); + } else { + renderOutputBuffer(codec, bufferIndex); + } + consecutiveDroppedFrameCount = 0; + return true; + } + + if (getState() != TrackRenderer.STATE_STARTED) { + return false; + } + + // Compute how many microseconds it is until the buffer's presentation time. + long elapsedSinceStartOfLoopUs = (SystemClock.elapsedRealtime() * 1000) - elapsedRealtimeUs; + long earlyUs = bufferInfo.presentationTimeUs - positionUs - elapsedSinceStartOfLoopUs; + + // Compute the buffer's desired release time in nanoseconds. + long systemTimeNs = System.nanoTime(); + long unadjustedFrameReleaseTimeNs = systemTimeNs + (earlyUs * 1000); + + // Apply a timestamp adjustment, if there is one. + long adjustedReleaseTimeNs = frameReleaseTimeHelper.adjustReleaseTime( + bufferInfo.presentationTimeUs, unadjustedFrameReleaseTimeNs); + earlyUs = (adjustedReleaseTimeNs - systemTimeNs) / 1000; + + if (earlyUs < -30000) { + // We're more than 30ms late rendering the frame. + dropOutputBuffer(codec, bufferIndex); + return true; + } + + if (Util.SDK_INT >= 21) { + // Let the underlying framework time the release. + if (earlyUs < 50000) { + renderOutputBufferV21(codec, bufferIndex, adjustedReleaseTimeNs); + consecutiveDroppedFrameCount = 0; + return true; + } + } else { + // We need to time the release ourselves. + if (earlyUs < 30000) { + if (earlyUs > 11000) { + // We're a little too early to render the frame. Sleep until the frame can be rendered. + // Note: The 11ms threshold was chosen fairly arbitrarily. + try { + // Subtracting 10000 rather than 11000 ensures the sleep time will be at least 1ms. + Thread.sleep((earlyUs - 10000) / 1000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + renderOutputBuffer(codec, bufferIndex); + consecutiveDroppedFrameCount = 0; + return true; + } + } + + // We're either not playing, or it's not time to render the frame yet. + return false; + } + + protected void skipOutputBuffer(MediaCodec codec, int bufferIndex) { + TraceUtil.beginSection("skipVideoBuffer"); + codec.releaseOutputBuffer(bufferIndex, false); + TraceUtil.endSection(); + codecCounters.skippedOutputBufferCount++; + } + + protected void dropOutputBuffer(MediaCodec codec, int bufferIndex) { + TraceUtil.beginSection("dropVideoBuffer"); + codec.releaseOutputBuffer(bufferIndex, false); + TraceUtil.endSection(); + codecCounters.droppedOutputBufferCount++; + droppedFrameCount++; + consecutiveDroppedFrameCount++; + codecCounters.maxConsecutiveDroppedOutputBufferCount = Math.max(consecutiveDroppedFrameCount, + codecCounters.maxConsecutiveDroppedOutputBufferCount); + if (droppedFrameCount == maxDroppedFrameCountToNotify) { + maybeNotifyDroppedFrameCount(); + } + } + + protected void renderOutputBuffer(MediaCodec codec, int bufferIndex) { + maybeNotifyVideoSizeChanged(); + TraceUtil.beginSection("releaseOutputBuffer"); + codec.releaseOutputBuffer(bufferIndex, true); + TraceUtil.endSection(); + codecCounters.renderedOutputBufferCount++; + renderedFirstFrame = true; + maybeNotifyDrawnToSurface(); + } + + @TargetApi(21) + protected void renderOutputBufferV21(MediaCodec codec, int bufferIndex, long releaseTimeNs) { + maybeNotifyVideoSizeChanged(); + TraceUtil.beginSection("releaseOutputBuffer"); + codec.releaseOutputBuffer(bufferIndex, releaseTimeNs); + TraceUtil.endSection(); + codecCounters.renderedOutputBufferCount++; + renderedFirstFrame = true; + maybeNotifyDrawnToSurface(); + } + + @SuppressLint("InlinedApi") + private void maybeSetMaxInputSize(android.media.MediaFormat format, boolean codecIsAdaptive) { + if (format.containsKey(android.media.MediaFormat.KEY_MAX_INPUT_SIZE)) { + // Already set. The source of the format may know better, so do nothing. + return; + } + int maxHeight = format.getInteger(android.media.MediaFormat.KEY_HEIGHT); + if (codecIsAdaptive && format.containsKey(android.media.MediaFormat.KEY_MAX_HEIGHT)) { + maxHeight = Math.max(maxHeight, format.getInteger(android.media.MediaFormat.KEY_MAX_HEIGHT)); + } + int maxWidth = format.getInteger(android.media.MediaFormat.KEY_WIDTH); + if (codecIsAdaptive && format.containsKey(android.media.MediaFormat.KEY_MAX_WIDTH)) { + maxWidth = Math.max(maxHeight, format.getInteger(android.media.MediaFormat.KEY_MAX_WIDTH)); + } + int maxPixels; + int minCompressionRatio; + switch (format.getString(android.media.MediaFormat.KEY_MIME)) { + case MimeTypes.VIDEO_H263: + case MimeTypes.VIDEO_MP4V: + maxPixels = maxWidth * maxHeight; + minCompressionRatio = 2; + break; + case MimeTypes.VIDEO_H264: + if ("BRAVIA 4K 2015".equals(Util.MODEL)) { + // The Sony BRAVIA 4k TV has input buffers that are too small for the calculated 4k video + // maximum input size, so use the default value. + return; + } + // Round up width/height to an integer number of macroblocks. + maxPixels = ((maxWidth + 15) / 16) * ((maxHeight + 15) / 16) * 16 * 16; + minCompressionRatio = 2; + break; + case MimeTypes.VIDEO_VP8: + // VPX does not specify a ratio so use the values from the platform's SoftVPX.cpp. + maxPixels = maxWidth * maxHeight; + minCompressionRatio = 2; + break; + case MimeTypes.VIDEO_H265: + case MimeTypes.VIDEO_VP9: + maxPixels = maxWidth * maxHeight; + minCompressionRatio = 4; + break; + default: + // Leave the default max input size. + return; + } + // Estimate the maximum input size assuming three channel 4:2:0 subsampled input frames. + int maxInputSize = (maxPixels * 3) / (2 * minCompressionRatio); + format.setInteger(android.media.MediaFormat.KEY_MAX_INPUT_SIZE, maxInputSize); + } + + private void maybeNotifyVideoSizeChanged() { + if (eventHandler == null || eventListener == null + || (lastReportedWidth == currentWidth && lastReportedHeight == currentHeight + && lastReportedUnappliedRotationDegrees == currentUnappliedRotationDegrees + && lastReportedPixelWidthHeightRatio == currentPixelWidthHeightRatio)) { + return; + } + // Make final copies to ensure the runnable reports the correct values. + final int currentWidth = this.currentWidth; + final int currentHeight = this.currentHeight; + final int currentUnappliedRotationDegrees = this.currentUnappliedRotationDegrees; + final float currentPixelWidthHeightRatio = this.currentPixelWidthHeightRatio; + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onVideoSizeChanged(currentWidth, currentHeight, + currentUnappliedRotationDegrees, currentPixelWidthHeightRatio); + } + }); + // Update the last reported values. + lastReportedWidth = currentWidth; + lastReportedHeight = currentHeight; + lastReportedUnappliedRotationDegrees = currentUnappliedRotationDegrees; + lastReportedPixelWidthHeightRatio = currentPixelWidthHeightRatio; + } + + private void maybeNotifyDrawnToSurface() { + if (eventHandler == null || eventListener == null || reportedDrawnToSurface) { + return; + } + // Make a final copy to ensure the runnable reports the correct surface. + final Surface surface = this.surface; + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onDrawnToSurface(surface); + } + }); + // Record that we have reported that the surface has been drawn to. + reportedDrawnToSurface = true; + } + + private void maybeNotifyDroppedFrameCount() { + if (eventHandler == null || eventListener == null || droppedFrameCount == 0) { + return; + } + long now = SystemClock.elapsedRealtime(); + // Make final copies to ensure the runnable reports the correct values. + final int countToNotify = droppedFrameCount; + final long elapsedToNotify = now - droppedFrameAccumulationStartTimeMs; + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onDroppedFrames(countToNotify, elapsedToNotify); + } + }); + // Reset the dropped frame tracking. + droppedFrameCount = 0; + droppedFrameAccumulationStartTimeMs = now; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/MediaFormat.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/MediaFormat.java new file mode 100755 index 000000000..56552bab9 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/MediaFormat.java @@ -0,0 +1,502 @@ +/* + * 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.messenger.exoplayer; + +import org.telegram.messenger.exoplayer.util.Assertions; +import org.telegram.messenger.exoplayer.util.MimeTypes; +import org.telegram.messenger.exoplayer.util.Util; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.os.Parcel; +import android.os.Parcelable; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * Defines the format of an elementary media stream. + */ +public final class MediaFormat implements Parcelable { + + public static final int NO_VALUE = -1; + + /** + * A value for {@link #subsampleOffsetUs} to indicate that subsample timestamps are relative to + * the timestamps of their parent samples. + */ + public static final long OFFSET_SAMPLE_RELATIVE = Long.MAX_VALUE; + + /** + * The identifier for the track represented by the format, or null if unknown or not applicable. + */ + public final String trackId; + /** + * The mime type of the format. + */ + public final String mimeType; + /** + * The average bandwidth in bits per second, or {@link #NO_VALUE} if unknown or not applicable. + */ + public final int bitrate; + /** + * The maximum size of a buffer of data (typically one sample) in the format, or {@link #NO_VALUE} + * if unknown or not applicable. + */ + public final int maxInputSize; + /** + * The duration in microseconds, or {@link C#UNKNOWN_TIME_US} if the duration is unknown, or + * {@link C#MATCH_LONGEST_US} if the duration should match the duration of the longest track whose + * duration is known. + */ + public final long durationUs; + /** + * Initialization data that must be provided to the decoder. Will not be null, but may be empty + * if initialization data is not required. + */ + public final List initializationData; + /** + * Whether the format represents an adaptive track, meaning that the format of the actual media + * data may change (e.g. to adapt to network conditions). + */ + public final boolean adaptive; + + // Video specific. + + /** + * The width of the video in pixels, or {@link #NO_VALUE} if unknown or not applicable. + */ + public final int width; + + /** + * The height of the video in pixels, or {@link #NO_VALUE} if unknown or not applicable. + */ + public final int height; + /** + * For formats that belong to an adaptive video track (either describing the track, or describing + * a specific format within it), this is the maximum width of the video in pixels that will be + * encountered in the stream. Set to {@link #NO_VALUE} if unknown or not applicable. + */ + public final int maxWidth; + /** + * For formats that belong to an adaptive video track (either describing the track, or describing + * a specific format within it), this is the maximum height of the video in pixels that will be + * encountered in the stream. Set to {@link #NO_VALUE} if unknown or not applicable. + */ + public final int maxHeight; + /** + * The clockwise rotation that should be applied to the video for it to be rendered in the correct + * orientation, or {@link #NO_VALUE} if unknown or not applicable. Only 0, 90, 180 and 270 are + * supported. + */ + public final int rotationDegrees; + /** + * The width to height ratio of pixels in the video, or {@link #NO_VALUE} if unknown or not + * applicable. + */ + public final float pixelWidthHeightRatio; + + // Audio specific. + + /** + * The number of audio channels, or {@link #NO_VALUE} if unknown or not applicable. + */ + public final int channelCount; + /** + * The audio sampling rate in Hz, or {@link #NO_VALUE} if unknown or not applicable. + */ + public final int sampleRate; + /** + * The encoding for PCM audio streams. If {@link #mimeType} is {@link MimeTypes#AUDIO_RAW} then + * one of {@link C#ENCODING_PCM_8BIT}, {@link C#ENCODING_PCM_16BIT}, {@link C#ENCODING_PCM_24BIT} + * and {@link C#ENCODING_PCM_32BIT}. Set to {@link #NO_VALUE} for other media types. + */ + public final int pcmEncoding; + /** + * The number of samples to trim from the start of the decoded audio stream. + */ + public final int encoderDelay; + /** + * The number of samples to trim from the end of the decoded audio stream. + */ + public final int encoderPadding; + + // Text specific. + + /** + * The language of the track, or null if unknown or not applicable. + */ + public final String language; + + /** + * For samples that contain subsamples, this is an offset that should be added to subsample + * timestamps. A value of {@link #OFFSET_SAMPLE_RELATIVE} indicates that subsample timestamps are + * relative to the timestamps of their parent samples. + */ + public final long subsampleOffsetUs; + + // Lazy-initialized hashcode and framework media format. + + private int hashCode; + private android.media.MediaFormat frameworkMediaFormat; + + public static MediaFormat createVideoFormat(String trackId, String mimeType, int bitrate, + int maxInputSize, long durationUs, int width, int height, List initializationData) { + return createVideoFormat(trackId, mimeType, bitrate, maxInputSize, durationUs, width, height, + initializationData, NO_VALUE, NO_VALUE); + } + + public static MediaFormat createVideoFormat(String trackId, String mimeType, int bitrate, + int maxInputSize, long durationUs, int width, int height, List initializationData, + int rotationDegrees, float pixelWidthHeightRatio) { + return new MediaFormat(trackId, mimeType, bitrate, maxInputSize, durationUs, width, height, + rotationDegrees, pixelWidthHeightRatio, NO_VALUE, NO_VALUE, null, OFFSET_SAMPLE_RELATIVE, + initializationData, false, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE); + } + + public static MediaFormat createAudioFormat(String trackId, String mimeType, int bitrate, + int maxInputSize, long durationUs, int channelCount, int sampleRate, + List initializationData, String language) { + return createAudioFormat(trackId, mimeType, bitrate, maxInputSize, durationUs, channelCount, + sampleRate, initializationData, language, NO_VALUE); + } + + public static MediaFormat createAudioFormat(String trackId, String mimeType, int bitrate, + int maxInputSize, long durationUs, int channelCount, int sampleRate, + List initializationData, String language, int pcmEncoding) { + return new MediaFormat(trackId, mimeType, bitrate, maxInputSize, durationUs, NO_VALUE, NO_VALUE, + NO_VALUE, NO_VALUE, channelCount, sampleRate, language, OFFSET_SAMPLE_RELATIVE, + initializationData, false, NO_VALUE, NO_VALUE, pcmEncoding, NO_VALUE, NO_VALUE); + } + + public static MediaFormat createTextFormat(String trackId, String mimeType, int bitrate, + long durationUs, String language) { + return createTextFormat(trackId, mimeType, bitrate, durationUs, language, + OFFSET_SAMPLE_RELATIVE); + } + + public static MediaFormat createTextFormat(String trackId, String mimeType, int bitrate, + long durationUs, String language, long subsampleOffsetUs) { + return new MediaFormat(trackId, mimeType, bitrate, NO_VALUE, durationUs, NO_VALUE, NO_VALUE, + NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, language, subsampleOffsetUs, null, false, NO_VALUE, + NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE); + } + + public static MediaFormat createImageFormat(String trackId, String mimeType, int bitrate, + long durationUs, List initializationData, String language) { + return new MediaFormat(trackId, mimeType, bitrate, NO_VALUE, durationUs, NO_VALUE, NO_VALUE, + NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, language, OFFSET_SAMPLE_RELATIVE, + initializationData, false, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE); + } + + public static MediaFormat createFormatForMimeType(String trackId, String mimeType, int bitrate, + long durationUs) { + return new MediaFormat(trackId, mimeType, bitrate, NO_VALUE, durationUs, NO_VALUE, NO_VALUE, + NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, OFFSET_SAMPLE_RELATIVE, null, false, NO_VALUE, + NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE); + } + + public static MediaFormat createId3Format() { + return createFormatForMimeType(null, MimeTypes.APPLICATION_ID3, MediaFormat.NO_VALUE, + C.UNKNOWN_TIME_US); + } + + /* package */ MediaFormat(Parcel in) { + trackId = in.readString(); + mimeType = in.readString(); + bitrate = in.readInt(); + maxInputSize = in.readInt(); + durationUs = in.readLong(); + width = in.readInt(); + height = in.readInt(); + rotationDegrees = in.readInt(); + pixelWidthHeightRatio = in.readFloat(); + channelCount = in.readInt(); + sampleRate = in.readInt(); + language = in.readString(); + subsampleOffsetUs = in.readLong(); + initializationData = new ArrayList<>(); + in.readList(initializationData, null); + adaptive = in.readInt() == 1; + maxWidth = in.readInt(); + maxHeight = in.readInt(); + pcmEncoding = in.readInt(); + encoderDelay = in.readInt(); + encoderPadding = in.readInt(); + } + + /* package */ MediaFormat(String trackId, String mimeType, int bitrate, int maxInputSize, + long durationUs, int width, int height, int rotationDegrees, float pixelWidthHeightRatio, + int channelCount, int sampleRate, String language, long subsampleOffsetUs, + List initializationData, boolean adaptive, int maxWidth, int maxHeight, + int pcmEncoding, int encoderDelay, int encoderPadding) { + this.trackId = trackId; + this.mimeType = Assertions.checkNotEmpty(mimeType); + this.bitrate = bitrate; + this.maxInputSize = maxInputSize; + this.durationUs = durationUs; + this.width = width; + this.height = height; + this.rotationDegrees = rotationDegrees; + this.pixelWidthHeightRatio = pixelWidthHeightRatio; + this.channelCount = channelCount; + this.sampleRate = sampleRate; + this.language = language; + this.subsampleOffsetUs = subsampleOffsetUs; + this.initializationData = initializationData == null ? Collections.emptyList() + : initializationData; + this.adaptive = adaptive; + this.maxWidth = maxWidth; + this.maxHeight = maxHeight; + this.pcmEncoding = pcmEncoding; + this.encoderDelay = encoderDelay; + this.encoderPadding = encoderPadding; + } + + public MediaFormat copyWithMaxInputSize(int maxInputSize) { + return new MediaFormat(trackId, mimeType, bitrate, maxInputSize, durationUs, width, height, + rotationDegrees, pixelWidthHeightRatio, channelCount, sampleRate, language, + subsampleOffsetUs, initializationData, adaptive, maxWidth, maxHeight, pcmEncoding, + encoderDelay, encoderPadding); + } + + public MediaFormat copyWithMaxVideoDimensions(int maxWidth, int maxHeight) { + return new MediaFormat(trackId, mimeType, bitrate, maxInputSize, durationUs, width, height, + rotationDegrees, pixelWidthHeightRatio, channelCount, sampleRate, language, + subsampleOffsetUs, initializationData, adaptive, maxWidth, maxHeight, pcmEncoding, + encoderDelay, encoderPadding); + } + + public MediaFormat copyWithSubsampleOffsetUs(long subsampleOffsetUs) { + return new MediaFormat(trackId, mimeType, bitrate, maxInputSize, durationUs, width, height, + rotationDegrees, pixelWidthHeightRatio, channelCount, sampleRate, language, + subsampleOffsetUs, initializationData, adaptive, maxWidth, maxHeight, pcmEncoding, + encoderDelay, encoderPadding); + } + + public MediaFormat copyWithDurationUs(long durationUs) { + return new MediaFormat(trackId, mimeType, bitrate, maxInputSize, durationUs, width, height, + rotationDegrees, pixelWidthHeightRatio, channelCount, sampleRate, language, + subsampleOffsetUs, initializationData, adaptive, maxWidth, maxHeight, pcmEncoding, + encoderDelay, encoderPadding); + } + + public MediaFormat copyWithLanguage(String language) { + return new MediaFormat(trackId, mimeType, bitrate, maxInputSize, durationUs, width, height, + rotationDegrees, pixelWidthHeightRatio, channelCount, sampleRate, language, + subsampleOffsetUs, initializationData, adaptive, maxWidth, maxHeight, pcmEncoding, + encoderDelay, encoderPadding); + } + + public MediaFormat copyWithFixedTrackInfo(String trackId, int bitrate, int width, int height, + String language) { + return new MediaFormat(trackId, mimeType, bitrate, maxInputSize, durationUs, width, height, + rotationDegrees, pixelWidthHeightRatio, channelCount, sampleRate, language, + subsampleOffsetUs, initializationData, adaptive, NO_VALUE, NO_VALUE, pcmEncoding, + encoderDelay, encoderPadding); + } + + public MediaFormat copyAsAdaptive(String trackId) { + return new MediaFormat(trackId, mimeType, NO_VALUE, NO_VALUE, durationUs, NO_VALUE, NO_VALUE, + NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, OFFSET_SAMPLE_RELATIVE, null, true, maxWidth, + maxHeight, NO_VALUE, NO_VALUE, NO_VALUE); + } + + public MediaFormat copyWithGaplessInfo(int encoderDelay, int encoderPadding) { + return new MediaFormat(trackId, mimeType, bitrate, maxInputSize, durationUs, width, height, + rotationDegrees, pixelWidthHeightRatio, channelCount, sampleRate, language, + subsampleOffsetUs, initializationData, adaptive, maxWidth, maxHeight, pcmEncoding, + encoderDelay, encoderPadding); + } + + /** + * @return A {@link MediaFormat} representation of this format. + */ + @SuppressLint("InlinedApi") + @TargetApi(16) + public final android.media.MediaFormat getFrameworkMediaFormatV16() { + if (frameworkMediaFormat == null) { + android.media.MediaFormat format = new android.media.MediaFormat(); + format.setString(android.media.MediaFormat.KEY_MIME, mimeType); + maybeSetStringV16(format, android.media.MediaFormat.KEY_LANGUAGE, language); + maybeSetIntegerV16(format, android.media.MediaFormat.KEY_MAX_INPUT_SIZE, maxInputSize); + maybeSetIntegerV16(format, android.media.MediaFormat.KEY_WIDTH, width); + maybeSetIntegerV16(format, android.media.MediaFormat.KEY_HEIGHT, height); + maybeSetIntegerV16(format, "rotation-degrees", rotationDegrees); + maybeSetIntegerV16(format, android.media.MediaFormat.KEY_MAX_WIDTH, maxWidth); + maybeSetIntegerV16(format, android.media.MediaFormat.KEY_MAX_HEIGHT, maxHeight); + maybeSetIntegerV16(format, android.media.MediaFormat.KEY_CHANNEL_COUNT, channelCount); + maybeSetIntegerV16(format, android.media.MediaFormat.KEY_SAMPLE_RATE, sampleRate); + maybeSetIntegerV16(format, "encoder-delay", encoderDelay); + maybeSetIntegerV16(format, "encoder-padding", encoderPadding); + for (int i = 0; i < initializationData.size(); i++) { + format.setByteBuffer("csd-" + i, ByteBuffer.wrap(initializationData.get(i))); + } + if (durationUs != C.UNKNOWN_TIME_US) { + format.setLong(android.media.MediaFormat.KEY_DURATION, durationUs); + } + frameworkMediaFormat = format; + } + return frameworkMediaFormat; + } + + /** + * Sets the framework format returned by {@link #getFrameworkMediaFormatV16()}. + * + * @deprecated This method only exists for FrameworkSampleSource, which is itself deprecated. + * @param format The framework format. + */ + @Deprecated + @TargetApi(16) + /* package */ final void setFrameworkFormatV16(android.media.MediaFormat format) { + frameworkMediaFormat = format; + } + + @Override + public String toString() { + return "MediaFormat(" + trackId + ", " + mimeType + ", " + bitrate + ", " + maxInputSize + + ", " + width + ", " + height + ", " + rotationDegrees + ", " + pixelWidthHeightRatio + + ", " + channelCount + ", " + sampleRate + ", " + language + ", " + durationUs + ", " + + adaptive + ", " + maxWidth + ", " + maxHeight + ", " + pcmEncoding + ", " + encoderDelay + + ", " + encoderPadding + ")"; + } + + @Override + public int hashCode() { + if (hashCode == 0) { + int result = 17; + result = 31 * result + (trackId == null ? 0 : trackId.hashCode()); + result = 31 * result + (mimeType == null ? 0 : mimeType.hashCode()); + result = 31 * result + bitrate; + result = 31 * result + maxInputSize; + result = 31 * result + width; + result = 31 * result + height; + result = 31 * result + rotationDegrees; + result = 31 * result + Float.floatToRawIntBits(pixelWidthHeightRatio); + result = 31 * result + (int) durationUs; + result = 31 * result + (adaptive ? 1231 : 1237); + result = 31 * result + maxWidth; + result = 31 * result + maxHeight; + result = 31 * result + channelCount; + result = 31 * result + sampleRate; + result = 31 * result + pcmEncoding; + result = 31 * result + encoderDelay; + result = 31 * result + encoderPadding; + result = 31 * result + (language == null ? 0 : language.hashCode()); + result = 31 * result + (int) subsampleOffsetUs; + for (int i = 0; i < initializationData.size(); i++) { + result = 31 * result + Arrays.hashCode(initializationData.get(i)); + } + hashCode = result; + } + return hashCode; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + MediaFormat other = (MediaFormat) obj; + if (adaptive != other.adaptive || bitrate != other.bitrate || maxInputSize != other.maxInputSize + || durationUs != other.durationUs || width != other.width || height != other.height + || rotationDegrees != other.rotationDegrees + || pixelWidthHeightRatio != other.pixelWidthHeightRatio + || maxWidth != other.maxWidth || maxHeight != other.maxHeight + || channelCount != other.channelCount || sampleRate != other.sampleRate + || pcmEncoding != other.pcmEncoding || encoderDelay != other.encoderDelay + || encoderPadding != other.encoderPadding || subsampleOffsetUs != other.subsampleOffsetUs + || !Util.areEqual(trackId, other.trackId) || !Util.areEqual(language, other.language) + || !Util.areEqual(mimeType, other.mimeType) + || initializationData.size() != other.initializationData.size()) { + return false; + } + for (int i = 0; i < initializationData.size(); i++) { + if (!Arrays.equals(initializationData.get(i), other.initializationData.get(i))) { + return false; + } + } + return true; + } + + @TargetApi(16) + private static final void maybeSetStringV16(android.media.MediaFormat format, String key, + String value) { + if (value != null) { + format.setString(key, value); + } + } + + @TargetApi(16) + private static final void maybeSetIntegerV16(android.media.MediaFormat format, String key, + int value) { + if (value != NO_VALUE) { + format.setInteger(key, value); + } + } + + // Parcelable implementation. + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(trackId); + dest.writeString(mimeType); + dest.writeInt(bitrate); + dest.writeInt(maxInputSize); + dest.writeLong(durationUs); + dest.writeInt(width); + dest.writeInt(height); + dest.writeInt(rotationDegrees); + dest.writeFloat(pixelWidthHeightRatio); + dest.writeInt(channelCount); + dest.writeInt(sampleRate); + dest.writeString(language); + dest.writeLong(subsampleOffsetUs); + dest.writeList(initializationData); + dest.writeInt(adaptive ? 1 : 0); + dest.writeInt(maxWidth); + dest.writeInt(maxHeight); + dest.writeInt(pcmEncoding); + dest.writeInt(encoderDelay); + dest.writeInt(encoderPadding); + } + + public static final Creator CREATOR = new Creator() { + + @Override + public MediaFormat createFromParcel(Parcel in) { + return new MediaFormat(in); + } + + @Override + public MediaFormat[] newArray(int size) { + return new MediaFormat[size]; + } + + }; + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/MediaFormatHolder.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/MediaFormatHolder.java new file mode 100755 index 000000000..2092c8511 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/MediaFormatHolder.java @@ -0,0 +1,34 @@ +/* + * 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.messenger.exoplayer; + +import org.telegram.messenger.exoplayer.drm.DrmInitData; + +/** + * Holds a {@link MediaFormat} and corresponding drm scheme initialization data. + */ +public final class MediaFormatHolder { + + /** + * The format of the media. + */ + public MediaFormat format; + /** + * Initialization data for drm schemes supported by the media. Null if the media is not encrypted. + */ + public DrmInitData drmInitData; + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/ParserException.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/ParserException.java new file mode 100755 index 000000000..aa2bfe7f2 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/ParserException.java @@ -0,0 +1,41 @@ +/* + * 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.messenger.exoplayer; + +import java.io.IOException; + +/** + * Thrown when an error occurs parsing media data. + */ +public class ParserException extends IOException { + + public ParserException() { + super(); + } + + public ParserException(String message) { + super(message); + } + + public ParserException(Throwable cause) { + super(cause); + } + + public ParserException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/SampleHolder.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/SampleHolder.java new file mode 100755 index 000000000..e8ae0e36a --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/SampleHolder.java @@ -0,0 +1,153 @@ +/* + * 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.messenger.exoplayer; + +import java.nio.ByteBuffer; + +/** + * Holds sample data and corresponding metadata. + */ +public final class SampleHolder { + + /** + * Disallows buffer replacement. + */ + public static final int BUFFER_REPLACEMENT_MODE_DISABLED = 0; + + /** + * Allows buffer replacement using {@link ByteBuffer#allocate(int)}. + */ + public static final int BUFFER_REPLACEMENT_MODE_NORMAL = 1; + + /** + * Allows buffer replacement using {@link ByteBuffer#allocateDirect(int)}. + */ + public static final int BUFFER_REPLACEMENT_MODE_DIRECT = 2; + + public final CryptoInfo cryptoInfo; + + /** + * A buffer holding the sample data. + */ + public ByteBuffer data; + + /** + * The size of the sample in bytes. + */ + public int size; + + /** + * Flags that accompany the sample. A combination of {@link C#SAMPLE_FLAG_SYNC}, + * {@link C#SAMPLE_FLAG_ENCRYPTED} and {@link C#SAMPLE_FLAG_DECODE_ONLY}. + */ + public int flags; + + /** + * The time at which the sample should be presented. + */ + public long timeUs; + + private final int bufferReplacementMode; + + /** + * @param bufferReplacementMode Determines the behavior of {@link #ensureSpaceForWrite(int)}. One + * of {@link #BUFFER_REPLACEMENT_MODE_DISABLED}, {@link #BUFFER_REPLACEMENT_MODE_NORMAL} and + * {@link #BUFFER_REPLACEMENT_MODE_DIRECT}. + */ + public SampleHolder(int bufferReplacementMode) { + this.cryptoInfo = new CryptoInfo(); + this.bufferReplacementMode = bufferReplacementMode; + } + + /** + * Ensures that {@link #data} is large enough to accommodate a write of a given length at its + * current position. + *

+ * If the capacity of {@link #data} is sufficient this method does nothing. If the capacity is + * insufficient then an attempt is made to replace {@link #data} with a new {@link ByteBuffer} + * whose capacity is sufficient. Data up to the current position is copied to the new buffer. + * + * @param length The length of the write that must be accommodated, in bytes. + * @throws IllegalStateException If there is insufficient capacity to accommodate the write and + * the buffer replacement mode of the holder is {@link #BUFFER_REPLACEMENT_MODE_DISABLED}. + */ + public void ensureSpaceForWrite(int length) throws IllegalStateException { + if (data == null) { + data = createReplacementBuffer(length); + return; + } + // Check whether the current buffer is sufficient. + int capacity = data.capacity(); + int position = data.position(); + int requiredCapacity = position + length; + if (capacity >= requiredCapacity) { + return; + } + // Instantiate a new buffer if possible. + ByteBuffer newData = createReplacementBuffer(requiredCapacity); + // Copy data up to the current position from the old buffer to the new one. + if (position > 0) { + data.position(0); + data.limit(position); + newData.put(data); + } + // Set the new buffer. + data = newData; + } + + /** + * Returns whether {@link #flags} has {@link C#SAMPLE_FLAG_ENCRYPTED} set. + */ + public boolean isEncrypted() { + return (flags & C.SAMPLE_FLAG_ENCRYPTED) != 0; + } + + /** + * Returns whether {@link #flags} has {@link C#SAMPLE_FLAG_DECODE_ONLY} set. + */ + public boolean isDecodeOnly() { + return (flags & C.SAMPLE_FLAG_DECODE_ONLY) != 0; + } + + /** + * Returns whether {@link #flags} has {@link C#SAMPLE_FLAG_SYNC} set. + */ + public boolean isSyncFrame() { + return (flags & C.SAMPLE_FLAG_SYNC) != 0; + } + + /** + * Clears {@link #data}. Does nothing if {@link #data} is null. + */ + public void clearData() { + if (data != null) { + data.clear(); + } + } + + private ByteBuffer createReplacementBuffer(int requiredCapacity) { + if (bufferReplacementMode == BUFFER_REPLACEMENT_MODE_NORMAL) { + return ByteBuffer.allocate(requiredCapacity); + } else if (bufferReplacementMode == BUFFER_REPLACEMENT_MODE_DIRECT) { + return ByteBuffer.allocateDirect(requiredCapacity); + } else { + int currentCapacity = data == null ? 0 : data.capacity(); + throw new IllegalStateException("Buffer too small (" + currentCapacity + " < " + + requiredCapacity + ")"); + } + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/SampleSource.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/SampleSource.java new file mode 100755 index 000000000..255820c49 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/SampleSource.java @@ -0,0 +1,214 @@ +/* + * 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.messenger.exoplayer; + +import java.io.IOException; + +/** + * A source of media samples. + *

+ * A {@link SampleSource} may expose one or multiple tracks. The number of tracks and each track's + * media format can be queried using {@link SampleSourceReader#getTrackCount()} and + * {@link SampleSourceReader#getFormat(int)} respectively. + */ +public interface SampleSource { + + /** + * The end of stream has been reached. + */ + public static final int END_OF_STREAM = -1; + /** + * Neither a sample nor a format was read in full. This may be because insufficient data is + * buffered upstream. If multiple tracks are enabled, this return value may indicate that the + * next piece of data to be returned from the {@link SampleSource} corresponds to a different + * track than the one for which data was requested. + */ + public static final int NOTHING_READ = -2; + /** + * A sample was read. + */ + public static final int SAMPLE_READ = -3; + /** + * A format was read. + */ + public static final int FORMAT_READ = -4; + /** + * Returned from {@link SampleSourceReader#readDiscontinuity(int)} to indicate no discontinuity. + */ + public static final long NO_DISCONTINUITY = Long.MIN_VALUE; + + /** + * A consumer of samples should call this method to register themselves and gain access to the + * source through the returned {@link SampleSourceReader}. + *

+ * {@link SampleSourceReader#release()} should be called on the returned object when access is no + * longer required. + * + * @return A {@link SampleSourceReader} that provides access to the source. + */ + public SampleSourceReader register(); + + /** + * An interface providing read access to a {@link SampleSource}. + */ + public interface SampleSourceReader { + + /** + * If the source is currently having difficulty preparing or loading samples, then this method + * throws the underlying error. Otherwise does nothing. + * + * @throws IOException The underlying error. + */ + public void maybeThrowError() throws IOException; + + /** + * Prepares the source. + *

+ * Preparation may require reading from the data source (e.g. to determine the available tracks + * and formats). If insufficient data is available then the call will return {@code false} + * rather than block. The method can be called repeatedly until the return value indicates + * success. + * + * @param positionUs The player's current playback position. + * @return True if the source was prepared, false otherwise. + */ + public boolean prepare(long positionUs); + + /** + * Returns the number of tracks exposed by the source. + *

+ * This method should only be called after the source has been prepared. + * + * @return The number of tracks. + */ + public int getTrackCount(); + + /** + * Returns the format of the specified track. + *

+ * Note that whilst the format of a track will remain constant, the format of the actual media + * stream may change dynamically. An example of this is where the track is adaptive + * (i.e. @link {@link MediaFormat#adaptive} is true). Hence the track formats returned through + * this method should not be used to configure decoders. Decoder configuration should be + * performed using the formats obtained when reading the media stream through calls to + * {@link #readData(int, long, MediaFormatHolder, SampleHolder)}. + *

+ * This method should only be called after the source has been prepared. + * + * @param track The track index. + * @return The format of the specified track. + */ + public MediaFormat getFormat(int track); + + /** + * Enable the specified track. This allows the track's format and samples to be read from + * {@link #readData(int, long, MediaFormatHolder, SampleHolder)}. + *

+ * This method should only be called after the source has been prepared, and when the specified + * track is disabled. + * + * @param track The track to enable. + * @param positionUs The player's current playback position. + */ + public void enable(int track, long positionUs); + + /** + * Indicates to the source that it should still be buffering data for the specified track. + *

+ * This method should only be called when the specified track is enabled. + * + * @param track The track to continue buffering. + * @param positionUs The current playback position. + * @return True if the track has available samples, or if the end of the stream has been + * reached. False if more data needs to be buffered for samples to become available. + */ + public boolean continueBuffering(int track, long positionUs); + + /** + * Attempts to read a pending discontinuity from the source. + *

+ * This method should only be called when the specified track is enabled. + * + * @param track The track from which to read. + * @return If a discontinuity was read then the playback position after the discontinuity. Else + * {@link #NO_DISCONTINUITY}. + */ + public long readDiscontinuity(int track); + + /** + * Attempts to read a sample or a new format from the source. + *

+ * This method should only be called when the specified track is enabled. + *

+ * Note that where multiple tracks are enabled, {@link #NOTHING_READ} may be returned if the + * next piece of data to be read from the {@link SampleSource} corresponds to a different track + * than the one for which data was requested. + *

+ * This method will always return {@link #NOTHING_READ} in the case that there's a pending + * discontinuity to be read from {@link #readDiscontinuity(int)} for the specified track. + * + * @param track The track from which to read. + * @param positionUs The current playback position. + * @param formatHolder A {@link MediaFormatHolder} object to populate in the case of a new + * format. + * @param sampleHolder A {@link SampleHolder} object to populate in the case of a new sample. + * If the caller requires the sample data then it must ensure that {@link SampleHolder#data} + * references a valid output buffer. + * @return The result, which can be {@link #SAMPLE_READ}, {@link #FORMAT_READ}, + * {@link #NOTHING_READ} or {@link #END_OF_STREAM}. + */ + public int readData(int track, long positionUs, MediaFormatHolder formatHolder, + SampleHolder sampleHolder); + + /** + * Seeks to the specified time in microseconds. + *

+ * This method should only be called when at least one track is enabled. + * + * @param positionUs The seek position in microseconds. + */ + public void seekToUs(long positionUs); + + /** + * Returns an estimate of the position up to which data is buffered. + *

+ * This method should only be called when at least one track is enabled. + * + * @return An estimate of the absolute position in microseconds up to which data is buffered, + * or {@link TrackRenderer#END_OF_TRACK_US} if data is buffered to the end of the stream, + * or {@link TrackRenderer#UNKNOWN_TIME_US} if no estimate is available. + */ + public long getBufferedPositionUs(); + + /** + * Disable the specified track. + *

+ * This method should only be called when the specified track is enabled. + * + * @param track The track to disable. + */ + public void disable(int track); + + /** + * Releases the {@link SampleSourceReader}. + *

+ * This method should be called when access to the {@link SampleSource} is no longer required. + */ + public void release(); + + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/SampleSourceTrackRenderer.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/SampleSourceTrackRenderer.java new file mode 100755 index 000000000..fbc5ebc10 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/SampleSourceTrackRenderer.java @@ -0,0 +1,267 @@ +/* + * 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.messenger.exoplayer; + +import org.telegram.messenger.exoplayer.MediaCodecUtil.DecoderQueryException; +import org.telegram.messenger.exoplayer.SampleSource.SampleSourceReader; + +import java.io.IOException; +import java.util.Arrays; + +/** + * Base class for {@link TrackRenderer} implementations that render samples obtained from a + * {@link SampleSource}. + */ +public abstract class SampleSourceTrackRenderer extends TrackRenderer { + + private final SampleSourceReader[] sources; + + private int[] handledSourceIndices; + private int[] handledSourceTrackIndices; + + private SampleSourceReader enabledSource; + private int enabledSourceTrackIndex; + + private long durationUs; + + /** + * @param sources One or more upstream sources from which the renderer can obtain samples. + */ + public SampleSourceTrackRenderer(SampleSource... sources) { + this.sources = new SampleSourceReader[sources.length]; + for (int i = 0; i < sources.length; i++) { + this.sources[i] = sources[i].register(); + } + } + + @Override + protected final boolean doPrepare(long positionUs) throws ExoPlaybackException { + boolean allSourcesPrepared = true; + for (int i = 0; i < sources.length; i++) { + allSourcesPrepared &= sources[i].prepare(positionUs); + } + if (!allSourcesPrepared) { + return false; + } + // The sources are all prepared. + int totalSourceTrackCount = 0; + for (int i = 0; i < sources.length; i++) { + totalSourceTrackCount += sources[i].getTrackCount(); + } + long durationUs = 0; + int handledTrackCount = 0; + int[] handledSourceIndices = new int[totalSourceTrackCount]; + int[] handledTrackIndices = new int[totalSourceTrackCount]; + int sourceCount = sources.length; + for (int sourceIndex = 0; sourceIndex < sourceCount; sourceIndex++) { + SampleSourceReader source = sources[sourceIndex]; + int sourceTrackCount = source.getTrackCount(); + for (int trackIndex = 0; trackIndex < sourceTrackCount; trackIndex++) { + MediaFormat format = source.getFormat(trackIndex); + boolean handlesTrack; + try { + handlesTrack = handlesTrack(format); + } catch (DecoderQueryException e) { + throw new ExoPlaybackException(e); + } + if (handlesTrack) { + handledSourceIndices[handledTrackCount] = sourceIndex; + handledTrackIndices[handledTrackCount] = trackIndex; + handledTrackCount++; + if (durationUs == TrackRenderer.UNKNOWN_TIME_US) { + // We've already encountered a track for which the duration is unknown, so the media + // duration is unknown regardless of the duration of this track. + } else { + long trackDurationUs = format.durationUs; + if (trackDurationUs == TrackRenderer.UNKNOWN_TIME_US) { + durationUs = TrackRenderer.UNKNOWN_TIME_US; + } else if (trackDurationUs == TrackRenderer.MATCH_LONGEST_US) { + // Do nothing. + } else { + durationUs = Math.max(durationUs, trackDurationUs); + } + } + } + } + } + this.durationUs = durationUs; + this.handledSourceIndices = Arrays.copyOf(handledSourceIndices, handledTrackCount); + this.handledSourceTrackIndices = Arrays.copyOf(handledTrackIndices, handledTrackCount); + return true; + } + + @Override + protected void onEnabled(int track, long positionUs, boolean joining) + throws ExoPlaybackException { + positionUs = shiftInputPosition(positionUs); + enabledSource = sources[handledSourceIndices[track]]; + enabledSourceTrackIndex = handledSourceTrackIndices[track]; + enabledSource.enable(enabledSourceTrackIndex, positionUs); + onDiscontinuity(positionUs); + } + + @Override + protected final void seekTo(long positionUs) throws ExoPlaybackException { + positionUs = shiftInputPosition(positionUs); + enabledSource.seekToUs(positionUs); + checkForDiscontinuity(positionUs); + } + + @Override + protected final void doSomeWork(long positionUs, long elapsedRealtimeUs) + throws ExoPlaybackException { + positionUs = shiftInputPosition(positionUs); + boolean sourceIsReady = enabledSource.continueBuffering(enabledSourceTrackIndex, positionUs); + positionUs = checkForDiscontinuity(positionUs); + doSomeWork(positionUs, elapsedRealtimeUs, sourceIsReady); + } + + @Override + protected long getBufferedPositionUs() { + return enabledSource.getBufferedPositionUs(); + } + + @Override + protected long getDurationUs() { + return durationUs; + } + + @Override + protected void maybeThrowError() throws ExoPlaybackException { + if (enabledSource != null) { + maybeThrowError(enabledSource); + } else { + int sourceCount = sources.length; + for (int i = 0; i < sourceCount; i++) { + maybeThrowError(sources[i]); + } + } + } + + @Override + protected void onDisabled() throws ExoPlaybackException { + enabledSource.disable(enabledSourceTrackIndex); + enabledSource = null; + } + + @Override + protected void onReleased() throws ExoPlaybackException { + int sourceCount = sources.length; + for (int i = 0; i < sourceCount; i++) { + sources[i].release(); + } + } + + @Override + protected final int getTrackCount() { + return handledSourceTrackIndices.length; + } + + @Override + protected final MediaFormat getFormat(int track) { + SampleSourceReader source = sources[handledSourceIndices[track]]; + return source.getFormat(handledSourceTrackIndices[track]); + } + + /** + * Shifts positions passed to {@link #onEnabled(int, long, boolean)}, {@link #seekTo(long)} and + * {@link #doSomeWork(long, long)}. + *

+ * The default implementation does not modify the position. Except in very specific cases, + * subclasses should not override this method. + * + * @param positionUs The position in microseconds. + * @return The adjusted position in microseconds. + */ + protected long shiftInputPosition(long positionUs) { + return positionUs; + } + + // Methods to be called by subclasses. + + /** + * Reads from the enabled upstream source. + * + * @param positionUs The current playback position. + * @param formatHolder A {@link MediaFormatHolder} object to populate in the case of a new format. + * @param sampleHolder A {@link SampleHolder} object to populate in the case of a new sample. + * If the caller requires the sample data then it must ensure that {@link SampleHolder#data} + * references a valid output buffer. + * @return The result, which can be {@link SampleSource#SAMPLE_READ}, + * {@link SampleSource#FORMAT_READ}, {@link SampleSource#NOTHING_READ} or + * {@link SampleSource#END_OF_STREAM}. + */ + protected final int readSource(long positionUs, MediaFormatHolder formatHolder, + SampleHolder sampleHolder) { + return enabledSource.readData(enabledSourceTrackIndex, positionUs, formatHolder, sampleHolder); + } + + // Abstract methods. + + /** + * Returns whether this renderer is capable of handling the provided track. + * + * @param mediaFormat The format of the track. + * @return True if the renderer can handle the track, false otherwise. + * @throws DecoderQueryException Thrown if there was an error querying decoders. + */ + protected abstract boolean handlesTrack(MediaFormat mediaFormat) throws DecoderQueryException; + + /** + * Invoked when a discontinuity is encountered. Also invoked when the renderer is enabled, for + * convenience. + * + * @param positionUs The playback position after the discontinuity, or the position at which + * the renderer is being enabled. + * @throws ExoPlaybackException If an error occurs handling the discontinuity. + */ + protected abstract void onDiscontinuity(long positionUs) throws ExoPlaybackException; + + /** + * Called by {@link #doSomeWork(long, long)}. + * + * @param positionUs The current media time in microseconds, measured at the start of the + * current iteration of the rendering loop. + * @param elapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in microseconds, + * measured at the start of the current iteration of the rendering loop. + * @param sourceIsReady The result of the most recent call to + * {@link SampleSourceReader#continueBuffering(int, long)}. + * @throws ExoPlaybackException If an error occurs. + * @throws ExoPlaybackException + */ + protected abstract void doSomeWork(long positionUs, long elapsedRealtimeUs, boolean sourceIsReady) + throws ExoPlaybackException; + + // Private methods. + + private long checkForDiscontinuity(long positionUs) throws ExoPlaybackException { + long discontinuityPositionUs = enabledSource.readDiscontinuity(enabledSourceTrackIndex); + if (discontinuityPositionUs != SampleSource.NO_DISCONTINUITY) { + onDiscontinuity(discontinuityPositionUs); + return discontinuityPositionUs; + } + return positionUs; + } + + private void maybeThrowError(SampleSourceReader source) throws ExoPlaybackException { + try { + source.maybeThrowError(); + } catch (IOException e) { + throw new ExoPlaybackException(e); + } + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/SingleSampleSource.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/SingleSampleSource.java new file mode 100755 index 000000000..940265c68 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/SingleSampleSource.java @@ -0,0 +1,301 @@ +/* + * 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.messenger.exoplayer; + +import org.telegram.messenger.exoplayer.SampleSource.SampleSourceReader; +import org.telegram.messenger.exoplayer.upstream.DataSource; +import org.telegram.messenger.exoplayer.upstream.DataSpec; +import org.telegram.messenger.exoplayer.upstream.Loader; +import org.telegram.messenger.exoplayer.upstream.Loader.Loadable; +import org.telegram.messenger.exoplayer.util.Assertions; + +import android.net.Uri; +import android.os.Handler; +import android.os.SystemClock; + +import java.io.IOException; +import java.util.Arrays; + +/** + * A {@link SampleSource} that loads the data at a given {@link Uri} as a single sample. + */ +public final class SingleSampleSource implements SampleSource, SampleSourceReader, Loader.Callback, + Loadable { + + /** + * Interface definition for a callback to be notified of {@link SingleSampleSource} events. + */ + public interface EventListener { + + /** + * Invoked when an error occurs loading media data. + * + * @param sourceId The id of the reporting {@link SampleSource}. + * @param e The cause of the failure. + */ + void onLoadError(int sourceId, IOException e); + + } + + /** + * The default minimum number of times to retry loading data prior to failing. + */ + public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 3; + + /** + * The initial size of the allocation used to hold the sample data. + */ + private static final int INITIAL_SAMPLE_SIZE = 1; + + private static final int STATE_SEND_FORMAT = 0; + private static final int STATE_SEND_SAMPLE = 1; + private static final int STATE_END_OF_STREAM = 2; + + private final Uri uri; + private final DataSource dataSource; + private final MediaFormat format; + private final int minLoadableRetryCount; + private final Handler eventHandler; + private final EventListener eventListener; + private final int eventSourceId; + + private int state; + private byte[] sampleData; + private int sampleSize; + + private long pendingDiscontinuityPositionUs; + private boolean loadingFinished; + private Loader loader; + private IOException currentLoadableException; + private int currentLoadableExceptionCount; + private long currentLoadableExceptionTimestamp; + + public SingleSampleSource(Uri uri, DataSource dataSource, MediaFormat format) { + this(uri, dataSource, format, DEFAULT_MIN_LOADABLE_RETRY_COUNT); + } + + public SingleSampleSource(Uri uri, DataSource dataSource, MediaFormat format, + int minLoadableRetryCount) { + this(uri, dataSource, format, minLoadableRetryCount, null, null, 0); + } + + public SingleSampleSource(Uri uri, DataSource dataSource, MediaFormat format, + int minLoadableRetryCount, Handler eventHandler, EventListener eventListener, + int eventSourceId) { + this.uri = uri; + this.dataSource = dataSource; + this.format = format; + this.minLoadableRetryCount = minLoadableRetryCount; + this.eventHandler = eventHandler; + this.eventListener = eventListener; + this.eventSourceId = eventSourceId; + sampleData = new byte[INITIAL_SAMPLE_SIZE]; + } + + @Override + public SampleSourceReader register() { + return this; + } + + @Override + public boolean prepare(long positionUs) { + if (loader == null) { + loader = new Loader("Loader:" + format.mimeType); + } + return true; + } + + @Override + public int getTrackCount() { + return 1; + } + + @Override + public MediaFormat getFormat(int track) { + return format; + } + + @Override + public void enable(int track, long positionUs) { + state = STATE_SEND_FORMAT; + pendingDiscontinuityPositionUs = NO_DISCONTINUITY; + clearCurrentLoadableException(); + maybeStartLoading(); + } + + @Override + public boolean continueBuffering(int track, long positionUs) { + maybeStartLoading(); + return loadingFinished; + } + + @Override + public void maybeThrowError() throws IOException { + if (currentLoadableException != null && currentLoadableExceptionCount > minLoadableRetryCount) { + throw currentLoadableException; + } + } + + @Override + public long readDiscontinuity(int track) { + long discontinuityPositionUs = pendingDiscontinuityPositionUs; + pendingDiscontinuityPositionUs = NO_DISCONTINUITY; + return discontinuityPositionUs; + } + + @Override + public int readData(int track, long positionUs, MediaFormatHolder formatHolder, + SampleHolder sampleHolder) { + if (state == STATE_END_OF_STREAM) { + return END_OF_STREAM; + } else if (state == STATE_SEND_FORMAT) { + formatHolder.format = format; + state = STATE_SEND_SAMPLE; + return FORMAT_READ; + } + + Assertions.checkState(state == STATE_SEND_SAMPLE); + if (!loadingFinished) { + return NOTHING_READ; + } else { + sampleHolder.timeUs = 0; + sampleHolder.size = sampleSize; + sampleHolder.flags = C.SAMPLE_FLAG_SYNC; + sampleHolder.ensureSpaceForWrite(sampleHolder.size); + sampleHolder.data.put(sampleData, 0, sampleSize); + state = STATE_END_OF_STREAM; + return SAMPLE_READ; + } + } + + @Override + public void seekToUs(long positionUs) { + if (state == STATE_END_OF_STREAM) { + pendingDiscontinuityPositionUs = positionUs; + state = STATE_SEND_SAMPLE; + } + } + + @Override + public long getBufferedPositionUs() { + return loadingFinished ? TrackRenderer.END_OF_TRACK_US : 0; + } + + @Override + public void disable(int track) { + state = STATE_END_OF_STREAM; + } + + @Override + public void release() { + if (loader != null) { + loader.release(); + loader = null; + } + } + + // Private methods. + + private void maybeStartLoading() { + if (loadingFinished || state == STATE_END_OF_STREAM || loader.isLoading()) { + return; + } + if (currentLoadableException != null) { + long elapsedMillis = SystemClock.elapsedRealtime() - currentLoadableExceptionTimestamp; + if (elapsedMillis < getRetryDelayMillis(currentLoadableExceptionCount)) { + return; + } + currentLoadableException = null; + } + loader.startLoading(this, this); + } + + private void clearCurrentLoadableException() { + currentLoadableException = null; + currentLoadableExceptionCount = 0; + } + + private long getRetryDelayMillis(long errorCount) { + return Math.min((errorCount - 1) * 1000, 5000); + } + + // Loader.Callback implementation. + + @Override + public void onLoadCompleted(Loadable loadable) { + loadingFinished = true; + clearCurrentLoadableException(); + } + + @Override + public void onLoadCanceled(Loadable loadable) { + // Never happens. + } + + @Override + public void onLoadError(Loadable loadable, IOException e) { + currentLoadableException = e; + currentLoadableExceptionCount++; + currentLoadableExceptionTimestamp = SystemClock.elapsedRealtime(); + notifyLoadError(e); + maybeStartLoading(); + } + + // Loadable implementation. + + @Override + public void cancelLoad() { + // Never happens. + } + + @Override + public boolean isLoadCanceled() { + return false; + } + + @Override + public void load() throws IOException, InterruptedException { + // We always load from the beginning, so reset the sampleSize to 0. + sampleSize = 0; + try { + // Create and open the input. + dataSource.open(new DataSpec(uri)); + // Load the sample data. + int result = 0; + while (result != C.RESULT_END_OF_INPUT) { + sampleSize += result; + if (sampleSize == sampleData.length) { + sampleData = Arrays.copyOf(sampleData, sampleData.length * 2); + } + result = dataSource.read(sampleData, sampleSize, sampleData.length - sampleSize); + } + } finally { + dataSource.close(); + } + } + + private void notifyLoadError(final IOException e) { + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onLoadError(eventSourceId, e); + } + }); + } + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/StandaloneMediaClock.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/StandaloneMediaClock.java new file mode 100755 index 000000000..af1133ede --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/StandaloneMediaClock.java @@ -0,0 +1,76 @@ +/* + * 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.messenger.exoplayer; + +import android.os.SystemClock; + +/** + * A standalone {@link MediaClock}. The clock can be started, stopped and its time can be set and + * retrieved. When started, this clock is based on {@link SystemClock#elapsedRealtime()}. + */ +/* package */ final class StandaloneMediaClock implements MediaClock { + + private boolean started; + + /** + * The media time when the clock was last set or stopped. + */ + private long positionUs; + + /** + * The difference between {@link SystemClock#elapsedRealtime()} and {@link #positionUs} + * when the clock was last set or started. + */ + private long deltaUs; + + /** + * Starts the clock. Does nothing if the clock is already started. + */ + public void start() { + if (!started) { + started = true; + deltaUs = elapsedRealtimeMinus(positionUs); + } + } + + /** + * Stops the clock. Does nothing if the clock is already stopped. + */ + public void stop() { + if (started) { + positionUs = elapsedRealtimeMinus(deltaUs); + started = false; + } + } + + /** + * @param timeUs The position to set in microseconds. + */ + public void setPositionUs(long timeUs) { + this.positionUs = timeUs; + deltaUs = elapsedRealtimeMinus(timeUs); + } + + @Override + public long getPositionUs() { + return started ? elapsedRealtimeMinus(deltaUs) : positionUs; + } + + private long elapsedRealtimeMinus(long toSubtractUs) { + return SystemClock.elapsedRealtime() * 1000 - toSubtractUs; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/TimeRange.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/TimeRange.java new file mode 100755 index 000000000..fa6e0a4ba --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/TimeRange.java @@ -0,0 +1,203 @@ +/* + * 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.messenger.exoplayer; + +import org.telegram.messenger.exoplayer.util.Clock; + +import android.os.SystemClock; + +/** + * A container to store a start and end time in microseconds. + */ +public interface TimeRange { + + /** + * Whether the range is static, meaning repeated calls to {@link #getCurrentBoundsMs(long[])} + * or {@link #getCurrentBoundsUs(long[])} will return identical results. + * + * @return Whether the range is static. + */ + public boolean isStatic(); + + /** + * Returns the start and end times (in milliseconds) of the TimeRange in the provided array, + * or creates a new one. + * + * @param out An array to store the start and end times; can be null. + * @return An array containing the start time (index 0) and end time (index 1) in milliseconds. + */ + public long[] getCurrentBoundsMs(long[] out); + + /** + * Returns the start and end times (in microseconds) of the TimeRange in the provided array, + * or creates a new one. + * + * @param out An array to store the start and end times; can be null. + * @return An array containing the start time (index 0) and end time (index 1) in microseconds. + */ + public long[] getCurrentBoundsUs(long[] out); + + /** + * A static {@link TimeRange}. + */ + public static final class StaticTimeRange implements TimeRange { + + private final long startTimeUs; + private final long endTimeUs; + + /** + * @param startTimeUs The beginning of the range. + * @param endTimeUs The end of the range. + */ + public StaticTimeRange(long startTimeUs, long endTimeUs) { + this.startTimeUs = startTimeUs; + this.endTimeUs = endTimeUs; + } + + @Override + public boolean isStatic() { + return true; + } + + @Override + public long[] getCurrentBoundsMs(long[] out) { + out = getCurrentBoundsUs(out); + out[0] /= 1000; + out[1] /= 1000; + return out; + } + + @Override + public long[] getCurrentBoundsUs(long[] out) { + if (out == null || out.length < 2) { + out = new long[2]; + } + out[0] = startTimeUs; + out[1] = endTimeUs; + return out; + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + (int) startTimeUs; + result = 31 * result + (int) endTimeUs; + return result; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + StaticTimeRange other = (StaticTimeRange) obj; + return other.startTimeUs == startTimeUs + && other.endTimeUs == endTimeUs; + } + + } + + /** + * A dynamic {@link TimeRange}. + */ + public static final class DynamicTimeRange implements TimeRange { + + private final long minStartTimeUs; + private final long maxEndTimeUs; + private final long elapsedRealtimeAtStartUs; + private final long bufferDepthUs; + private final Clock systemClock; + + /** + * @param minStartTimeUs A lower bound on the beginning of the range. + * @param maxEndTimeUs An upper bound on the end of the range. + * @param elapsedRealtimeAtStartUs The value of {@link SystemClock#elapsedRealtime()}, + * multiplied by 1000, corresponding to a media time of zero. + * @param bufferDepthUs The buffer depth of the media, or -1. + * @param systemClock A system clock. + */ + public DynamicTimeRange(long minStartTimeUs, long maxEndTimeUs, long elapsedRealtimeAtStartUs, + long bufferDepthUs, Clock systemClock) { + this.minStartTimeUs = minStartTimeUs; + this.maxEndTimeUs = maxEndTimeUs; + this.elapsedRealtimeAtStartUs = elapsedRealtimeAtStartUs; + this.bufferDepthUs = bufferDepthUs; + this.systemClock = systemClock; + } + + @Override + public boolean isStatic() { + return false; + } + + @Override + public long[] getCurrentBoundsMs(long[] out) { + out = getCurrentBoundsUs(out); + out[0] /= 1000; + out[1] /= 1000; + return out; + } + + @Override + public long[] getCurrentBoundsUs(long[] out) { + if (out == null || out.length < 2) { + out = new long[2]; + } + // Don't allow the end time to be greater than the total elapsed time. + long currentEndTimeUs = Math.min(maxEndTimeUs, + (systemClock.elapsedRealtime() * 1000) - elapsedRealtimeAtStartUs); + long currentStartTimeUs = minStartTimeUs; + if (bufferDepthUs != -1) { + // Don't allow the start time to be less than the current end time minus the buffer depth. + currentStartTimeUs = Math.max(currentStartTimeUs, + currentEndTimeUs - bufferDepthUs); + } + out[0] = currentStartTimeUs; + out[1] = currentEndTimeUs; + return out; + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + (int) minStartTimeUs; + result = 31 * result + (int) maxEndTimeUs; + result = 31 * result + (int) elapsedRealtimeAtStartUs; + result = 31 * result + (int) bufferDepthUs; + return result; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + DynamicTimeRange other = (DynamicTimeRange) obj; + return other.minStartTimeUs == minStartTimeUs + && other.maxEndTimeUs == maxEndTimeUs + && other.elapsedRealtimeAtStartUs == elapsedRealtimeAtStartUs + && other.bufferDepthUs == bufferDepthUs; + } + + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/TrackRenderer.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/TrackRenderer.java new file mode 100755 index 000000000..e25ee92d4 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/TrackRenderer.java @@ -0,0 +1,371 @@ +/* + * 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.messenger.exoplayer; + +import org.telegram.messenger.exoplayer.ExoPlayer.ExoPlayerComponent; +import org.telegram.messenger.exoplayer.util.Assertions; + +/** + * Renders a single component of media. + * + *

Internally, a renderer's lifecycle is managed by the owning {@link ExoPlayer}. The player + * will transition its renderers through various states as the overall playback state changes. The + * valid state transitions are shown below, annotated with the methods that are invoked during each + * transition. + *

TrackRenderer state transitions

+ */ +public abstract class TrackRenderer implements ExoPlayerComponent { + + /** + * Represents an unknown time or duration. Equal to {@link C#UNKNOWN_TIME_US}. + */ + public static final long UNKNOWN_TIME_US = C.UNKNOWN_TIME_US; // -1 + /** + * Represents a time or duration that should match the duration of the longest track whose + * duration is known. Equal to {@link C#MATCH_LONGEST_US}. + */ + public static final long MATCH_LONGEST_US = C.MATCH_LONGEST_US; // -2 + /** + * Represents the time of the end of the track. + */ + public static final long END_OF_TRACK_US = -3; + + /** + * The renderer has been released and should not be used. + */ + protected static final int STATE_RELEASED = -1; + /** + * The renderer has not yet been prepared. + */ + protected static final int STATE_UNPREPARED = 0; + /** + * The renderer has completed necessary preparation. Preparation may include, for example, + * reading the header of a media file to determine the track format and duration. + *

+ * The renderer should not hold scarce or expensive system resources (e.g. media decoders) and + * should not be actively buffering media data when in this state. + */ + protected static final int STATE_PREPARED = 1; + /** + * The renderer is enabled. It should either be ready to be started, or be actively working + * towards this state (e.g. a renderer in this state will typically hold any resources that it + * requires, such as media decoders, and will have buffered or be buffering any media data that + * is required to start playback). + */ + protected static final int STATE_ENABLED = 2; + /** + * The renderer is started. Calls to {@link #doSomeWork(long, long)} should cause the media to be + * rendered. + */ + protected static final int STATE_STARTED = 3; + + private int state; + + /** + * If the renderer advances its own playback position then this method returns a corresponding + * {@link MediaClock}. If provided, the player will use the returned {@link MediaClock} as its + * source of time during playback. A player may have at most one renderer that returns a + * {@link MediaClock} from this method. + * + * @return The {@link MediaClock} tracking the playback position of the renderer, or null. + */ + protected MediaClock getMediaClock() { + return null; + } + + /** + * Returns the current state of the renderer. + * + * @return The current state (one of the STATE_* constants). + */ + protected final int getState() { + return state; + } + + /** + * Prepares the renderer. This method is non-blocking, and hence it may be necessary to call it + * more than once in order to transition the renderer into the prepared state. + * + * @param positionUs The player's current playback position. + * @return The current state (one of the STATE_* constants), for convenience. + * @throws ExoPlaybackException If an error occurs. + */ + /* package */ final int prepare(long positionUs) throws ExoPlaybackException { + Assertions.checkState(state == STATE_UNPREPARED); + state = doPrepare(positionUs) ? STATE_PREPARED : STATE_UNPREPARED; + return state; + } + + /** + * Invoked to make progress when the renderer is in the {@link #STATE_UNPREPARED} state. This + * method will be called repeatedly until {@code true} is returned. + *

+ * This method should return quickly, and should not block if the renderer is currently unable to + * make any useful progress. + * + * @param positionUs The player's current playback position. + * @return True if the renderer is now prepared. False otherwise. + * @throws ExoPlaybackException If an error occurs. + */ + protected abstract boolean doPrepare(long positionUs) throws ExoPlaybackException; + + /** + * Returns the number of tracks exposed by the renderer. + *

+ * This method may be called when the renderer is in the following states: + * {@link #STATE_PREPARED}, {@link #STATE_ENABLED}, {@link #STATE_STARTED} + * + * @return The number of tracks. + */ + protected abstract int getTrackCount(); + + /** + * Returns the format of the specified track. + *

+ * This method may be called when the renderer is in the following states: + * {@link #STATE_PREPARED}, {@link #STATE_ENABLED}, {@link #STATE_STARTED} + * + * @param track The track index. + * @return The format of the specified track. + */ + protected abstract MediaFormat getFormat(int track); + + /** + * Enable the renderer for a specified track. + * + * @param track The track for which the renderer is being enabled. + * @param positionUs The player's current position. + * @param joining Whether this renderer is being enabled to join an ongoing playback. + * @throws ExoPlaybackException If an error occurs. + */ + /* package */ final void enable(int track, long positionUs, boolean joining) + throws ExoPlaybackException { + Assertions.checkState(state == STATE_PREPARED); + state = STATE_ENABLED; + onEnabled(track, positionUs, joining); + } + + /** + * Called when the renderer is enabled. + *

+ * The default implementation is a no-op. + * + * @param track The track for which the renderer is being enabled. + * @param positionUs The player's current position. + * @param joining Whether this renderer is being enabled to join an ongoing playback. + * @throws ExoPlaybackException If an error occurs. + */ + protected void onEnabled(int track, long positionUs, boolean joining) + throws ExoPlaybackException { + // Do nothing. + } + + /** + * Starts the renderer, meaning that calls to {@link #doSomeWork(long, long)} will cause the + * track to be rendered. + * + * @throws ExoPlaybackException If an error occurs. + */ + /* package */ final void start() throws ExoPlaybackException { + Assertions.checkState(state == STATE_ENABLED); + state = STATE_STARTED; + onStarted(); + } + + /** + * Called when the renderer is started. + *

+ * The default implementation is a no-op. + * + * @throws ExoPlaybackException If an error occurs. + */ + protected void onStarted() throws ExoPlaybackException { + // Do nothing. + } + + /** + * Stops the renderer. + * + * @throws ExoPlaybackException If an error occurs. + */ + /* package */ final void stop() throws ExoPlaybackException { + Assertions.checkState(state == STATE_STARTED); + state = STATE_ENABLED; + onStopped(); + } + + /** + * Called when the renderer is stopped. + *

+ * The default implementation is a no-op. + * + * @throws ExoPlaybackException If an error occurs. + */ + protected void onStopped() throws ExoPlaybackException { + // Do nothing. + } + + /** + * Disable the renderer. + * + * @throws ExoPlaybackException If an error occurs. + */ + /* package */ final void disable() throws ExoPlaybackException { + Assertions.checkState(state == STATE_ENABLED); + state = STATE_PREPARED; + onDisabled(); + } + + /** + * Called when the renderer is disabled. + *

+ * The default implementation is a no-op. + * + * @throws ExoPlaybackException If an error occurs. + */ + protected void onDisabled() throws ExoPlaybackException { + // Do nothing. + } + + /** + * Releases the renderer. + * + * @throws ExoPlaybackException If an error occurs. + */ + /* package */ final void release() throws ExoPlaybackException { + Assertions.checkState(state != STATE_ENABLED + && state != STATE_STARTED + && state != STATE_RELEASED); + state = STATE_RELEASED; + onReleased(); + } + + /** + * Called when the renderer is released. + *

+ * The default implementation is a no-op. + * + * @throws ExoPlaybackException If an error occurs. + */ + protected void onReleased() throws ExoPlaybackException { + // Do nothing. + } + + /** + * Whether the renderer is ready for the {@link ExoPlayer} instance to transition to + * {@link ExoPlayer#STATE_ENDED}. The player will make this transition as soon as {@code true} is + * returned by all of its {@link TrackRenderer}s. + *

+ * This method may be called when the renderer is in the following states: + * {@link #STATE_ENABLED}, {@link #STATE_STARTED} + * + * @return Whether the renderer is ready for the player to transition to the ended state. + */ + protected abstract boolean isEnded(); + + /** + * Whether the renderer is able to immediately render media from the current position. + *

+ * If the renderer is in the {@link #STATE_STARTED} state then returning true indicates that the + * renderer has everything that it needs to continue playback. Returning false indicates that + * the player should pause until the renderer is ready. + *

+ * If the renderer is in the {@link #STATE_ENABLED} state then returning true indicates that the + * renderer is ready for playback to be started. Returning false indicates that it is not. + *

+ * This method may be called when the renderer is in the following states: + * {@link #STATE_ENABLED}, {@link #STATE_STARTED} + * + * @return True if the renderer is ready to render media. False otherwise. + */ + protected abstract boolean isReady(); + + /** + * Invoked to make progress when the renderer is in the {@link #STATE_ENABLED} or + * {@link #STATE_STARTED} states. + *

+ * If the renderer's state is {@link #STATE_STARTED}, then repeated calls to this method should + * cause the media track to be rendered. If the state is {@link #STATE_ENABLED}, then repeated + * calls should make progress towards getting the renderer into a position where it is ready to + * render the track. + *

+ * This method should return quickly, and should not block if the renderer is currently unable to + * make any useful progress. + *

+ * This method may be called when the renderer is in the following states: + * {@link #STATE_ENABLED}, {@link #STATE_STARTED} + * + * @param positionUs The current media time in microseconds, measured at the start of the + * current iteration of the rendering loop. + * @param elapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in microseconds, + * measured at the start of the current iteration of the rendering loop. + * @throws ExoPlaybackException If an error occurs. + */ + protected abstract void doSomeWork(long positionUs, long elapsedRealtimeUs) + throws ExoPlaybackException; + + /** + * Throws an error that's preventing the renderer from making progress or buffering more data at + * this point in time. + * + * @throws ExoPlaybackException An error that's preventing the renderer from making progress or + * buffering more data. + */ + protected abstract void maybeThrowError() throws ExoPlaybackException; + + /** + * Returns the duration of the media being rendered. + *

+ * This method may be called when the renderer is in the following states: + * {@link #STATE_PREPARED}, {@link #STATE_ENABLED}, {@link #STATE_STARTED} + * + * @return The duration of the track in microseconds, or {@link #MATCH_LONGEST_US} if + * the track's duration should match that of the longest track whose duration is known, or + * or {@link #UNKNOWN_TIME_US} if the duration is not known. + */ + protected abstract long getDurationUs(); + + /** + * Returns an estimate of the absolute position in microseconds up to which data is buffered. + *

+ * This method may be called when the renderer is in the following states: + * {@link #STATE_ENABLED}, {@link #STATE_STARTED} + * + * @return An estimate of the absolute position in microseconds up to which data is buffered, + * or {@link #END_OF_TRACK_US} if the track is fully buffered, or {@link #UNKNOWN_TIME_US} if + * no estimate is available. + */ + protected abstract long getBufferedPositionUs(); + + /** + * Seeks to a specified time in the track. + *

+ * This method may be called when the renderer is in the following states: + * {@link #STATE_ENABLED} + * + * @param positionUs The desired playback position in microseconds. + * @throws ExoPlaybackException If an error occurs. + */ + protected abstract void seekTo(long positionUs) throws ExoPlaybackException; + + @Override + public void handleMessage(int what, Object object) throws ExoPlaybackException { + // Do nothing. + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/VideoFrameReleaseTimeHelper.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/VideoFrameReleaseTimeHelper.java new file mode 100755 index 000000000..81788d169 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/VideoFrameReleaseTimeHelper.java @@ -0,0 +1,305 @@ +/* + * 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.messenger.exoplayer; + +import android.annotation.TargetApi; +import android.content.Context; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Message; +import android.view.Choreographer; +import android.view.Choreographer.FrameCallback; +import android.view.WindowManager; + +/** + * Makes a best effort to adjust frame release timestamps for a smoother visual result. + */ +@TargetApi(16) +public final class VideoFrameReleaseTimeHelper { + + private static final long CHOREOGRAPHER_SAMPLE_DELAY_MILLIS = 500; + private static final long MAX_ALLOWED_DRIFT_NS = 20000000; + + private static final long VSYNC_OFFSET_PERCENTAGE = 80; + private static final int MIN_FRAMES_FOR_ADJUSTMENT = 6; + + private final VSyncSampler vsyncSampler; + private final boolean useDefaultDisplayVsync; + private final long vsyncDurationNs; + private final long vsyncOffsetNs; + + private long lastFramePresentationTimeUs; + private long adjustedLastFrameTimeNs; + private long pendingAdjustedFrameTimeNs; + + private boolean haveSync; + private long syncUnadjustedReleaseTimeNs; + private long syncFramePresentationTimeNs; + private long frameCount; + + /** + * Constructs an instance that smoothes frame release but does not snap release to the default + * display's vsync signal. + */ + public VideoFrameReleaseTimeHelper() { + this(-1, false); + } + + /** + * Constructs an instance that smoothes frame release and snaps release to the default display's + * vsync signal. + * + * @param context A context from which information about the default display can be retrieved. + */ + public VideoFrameReleaseTimeHelper(Context context) { + this(getDefaultDisplayRefreshRate(context), true); + } + + private VideoFrameReleaseTimeHelper(float defaultDisplayRefreshRate, + boolean useDefaultDisplayVsync) { + this.useDefaultDisplayVsync = useDefaultDisplayVsync; + if (useDefaultDisplayVsync) { + vsyncSampler = VSyncSampler.getInstance(); + vsyncDurationNs = (long) (1000000000d / defaultDisplayRefreshRate); + vsyncOffsetNs = (vsyncDurationNs * VSYNC_OFFSET_PERCENTAGE) / 100; + } else { + vsyncSampler = null; + vsyncDurationNs = -1; + vsyncOffsetNs = -1; + } + } + + /** + * Enables the helper. + */ + public void enable() { + haveSync = false; + if (useDefaultDisplayVsync) { + vsyncSampler.addObserver(); + } + } + + /** + * Disables the helper. + */ + public void disable() { + if (useDefaultDisplayVsync) { + vsyncSampler.removeObserver(); + } + } + + /** + * Called to make a fine-grained adjustment to a frame release time. + * + * @param framePresentationTimeUs The frame's media presentation time, in microseconds. + * @param unadjustedReleaseTimeNs The frame's unadjusted release time, in nanoseconds and in + * the same time base as {@link System#nanoTime()}. + * @return An adjusted release time for the frame, in nanoseconds and in the same time base as + * {@link System#nanoTime()}. + */ + public long adjustReleaseTime(long framePresentationTimeUs, long unadjustedReleaseTimeNs) { + long framePresentationTimeNs = framePresentationTimeUs * 1000; + + // Until we know better, the adjustment will be a no-op. + long adjustedFrameTimeNs = framePresentationTimeNs; + long adjustedReleaseTimeNs = unadjustedReleaseTimeNs; + + if (haveSync) { + // See if we've advanced to the next frame. + if (framePresentationTimeUs != lastFramePresentationTimeUs) { + frameCount++; + adjustedLastFrameTimeNs = pendingAdjustedFrameTimeNs; + } + if (frameCount >= MIN_FRAMES_FOR_ADJUSTMENT) { + // We're synced and have waited the required number of frames to apply an adjustment. + // Calculate the average frame time across all the frames we've seen since the last sync. + // This will typically give us a frame rate at a finer granularity than the frame times + // themselves (which often only have millisecond granularity). + long averageFrameDurationNs = (framePresentationTimeNs - syncFramePresentationTimeNs) + / frameCount; + // Project the adjusted frame time forward using the average. + long candidateAdjustedFrameTimeNs = adjustedLastFrameTimeNs + averageFrameDurationNs; + + if (isDriftTooLarge(candidateAdjustedFrameTimeNs, unadjustedReleaseTimeNs)) { + haveSync = false; + } else { + adjustedFrameTimeNs = candidateAdjustedFrameTimeNs; + adjustedReleaseTimeNs = syncUnadjustedReleaseTimeNs + adjustedFrameTimeNs + - syncFramePresentationTimeNs; + } + } else { + // We're synced but haven't waited the required number of frames to apply an adjustment. + // Check drift anyway. + if (isDriftTooLarge(framePresentationTimeNs, unadjustedReleaseTimeNs)) { + haveSync = false; + } + } + } + + // If we need to sync, do so now. + if (!haveSync) { + syncFramePresentationTimeNs = framePresentationTimeNs; + syncUnadjustedReleaseTimeNs = unadjustedReleaseTimeNs; + frameCount = 0; + haveSync = true; + onSynced(); + } + + lastFramePresentationTimeUs = framePresentationTimeUs; + pendingAdjustedFrameTimeNs = adjustedFrameTimeNs; + + if (vsyncSampler == null || vsyncSampler.sampledVsyncTimeNs == 0) { + return adjustedReleaseTimeNs; + } + + // Find the timestamp of the closest vsync. This is the vsync that we're targeting. + long snappedTimeNs = closestVsync(adjustedReleaseTimeNs, + vsyncSampler.sampledVsyncTimeNs, vsyncDurationNs); + // Apply an offset so that we release before the target vsync, but after the previous one. + return snappedTimeNs - vsyncOffsetNs; + } + + protected void onSynced() { + // Do nothing. + } + + private boolean isDriftTooLarge(long frameTimeNs, long releaseTimeNs) { + long elapsedFrameTimeNs = frameTimeNs - syncFramePresentationTimeNs; + long elapsedReleaseTimeNs = releaseTimeNs - syncUnadjustedReleaseTimeNs; + return Math.abs(elapsedReleaseTimeNs - elapsedFrameTimeNs) > MAX_ALLOWED_DRIFT_NS; + } + + private static long closestVsync(long releaseTime, long sampledVsyncTime, long vsyncDuration) { + long vsyncCount = (releaseTime - sampledVsyncTime) / vsyncDuration; + long snappedTimeNs = sampledVsyncTime + (vsyncDuration * vsyncCount); + long snappedBeforeNs; + long snappedAfterNs; + if (releaseTime <= snappedTimeNs) { + snappedBeforeNs = snappedTimeNs - vsyncDuration; + snappedAfterNs = snappedTimeNs; + } else { + snappedBeforeNs = snappedTimeNs; + snappedAfterNs = snappedTimeNs + vsyncDuration; + } + long snappedAfterDiff = snappedAfterNs - releaseTime; + long snappedBeforeDiff = releaseTime - snappedBeforeNs; + return snappedAfterDiff < snappedBeforeDiff ? snappedAfterNs : snappedBeforeNs; + } + + private static float getDefaultDisplayRefreshRate(Context context) { + WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + return manager.getDefaultDisplay().getRefreshRate(); + } + + /** + * Manages the lifecycle of a single {@link Choreographer} to be shared among all + * {@link VideoFrameReleaseTimeHelper} instances. This is done to avoid a bug fixed in platform + * API version 23 that causes resource leakage. See [Internal: b/12455729]. + */ + private static final class VSyncSampler implements FrameCallback, Handler.Callback { + + public volatile long sampledVsyncTimeNs; + + private static final int CREATE_CHOREOGRAPHER = 0; + private static final int MSG_ADD_OBSERVER = 1; + private static final int MSG_REMOVE_OBSERVER = 2; + + private static final VSyncSampler INSTANCE = new VSyncSampler(); + + private final Handler handler; + private final HandlerThread choreographerOwnerThread; + private Choreographer choreographer; + private int observerCount; + + public static VSyncSampler getInstance() { + return INSTANCE; + } + + private VSyncSampler() { + choreographerOwnerThread = new HandlerThread("ChoreographerOwner:Handler"); + choreographerOwnerThread.start(); + handler = new Handler(choreographerOwnerThread.getLooper(), this); + handler.sendEmptyMessage(CREATE_CHOREOGRAPHER); + } + + /** + * Tells the {@link VSyncSampler} that there is a new {@link VideoFrameReleaseTimeHelper} + * instance observing the currentSampledVsyncTimeNs value. As a consequence, if necessary, it + * will register itself as a {@code doFrame} callback listener. + */ + public void addObserver() { + handler.sendEmptyMessage(MSG_ADD_OBSERVER); + } + + /** + * Counterpart of {@code addNewObservingHelper}. This method should be called once the observer + * no longer needs to read {@link #sampledVsyncTimeNs} + */ + public void removeObserver() { + handler.sendEmptyMessage(MSG_REMOVE_OBSERVER); + } + + @Override + public void doFrame(long vsyncTimeNs) { + sampledVsyncTimeNs = vsyncTimeNs; + choreographer.postFrameCallbackDelayed(this, CHOREOGRAPHER_SAMPLE_DELAY_MILLIS); + } + + @Override + public boolean handleMessage(Message message) { + switch (message.what) { + case CREATE_CHOREOGRAPHER: { + createChoreographerInstanceInternal(); + return true; + } + case MSG_ADD_OBSERVER: { + addObserverInternal(); + return true; + } + case MSG_REMOVE_OBSERVER: { + removeObserverInternal(); + return true; + } + default: { + return false; + } + } + } + + + private void createChoreographerInstanceInternal() { + choreographer = Choreographer.getInstance(); + } + + private void addObserverInternal() { + observerCount++; + if (observerCount == 1) { + choreographer.postFrameCallback(this); + } + } + + private void removeObserverInternal() { + observerCount--; + if (observerCount == 0) { + choreographer.removeFrameCallback(this); + sampledVsyncTimeNs = 0; + } + } + + + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/audio/AudioCapabilities.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/audio/AudioCapabilities.java new file mode 100755 index 000000000..2407df368 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/audio/AudioCapabilities.java @@ -0,0 +1,122 @@ +/* + * 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.messenger.exoplayer.audio; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.media.AudioFormat; +import android.media.AudioManager; + +import java.util.Arrays; + +/** + * Represents the set of audio formats a device is capable of playing back. + */ +@TargetApi(21) +public final class AudioCapabilities { + + /** + * The minimum audio capabilities supported by all devices. + */ + public static final AudioCapabilities DEFAULT_AUDIO_CAPABILITIES = + new AudioCapabilities(new int[] {AudioFormat.ENCODING_PCM_16BIT}, 2); + + /** + * Gets the current audio capabilities. Note that to be notified when audio capabilities change, + * you can create an instance of {@link AudioCapabilitiesReceiver} and register a listener. + * + * @param context Context for receiving the initial broadcast. + * @return Current audio capabilities for the device. + */ + @SuppressWarnings("InlinedApi") + public static AudioCapabilities getCapabilities(Context context) { + return getCapabilities( + context.registerReceiver(null, new IntentFilter(AudioManager.ACTION_HDMI_AUDIO_PLUG))); + } + + @SuppressLint("InlinedApi") + /* package */ static AudioCapabilities getCapabilities(Intent intent) { + if (intent == null || intent.getIntExtra(AudioManager.EXTRA_AUDIO_PLUG_STATE, 0) == 0) { + return DEFAULT_AUDIO_CAPABILITIES; + } + return new AudioCapabilities(intent.getIntArrayExtra(AudioManager.EXTRA_ENCODINGS), + intent.getIntExtra(AudioManager.EXTRA_MAX_CHANNEL_COUNT, 0)); + } + + private final int[] supportedEncodings; + private final int maxChannelCount; + + /** + * Constructs new audio capabilities based on a set of supported encodings and a maximum channel + * count. + * + * @param supportedEncodings Supported audio encodings from {@link android.media.AudioFormat}'s + * {@code ENCODING_*} constants. + * @param maxChannelCount The maximum number of audio channels that can be played simultaneously. + */ + /* package */ AudioCapabilities(int[] supportedEncodings, int maxChannelCount) { + if (supportedEncodings != null) { + this.supportedEncodings = Arrays.copyOf(supportedEncodings, supportedEncodings.length); + Arrays.sort(this.supportedEncodings); + } else { + this.supportedEncodings = new int[0]; + } + this.maxChannelCount = maxChannelCount; + } + + /** + * Returns whether this device supports playback of the specified audio {@code encoding}. + * + * @param encoding One of {@link android.media.AudioFormat}'s {@code ENCODING_*} constants. + * @return Whether this device supports playback the specified audio {@code encoding}. + */ + public boolean supportsEncoding(int encoding) { + return Arrays.binarySearch(supportedEncodings, encoding) >= 0; + } + + /** Returns the maximum number of channels the device can play at the same time. */ + public int getMaxChannelCount() { + return maxChannelCount; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof AudioCapabilities)) { + return false; + } + AudioCapabilities audioCapabilities = (AudioCapabilities) other; + return Arrays.equals(supportedEncodings, audioCapabilities.supportedEncodings) + && maxChannelCount == audioCapabilities.maxChannelCount; + } + + @Override + public int hashCode() { + return maxChannelCount + 31 * Arrays.hashCode(supportedEncodings); + } + + @Override + public String toString() { + return "AudioCapabilities[maxChannelCount=" + maxChannelCount + + ", supportedEncodings=" + Arrays.toString(supportedEncodings) + "]"; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/audio/AudioCapabilitiesReceiver.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/audio/AudioCapabilitiesReceiver.java new file mode 100755 index 000000000..d2d70fed1 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/audio/AudioCapabilitiesReceiver.java @@ -0,0 +1,104 @@ +/* + * 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.messenger.exoplayer.audio; + +import org.telegram.messenger.exoplayer.util.Assertions; +import org.telegram.messenger.exoplayer.util.Util; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.media.AudioManager; + +/** + * Notifies a listener when the audio playback capabilities change. Call {@link #register} to start + * (or resume) receiving notifications, and {@link #unregister} to stop. + */ +public final class AudioCapabilitiesReceiver { + + /** + * Listener notified when audio capabilities change. + */ + public interface Listener { + + /** + * Called when the audio capabilities change. + * + * @param audioCapabilities Current audio capabilities for the device. + */ + void onAudioCapabilitiesChanged(AudioCapabilities audioCapabilities); + + } + + private final Context context; + private final Listener listener; + private final BroadcastReceiver receiver; + + /* package */ AudioCapabilities audioCapabilities; + + /** + * Constructs a new audio capabilities receiver. + * + * @param context Context for registering to receive broadcasts. + * @param listener Listener to notify when audio capabilities change. + */ + public AudioCapabilitiesReceiver(Context context, Listener listener) { + this.context = Assertions.checkNotNull(context); + this.listener = Assertions.checkNotNull(listener); + this.receiver = Util.SDK_INT >= 21 ? new HdmiAudioPlugBroadcastReceiver() : null; + } + + /** + * Registers to notify the listener when audio capabilities change. The current capabilities will + * be returned. It is important to call {@link #unregister} so that the listener can be garbage + * collected. + * + * @return Current audio capabilities for the device. + */ + @SuppressWarnings("InlinedApi") + public AudioCapabilities register() { + Intent stickyIntent = receiver == null ? null + : context.registerReceiver(receiver, new IntentFilter(AudioManager.ACTION_HDMI_AUDIO_PLUG)); + audioCapabilities = AudioCapabilities.getCapabilities(stickyIntent); + return audioCapabilities; + } + + /** + * Unregisters to stop notifying the listener when audio capabilities change. + */ + public void unregister() { + if (receiver != null) { + context.unregisterReceiver(receiver); + } + } + + private final class HdmiAudioPlugBroadcastReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + if (!isInitialStickyBroadcast()) { + AudioCapabilities newAudioCapabilities = AudioCapabilities.getCapabilities(intent); + if (!newAudioCapabilities.equals(audioCapabilities)) { + audioCapabilities = newAudioCapabilities; + listener.onAudioCapabilitiesChanged(newAudioCapabilities); + } + } + } + + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/audio/AudioTrack.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/audio/AudioTrack.java new file mode 100755 index 000000000..9dc7059b3 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/audio/AudioTrack.java @@ -0,0 +1,1337 @@ +/* + * 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.messenger.exoplayer.audio; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.util.Ac3Util; +import org.telegram.messenger.exoplayer.util.Assertions; +import org.telegram.messenger.exoplayer.util.DtsUtil; +import org.telegram.messenger.exoplayer.util.MimeTypes; +import org.telegram.messenger.exoplayer.util.Util; + +import android.annotation.TargetApi; +import android.media.AudioFormat; +import android.media.AudioManager; +import android.media.AudioTimestamp; +import android.media.PlaybackParams; +import android.os.ConditionVariable; +import android.os.SystemClock; +import android.util.Log; + +import java.lang.reflect.Method; +import java.nio.ByteBuffer; + +/** + * Plays audio data. The implementation delegates to an {@link android.media.AudioTrack} and handles + * playback position smoothing, non-blocking writes and reconfiguration. + *

+ * Before starting playback, specify the input audio format by calling one of the {@link #configure} + * methods and {@link #initialize} the instance, optionally specifying an audio session. + *

+ * Call {@link #handleBuffer(ByteBuffer, int, int, long)} to write data to play back, and + * {@link #handleDiscontinuity()} when a buffer is skipped. Call {@link #play()} to start playing + * back written data. + *

+ * Call {@link #configure} again whenever the input format changes. If {@link #isInitialized()} + * returns false after calling {@link #configure}, it is necessary to re-{@link #initialize} the + * instance before writing more data. + *

+ * The underlying framework audio track is created by {@link #initialize} and released + * asynchronously by {@link #reset} (and {@link #configure}, unless the format is unchanged). + * Reinitialization blocks until releasing the old audio track completes. It is safe to + * re-{@link #initialize} the instance after calling {@link #reset()}, without reconfiguration. + *

+ * Call {@link #release()} when the instance will no longer be used. + */ +public final class AudioTrack { + + /** + * Thrown when a failure occurs instantiating an {@link android.media.AudioTrack}. + */ + public static final class InitializationException extends Exception { + + /** + * The state as reported by {@link android.media.AudioTrack#getState()}. + */ + public final int audioTrackState; + + public InitializationException( + int audioTrackState, int sampleRate, int channelConfig, int bufferSize) { + super("AudioTrack init failed: " + audioTrackState + ", Config(" + sampleRate + ", " + + channelConfig + ", " + bufferSize + ")"); + this.audioTrackState = audioTrackState; + } + + } + + /** + * Thrown when a failure occurs writing to an {@link android.media.AudioTrack}. + */ + public static final class WriteException extends Exception { + + /** + * The value returned from {@link android.media.AudioTrack#write(byte[], int, int)}. + */ + public final int errorCode; + + public WriteException(int errorCode) { + super("AudioTrack write failed: " + errorCode); + this.errorCode = errorCode; + } + + } + + /** + * Thrown when {@link android.media.AudioTrack#getTimestamp} returns a spurious timestamp, if + * {@code AudioTrack#failOnSpuriousAudioTimestamp} is set. + */ + public static final class InvalidAudioTrackTimestampException extends RuntimeException { + + public InvalidAudioTrackTimestampException(String message) { + super(message); + } + + } + + /** + * Returned in the result of {@link #handleBuffer} if the buffer was discontinuous. + */ + public static final int RESULT_POSITION_DISCONTINUITY = 1; + /** + * Returned in the result of {@link #handleBuffer} if the buffer can be released. + */ + public static final int RESULT_BUFFER_CONSUMED = 2; + + /** + * Represents an unset {@link android.media.AudioTrack} session identifier. + */ + public static final int SESSION_ID_NOT_SET = 0; + + /** + * Returned by {@link #getCurrentPositionUs} when the position is not set. + */ + public static final long CURRENT_POSITION_NOT_SET = Long.MIN_VALUE; + + /** + * A minimum length for the {@link android.media.AudioTrack} buffer, in microseconds. + */ + private static final long MIN_BUFFER_DURATION_US = 250000; + /** + * A maximum length for the {@link android.media.AudioTrack} buffer, in microseconds. + */ + private static final long MAX_BUFFER_DURATION_US = 750000; + /** + * The length for passthrough {@link android.media.AudioTrack} buffers, in microseconds. + */ + private static final long PASSTHROUGH_BUFFER_DURATION_US = 250000; + /** + * A multiplication factor to apply to the minimum buffer size requested by the underlying + * {@link android.media.AudioTrack}. + */ + private static final int BUFFER_MULTIPLICATION_FACTOR = 4; + + private static final String TAG = "AudioTrack"; + + /** + * AudioTrack timestamps are deemed spurious if they are offset from the system clock by more + * than this amount. + * + *

This is a fail safe that should not be required on correctly functioning devices. + */ + private static final long MAX_AUDIO_TIMESTAMP_OFFSET_US = 5 * C.MICROS_PER_SECOND; + + /** + * AudioTrack latencies are deemed impossibly large if they are greater than this amount. + * + *

This is a fail safe that should not be required on correctly functioning devices. + */ + private static final long MAX_LATENCY_US = 5 * C.MICROS_PER_SECOND; + + private static final int START_NOT_SET = 0; + private static final int START_IN_SYNC = 1; + private static final int START_NEED_SYNC = 2; + + private static final int MAX_PLAYHEAD_OFFSET_COUNT = 10; + private static final int MIN_PLAYHEAD_OFFSET_SAMPLE_INTERVAL_US = 30000; + private static final int MIN_TIMESTAMP_SAMPLE_INTERVAL_US = 500000; + + /** + * Whether to enable a workaround for an issue where an audio effect does not keep its session + * active across releasing/initializing a new audio track, on platform API version < 21. + *

+ * The flag must be set before creating a player. + */ + public static boolean enablePreV21AudioSessionWorkaround = false; + + /** + * Whether to throw an {@link InvalidAudioTrackTimestampException} when a spurious timestamp is + * reported from {@link android.media.AudioTrack#getTimestamp}. + *

+ * The flag must be set before creating a player. Should be set to {@code true} for testing and + * debugging purposes only. + */ + public static boolean failOnSpuriousAudioTimestamp = false; + + private final AudioCapabilities audioCapabilities; + private final int streamType; + private final ConditionVariable releasingConditionVariable; + private final long[] playheadOffsets; + private final AudioTrackUtil audioTrackUtil; + + /** + * Used to keep the audio session active on pre-V21 builds (see {@link #initialize()}). + */ + private android.media.AudioTrack keepSessionIdAudioTrack; + + private android.media.AudioTrack audioTrack; + private int sampleRate; + private int channelConfig; + private int sourceEncoding; + private int targetEncoding; + private boolean passthrough; + private int pcmFrameSize; + private int bufferSize; + private long bufferSizeUs; + + private int nextPlayheadOffsetIndex; + private int playheadOffsetCount; + private long smoothedPlayheadOffsetUs; + private long lastPlayheadSampleTimeUs; + private boolean audioTimestampSet; + private long lastTimestampSampleTimeUs; + + private Method getLatencyMethod; + private long submittedPcmBytes; + private long submittedEncodedFrames; + private int framesPerEncodedSample; + private int startMediaTimeState; + private long startMediaTimeUs; + private long resumeSystemTimeUs; + private long latencyUs; + private float volume; + + private byte[] temporaryBuffer; + private int temporaryBufferOffset; + private int bufferBytesRemaining; + + private ByteBuffer resampledBuffer; + private boolean useResampledBuffer; + + /** + * Creates an audio track with default audio capabilities (no encoded audio passthrough support). + */ + public AudioTrack() { + this(null, AudioManager.STREAM_MUSIC); + } + + /** + * Creates an audio track using the specified audio capabilities and stream type. + * + * @param audioCapabilities The current audio playback capabilities. + * @param streamType The type of audio stream for the underlying {@link android.media.AudioTrack}. + */ + public AudioTrack(AudioCapabilities audioCapabilities, int streamType) { + this.audioCapabilities = audioCapabilities; + this.streamType = streamType; + releasingConditionVariable = new ConditionVariable(true); + if (Util.SDK_INT >= 18) { + try { + getLatencyMethod = + android.media.AudioTrack.class.getMethod("getLatency", (Class[]) null); + } catch (NoSuchMethodException e) { + // There's no guarantee this method exists. Do nothing. + } + } + if (Util.SDK_INT >= 23) { + audioTrackUtil = new AudioTrackUtilV23(); + } else if (Util.SDK_INT >= 19) { + audioTrackUtil = new AudioTrackUtilV19(); + } else { + audioTrackUtil = new AudioTrackUtil(); + } + playheadOffsets = new long[MAX_PLAYHEAD_OFFSET_COUNT]; + volume = 1.0f; + startMediaTimeState = START_NOT_SET; + } + + /** + * Returns whether it is possible to play back input audio in the specified format using encoded + * audio passthrough. + */ + public boolean isPassthroughSupported(String mimeType) { + return audioCapabilities != null + && audioCapabilities.supportsEncoding(getEncodingForMimeType(mimeType)); + } + + /** + * Returns whether the audio track has been successfully initialized via {@link #initialize} and + * not yet {@link #reset}. + */ + public boolean isInitialized() { + return audioTrack != null; + } + + /** + * Returns the playback position in the stream starting at zero, in microseconds, or + * {@link #CURRENT_POSITION_NOT_SET} if it is not yet available. + * + *

If the device supports it, the method uses the playback timestamp from + * {@link android.media.AudioTrack#getTimestamp}. Otherwise, it derives a smoothed position by + * sampling the {@link android.media.AudioTrack}'s frame position. + * + * @param sourceEnded Specify {@code true} if no more input buffers will be provided. + * @return The playback position relative to the start of playback, in microseconds. + */ + public long getCurrentPositionUs(boolean sourceEnded) { + if (!hasCurrentPositionUs()) { + return CURRENT_POSITION_NOT_SET; + } + + if (audioTrack.getPlayState() == android.media.AudioTrack.PLAYSTATE_PLAYING) { + maybeSampleSyncParams(); + } + + long systemClockUs = System.nanoTime() / 1000; + long currentPositionUs; + if (audioTimestampSet) { + // How long ago in the past the audio timestamp is (negative if it's in the future). + long presentationDiff = systemClockUs - (audioTrackUtil.getTimestampNanoTime() / 1000); + // Fixes such difference if the playback speed is not real time speed. + long actualSpeedPresentationDiff = (long) (presentationDiff + * audioTrackUtil.getPlaybackSpeed()); + long framesDiff = durationUsToFrames(actualSpeedPresentationDiff); + // The position of the frame that's currently being presented. + long currentFramePosition = audioTrackUtil.getTimestampFramePosition() + framesDiff; + currentPositionUs = framesToDurationUs(currentFramePosition) + startMediaTimeUs; + } else { + if (playheadOffsetCount == 0) { + // The AudioTrack has started, but we don't have any samples to compute a smoothed position. + currentPositionUs = audioTrackUtil.getPlaybackHeadPositionUs() + startMediaTimeUs; + } else { + // getPlayheadPositionUs() only has a granularity of ~20ms, so we base the position off the + // system clock (and a smoothed offset between it and the playhead position) so as to + // prevent jitter in the reported positions. + currentPositionUs = systemClockUs + smoothedPlayheadOffsetUs + startMediaTimeUs; + } + if (!sourceEnded) { + currentPositionUs -= latencyUs; + } + } + + return currentPositionUs; + } + + /** + * Configures (or reconfigures) the audio track, inferring a suitable buffer size automatically. + * + * @param mimeType The mime type. + * @param channelCount The number of channels. + * @param sampleRate The sample rate in Hz. + * @param pcmEncoding For PCM formats, the encoding used. One of {@link C#ENCODING_PCM_16BIT}, + * {@link C#ENCODING_PCM_16BIT}, {@link C#ENCODING_PCM_24BIT} and + * {@link C#ENCODING_PCM_32BIT}. + */ + public void configure(String mimeType, int channelCount, int sampleRate, int pcmEncoding) { + configure(mimeType, channelCount, sampleRate, pcmEncoding, 0); + } + + /** + * Configures (or reconfigures) the audio track. + * + * @param mimeType The mime type. + * @param channelCount The number of channels. + * @param sampleRate The sample rate in Hz. + * @param pcmEncoding For PCM formats, the encoding used. One of {@link C#ENCODING_PCM_16BIT}, + * {@link C#ENCODING_PCM_16BIT}, {@link C#ENCODING_PCM_24BIT} and + * {@link C#ENCODING_PCM_32BIT}. + * @param specifiedBufferSize A specific size for the playback buffer in bytes, or 0 to infer a + * suitable buffer size automatically. + */ + public void configure(String mimeType, int channelCount, int sampleRate, int pcmEncoding, + int specifiedBufferSize) { + int channelConfig; + switch (channelCount) { + case 1: + channelConfig = AudioFormat.CHANNEL_OUT_MONO; + break; + case 2: + channelConfig = AudioFormat.CHANNEL_OUT_STEREO; + break; + case 3: + channelConfig = AudioFormat.CHANNEL_OUT_STEREO | AudioFormat.CHANNEL_OUT_FRONT_CENTER; + break; + case 4: + channelConfig = AudioFormat.CHANNEL_OUT_QUAD; + break; + case 5: + channelConfig = AudioFormat.CHANNEL_OUT_QUAD | AudioFormat.CHANNEL_OUT_FRONT_CENTER; + break; + case 6: + channelConfig = AudioFormat.CHANNEL_OUT_5POINT1; + break; + case 7: + channelConfig = AudioFormat.CHANNEL_OUT_5POINT1 | AudioFormat.CHANNEL_OUT_BACK_CENTER; + break; + case 8: + channelConfig = C.CHANNEL_OUT_7POINT1_SURROUND; + break; + default: + throw new IllegalArgumentException("Unsupported channel count: " + channelCount); + } + + boolean passthrough = !MimeTypes.AUDIO_RAW.equals(mimeType); + int sourceEncoding; + if (passthrough) { + sourceEncoding = getEncodingForMimeType(mimeType); + } else if (pcmEncoding == C.ENCODING_PCM_8BIT || pcmEncoding == C.ENCODING_PCM_16BIT + || pcmEncoding == C.ENCODING_PCM_24BIT || pcmEncoding == C.ENCODING_PCM_32BIT) { + sourceEncoding = pcmEncoding; + } else { + throw new IllegalArgumentException("Unsupported PCM encoding: " + pcmEncoding); + } + + if (isInitialized() && this.sourceEncoding == sourceEncoding && this.sampleRate == sampleRate + && this.channelConfig == channelConfig) { + // We already have an audio track with the correct sample rate, channel config and encoding. + return; + } + + reset(); + + this.sourceEncoding = sourceEncoding; + this.passthrough = passthrough; + this.sampleRate = sampleRate; + this.channelConfig = channelConfig; + targetEncoding = passthrough ? sourceEncoding : C.ENCODING_PCM_16BIT; + pcmFrameSize = 2 * channelCount; // 2 bytes per 16-bit sample * number of channels. + + if (specifiedBufferSize != 0) { + bufferSize = specifiedBufferSize; + } else if (passthrough) { + // TODO: Set the minimum buffer size using getMinBufferSize when it takes the encoding into + // account. [Internal: b/25181305] + if (targetEncoding == C.ENCODING_AC3 || targetEncoding == C.ENCODING_E_AC3) { + // AC-3 allows bitrates up to 640 kbit/s. + bufferSize = (int) (PASSTHROUGH_BUFFER_DURATION_US * 80 * 1024 / C.MICROS_PER_SECOND); + } else /* (targetEncoding == C.ENCODING_DTS || targetEncoding == C.ENCODING_DTS_HD) */ { + // DTS allows an 'open' bitrate, but we assume the maximum listed value: 1536 kbit/s. + bufferSize = (int) (PASSTHROUGH_BUFFER_DURATION_US * 192 * 1024 / C.MICROS_PER_SECOND); + } + } else { + int minBufferSize = + android.media.AudioTrack.getMinBufferSize(sampleRate, channelConfig, targetEncoding); + Assertions.checkState(minBufferSize != android.media.AudioTrack.ERROR_BAD_VALUE); + int multipliedBufferSize = minBufferSize * BUFFER_MULTIPLICATION_FACTOR; + int minAppBufferSize = (int) durationUsToFrames(MIN_BUFFER_DURATION_US) * pcmFrameSize; + int maxAppBufferSize = (int) Math.max(minBufferSize, + durationUsToFrames(MAX_BUFFER_DURATION_US) * pcmFrameSize); + bufferSize = multipliedBufferSize < minAppBufferSize ? minAppBufferSize + : multipliedBufferSize > maxAppBufferSize ? maxAppBufferSize + : multipliedBufferSize; + } + bufferSizeUs = passthrough ? C.UNKNOWN_TIME_US + : framesToDurationUs(pcmBytesToFrames(bufferSize)); + } + + /** + * Initializes the audio track for writing new buffers using {@link #handleBuffer}. + * + * @return The audio track session identifier. + */ + public int initialize() throws InitializationException { + return initialize(SESSION_ID_NOT_SET); + } + + /** + * Initializes the audio track for writing new buffers using {@link #handleBuffer}. + * + * @param sessionId Audio track session identifier to re-use, or {@link #SESSION_ID_NOT_SET} to + * create a new one. + * @return The new (or re-used) session identifier. + */ + public int initialize(int sessionId) throws InitializationException { + // If we're asynchronously releasing a previous audio track then we block until it has been + // released. This guarantees that we cannot end up in a state where we have multiple audio + // track instances. Without this guarantee it would be possible, in extreme cases, to exhaust + // the shared memory that's available for audio track buffers. This would in turn cause the + // initialization of the audio track to fail. + releasingConditionVariable.block(); + + if (sessionId == SESSION_ID_NOT_SET) { + audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig, + targetEncoding, bufferSize, android.media.AudioTrack.MODE_STREAM); + } else { + // Re-attach to the same audio session. + audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig, + targetEncoding, bufferSize, android.media.AudioTrack.MODE_STREAM, sessionId); + } + checkAudioTrackInitialized(); + + sessionId = audioTrack.getAudioSessionId(); + if (enablePreV21AudioSessionWorkaround) { + if (Util.SDK_INT < 21) { + // The workaround creates an audio track with a two byte buffer on the same session, and + // does not release it until this object is released, which keeps the session active. + if (keepSessionIdAudioTrack != null + && sessionId != keepSessionIdAudioTrack.getAudioSessionId()) { + releaseKeepSessionIdAudioTrack(); + } + if (keepSessionIdAudioTrack == null) { + int sampleRate = 4000; // Equal to private android.media.AudioTrack.MIN_SAMPLE_RATE. + int channelConfig = AudioFormat.CHANNEL_OUT_MONO; + int encoding = C.ENCODING_PCM_16BIT; + int bufferSize = 2; // Use a two byte buffer, as it is not actually used for playback. + keepSessionIdAudioTrack = new android.media.AudioTrack(streamType, sampleRate, + channelConfig, encoding, bufferSize, android.media.AudioTrack.MODE_STATIC, sessionId); + } + } + } + + audioTrackUtil.reconfigure(audioTrack, needsPassthroughWorkarounds()); + setAudioTrackVolume(); + + return sessionId; + } + + /** + * Returns the size of this {@link AudioTrack}'s buffer in bytes. + *

+ * The value returned from this method may change as a result of calling one of the + * {@link #configure} methods. + * + * @return The size of the buffer in bytes. + */ + public int getBufferSize() { + return bufferSize; + } + + /** + * Returns the size of the buffer in microseconds for PCM {@link AudioTrack}s, or + * {@link C#UNKNOWN_TIME_US} for passthrough {@link AudioTrack}s. + *

+ * The value returned from this method may change as a result of calling one of the + * {@link #configure} methods. + * + * @return The size of the buffer in microseconds for PCM {@link AudioTrack}s, or + * {@link C#UNKNOWN_TIME_US} for passthrough {@link AudioTrack}s. + */ + public long getBufferSizeUs() { + return bufferSizeUs; + } + + /** + * Starts or resumes playing audio if the audio track has been initialized. + */ + public void play() { + if (isInitialized()) { + resumeSystemTimeUs = System.nanoTime() / 1000; + audioTrack.play(); + } + } + + /** + * Signals to the audio track that the next buffer is discontinuous with the previous buffer. + */ + public void handleDiscontinuity() { + // Force resynchronization after a skipped buffer. + if (startMediaTimeState == START_IN_SYNC) { + startMediaTimeState = START_NEED_SYNC; + } + } + + /** + * Attempts to write {@code size} bytes from {@code buffer} at {@code offset} to the audio track. + * Returns a bit field containing {@link #RESULT_BUFFER_CONSUMED} if the buffer can be released + * (due to having been written), and {@link #RESULT_POSITION_DISCONTINUITY} if the buffer was + * discontinuous with previously written data. + * + * @param buffer The buffer containing audio data to play back. + * @param offset The offset in the buffer from which to consume data. + * @param size The number of bytes to consume from {@code buffer}. + * @param presentationTimeUs Presentation timestamp of the next buffer in microseconds. + * @return A bit field with {@link #RESULT_BUFFER_CONSUMED} if the buffer can be released, and + * {@link #RESULT_POSITION_DISCONTINUITY} if the buffer was not contiguous with previously + * written data. + * @throws WriteException If an error occurs writing the audio data. + */ + public int handleBuffer(ByteBuffer buffer, int offset, int size, long presentationTimeUs) + throws WriteException { + if (size == 0) { + return RESULT_BUFFER_CONSUMED; + } + + if (needsPassthroughWorkarounds()) { + // An AC-3 audio track continues to play data written while it is paused. Stop writing so its + // buffer empties. See [Internal: b/18899620]. + if (audioTrack.getPlayState() == android.media.AudioTrack.PLAYSTATE_PAUSED) { + return 0; + } + + // A new AC-3 audio track's playback position continues to increase from the old track's + // position for a short time after is has been released. Avoid writing data until the playback + // head position actually returns to zero. + if (audioTrack.getPlayState() == android.media.AudioTrack.PLAYSTATE_STOPPED + && audioTrackUtil.getPlaybackHeadPosition() != 0) { + return 0; + } + } + + int result = 0; + if (bufferBytesRemaining == 0) { + // The previous buffer (if there was one) was fully written to the audio track. We're now + // seeing a new buffer for the first time. + + useResampledBuffer = targetEncoding != sourceEncoding; + if (useResampledBuffer) { + Assertions.checkState(targetEncoding == C.ENCODING_PCM_16BIT); + // Resample the buffer to get the data in the target encoding. + resampledBuffer = resampleTo16BitPcm(buffer, offset, size, sourceEncoding, resampledBuffer); + // Use the resampled buffer, offset and size. + buffer = resampledBuffer; + offset = resampledBuffer.position(); + size = resampledBuffer.limit(); + } + + bufferBytesRemaining = size; + buffer.position(offset); + if (passthrough && framesPerEncodedSample == 0) { + // If this is the first encoded sample, calculate the sample size in frames. + framesPerEncodedSample = getFramesPerEncodedSample(targetEncoding, buffer); + } + if (startMediaTimeState == START_NOT_SET) { + startMediaTimeUs = Math.max(0, presentationTimeUs); + startMediaTimeState = START_IN_SYNC; + } else { + // Sanity check that bufferStartTime is consistent with the expected value. + long expectedBufferStartTime = startMediaTimeUs + framesToDurationUs(getSubmittedFrames()); + if (startMediaTimeState == START_IN_SYNC + && Math.abs(expectedBufferStartTime - presentationTimeUs) > 200000) { + Log.e(TAG, "Discontinuity detected [expected " + expectedBufferStartTime + ", got " + + presentationTimeUs + "]"); + startMediaTimeState = START_NEED_SYNC; + } + if (startMediaTimeState == START_NEED_SYNC) { + // Adjust startMediaTimeUs to be consistent with the current buffer's start time and the + // number of bytes submitted. + startMediaTimeUs += (presentationTimeUs - expectedBufferStartTime); + startMediaTimeState = START_IN_SYNC; + result |= RESULT_POSITION_DISCONTINUITY; + } + } + if (Util.SDK_INT < 21) { + // Copy {@code buffer} into {@code temporaryBuffer}. + if (temporaryBuffer == null || temporaryBuffer.length < size) { + temporaryBuffer = new byte[size]; + } + buffer.get(temporaryBuffer, 0, size); + temporaryBufferOffset = 0; + } + } + + int bytesWritten = 0; + if (Util.SDK_INT < 21) { // passthrough == false + // Work out how many bytes we can write without the risk of blocking. + int bytesPending = + (int) (submittedPcmBytes - (audioTrackUtil.getPlaybackHeadPosition() * pcmFrameSize)); + int bytesToWrite = bufferSize - bytesPending; + if (bytesToWrite > 0) { + bytesToWrite = Math.min(bufferBytesRemaining, bytesToWrite); + bytesWritten = audioTrack.write(temporaryBuffer, temporaryBufferOffset, bytesToWrite); + if (bytesWritten >= 0) { + temporaryBufferOffset += bytesWritten; + } + } + } else { + ByteBuffer data = useResampledBuffer ? resampledBuffer : buffer; + bytesWritten = writeNonBlockingV21(audioTrack, data, bufferBytesRemaining); + } + + if (bytesWritten < 0) { + throw new WriteException(bytesWritten); + } + + bufferBytesRemaining -= bytesWritten; + if (!passthrough) { + submittedPcmBytes += bytesWritten; + } + if (bufferBytesRemaining == 0) { + if (passthrough) { + submittedEncodedFrames += framesPerEncodedSample; + } + result |= RESULT_BUFFER_CONSUMED; + } + return result; + } + + /** + * Ensures that the last data passed to {@link #handleBuffer(ByteBuffer, int, int, long)} is + * played out in full. + */ + public void handleEndOfStream() { + if (isInitialized()) { + audioTrackUtil.handleEndOfStream(getSubmittedFrames()); + } + } + + /** + * Returns whether the audio track has more data pending that will be played back. + */ + public boolean hasPendingData() { + return isInitialized() + && (getSubmittedFrames() > audioTrackUtil.getPlaybackHeadPosition() + || overrideHasPendingData()); + } + + /** + * Sets the playback parameters. Only available for SDK_INT >= 23 + * + * @throws UnsupportedOperationException if the Playback Parameters are not supported. That is, + * SDK_INT < 23. + */ + public void setPlaybackParams(PlaybackParams playbackParams) { + audioTrackUtil.setPlaybackParameters(playbackParams); + } + + /** + * Sets the playback volume. + */ + public void setVolume(float volume) { + if (this.volume != volume) { + this.volume = volume; + setAudioTrackVolume(); + } + } + + private void setAudioTrackVolume() { + if (!isInitialized()) { + // Do nothing. + } else if (Util.SDK_INT >= 21) { + setAudioTrackVolumeV21(audioTrack, volume); + } else { + setAudioTrackVolumeV3(audioTrack, volume); + } + } + + /** + * Pauses playback. + */ + public void pause() { + if (isInitialized()) { + resetSyncParams(); + audioTrackUtil.pause(); + } + } + + /** + * Releases the underlying audio track asynchronously. Calling {@link #initialize} will block + * until the audio track has been released, so it is safe to initialize immediately after + * resetting. The audio session may remain active until the instance is {@link #release}d. + */ + public void reset() { + if (isInitialized()) { + submittedPcmBytes = 0; + submittedEncodedFrames = 0; + framesPerEncodedSample = 0; + bufferBytesRemaining = 0; + startMediaTimeState = START_NOT_SET; + latencyUs = 0; + resetSyncParams(); + int playState = audioTrack.getPlayState(); + if (playState == android.media.AudioTrack.PLAYSTATE_PLAYING) { + audioTrack.pause(); + } + // AudioTrack.release can take some time, so we call it on a background thread. + final android.media.AudioTrack toRelease = audioTrack; + audioTrack = null; + audioTrackUtil.reconfigure(null, false); + releasingConditionVariable.close(); + new Thread() { + @Override + public void run() { + try { + toRelease.flush(); + toRelease.release(); + } finally { + releasingConditionVariable.open(); + } + } + }.start(); + } + } + + /** + * Releases all resources associated with this instance. + */ + public void release() { + reset(); + releaseKeepSessionIdAudioTrack(); + } + + /** + * Releases {@link #keepSessionIdAudioTrack} asynchronously, if it is non-{@code null}. + */ + private void releaseKeepSessionIdAudioTrack() { + if (keepSessionIdAudioTrack == null) { + return; + } + + // AudioTrack.release can take some time, so we call it on a background thread. + final android.media.AudioTrack toRelease = keepSessionIdAudioTrack; + keepSessionIdAudioTrack = null; + new Thread() { + @Override + public void run() { + toRelease.release(); + } + }.start(); + } + + /** + * Returns whether {@link #getCurrentPositionUs} can return the current playback position. + */ + private boolean hasCurrentPositionUs() { + return isInitialized() && startMediaTimeState != START_NOT_SET; + } + + /** + * Updates the audio track latency and playback position parameters. + */ + private void maybeSampleSyncParams() { + long playbackPositionUs = audioTrackUtil.getPlaybackHeadPositionUs(); + if (playbackPositionUs == 0) { + // The AudioTrack hasn't output anything yet. + return; + } + long systemClockUs = System.nanoTime() / 1000; + if (systemClockUs - lastPlayheadSampleTimeUs >= MIN_PLAYHEAD_OFFSET_SAMPLE_INTERVAL_US) { + // Take a new sample and update the smoothed offset between the system clock and the playhead. + playheadOffsets[nextPlayheadOffsetIndex] = playbackPositionUs - systemClockUs; + nextPlayheadOffsetIndex = (nextPlayheadOffsetIndex + 1) % MAX_PLAYHEAD_OFFSET_COUNT; + if (playheadOffsetCount < MAX_PLAYHEAD_OFFSET_COUNT) { + playheadOffsetCount++; + } + lastPlayheadSampleTimeUs = systemClockUs; + smoothedPlayheadOffsetUs = 0; + for (int i = 0; i < playheadOffsetCount; i++) { + smoothedPlayheadOffsetUs += playheadOffsets[i] / playheadOffsetCount; + } + } + + if (needsPassthroughWorkarounds()) { + // Don't sample the timestamp and latency if this is an AC-3 passthrough AudioTrack on + // platform API versions 21/22, as incorrect values are returned. See [Internal: b/21145353]. + return; + } + + if (systemClockUs - lastTimestampSampleTimeUs >= MIN_TIMESTAMP_SAMPLE_INTERVAL_US) { + audioTimestampSet = audioTrackUtil.updateTimestamp(); + if (audioTimestampSet) { + // Perform sanity checks on the timestamp. + long audioTimestampUs = audioTrackUtil.getTimestampNanoTime() / 1000; + long audioTimestampFramePosition = audioTrackUtil.getTimestampFramePosition(); + if (audioTimestampUs < resumeSystemTimeUs) { + // The timestamp corresponds to a time before the track was most recently resumed. + audioTimestampSet = false; + } else if (Math.abs(audioTimestampUs - systemClockUs) > MAX_AUDIO_TIMESTAMP_OFFSET_US) { + // The timestamp time base is probably wrong. + String message = "Spurious audio timestamp (system clock mismatch): " + + audioTimestampFramePosition + ", " + audioTimestampUs + ", " + systemClockUs + ", " + + playbackPositionUs; + if (failOnSpuriousAudioTimestamp) { + throw new InvalidAudioTrackTimestampException(message); + } + Log.w(TAG, message); + audioTimestampSet = false; + } else if (Math.abs(framesToDurationUs(audioTimestampFramePosition) - playbackPositionUs) + > MAX_AUDIO_TIMESTAMP_OFFSET_US) { + // The timestamp frame position is probably wrong. + String message = "Spurious audio timestamp (frame position mismatch): " + + audioTimestampFramePosition + ", " + audioTimestampUs + ", " + systemClockUs + ", " + + playbackPositionUs; + if (failOnSpuriousAudioTimestamp) { + throw new InvalidAudioTrackTimestampException(message); + } + Log.w(TAG, message); + audioTimestampSet = false; + } + } + if (getLatencyMethod != null && !passthrough) { + try { + // Compute the audio track latency, excluding the latency due to the buffer (leaving + // latency due to the mixer and audio hardware driver). + latencyUs = (Integer) getLatencyMethod.invoke(audioTrack, (Object[]) null) * 1000L + - bufferSizeUs; + // Sanity check that the latency is non-negative. + latencyUs = Math.max(latencyUs, 0); + // Sanity check that the latency isn't too large. + if (latencyUs > MAX_LATENCY_US) { + Log.w(TAG, "Ignoring impossibly large audio latency: " + latencyUs); + latencyUs = 0; + } + } catch (Exception e) { + // The method existed, but doesn't work. Don't try again. + getLatencyMethod = null; + } + } + lastTimestampSampleTimeUs = systemClockUs; + } + } + + /** + * Checks that {@link #audioTrack} has been successfully initialized. If it has then calling this + * method is a no-op. If it hasn't then {@link #audioTrack} is released and set to null, and an + * exception is thrown. + * + * @throws InitializationException If {@link #audioTrack} has not been successfully initialized. + */ + private void checkAudioTrackInitialized() throws InitializationException { + int state = audioTrack.getState(); + if (state == android.media.AudioTrack.STATE_INITIALIZED) { + return; + } + // The track is not successfully initialized. Release and null the track. + try { + audioTrack.release(); + } catch (Exception e) { + // The track has already failed to initialize, so it wouldn't be that surprising if release + // were to fail too. Swallow the exception. + } finally { + audioTrack = null; + } + + throw new InitializationException(state, sampleRate, channelConfig, bufferSize); + } + + private long pcmBytesToFrames(long byteCount) { + return byteCount / pcmFrameSize; + } + + private long framesToDurationUs(long frameCount) { + return (frameCount * C.MICROS_PER_SECOND) / sampleRate; + } + + private long durationUsToFrames(long durationUs) { + return (durationUs * sampleRate) / C.MICROS_PER_SECOND; + } + + private long getSubmittedFrames() { + return passthrough ? submittedEncodedFrames : pcmBytesToFrames(submittedPcmBytes); + } + + private void resetSyncParams() { + smoothedPlayheadOffsetUs = 0; + playheadOffsetCount = 0; + nextPlayheadOffsetIndex = 0; + lastPlayheadSampleTimeUs = 0; + audioTimestampSet = false; + lastTimestampSampleTimeUs = 0; + } + + /** + * Returns whether to work around problems with passthrough audio tracks. + * See [Internal: b/18899620, b/19187573, b/21145353]. + */ + private boolean needsPassthroughWorkarounds() { + return Util.SDK_INT < 23 + && (targetEncoding == C.ENCODING_AC3 || targetEncoding == C.ENCODING_E_AC3); + } + + /** + * Returns whether the audio track should behave as though it has pending data. This is to work + * around an issue on platform API versions 21/22 where AC-3 audio tracks can't be paused, so we + * empty their buffers when paused. In this case, they should still behave as if they have + * pending data, otherwise writing will never resume. + */ + private boolean overrideHasPendingData() { + return needsPassthroughWorkarounds() + && audioTrack.getPlayState() == android.media.AudioTrack.PLAYSTATE_PAUSED + && audioTrack.getPlaybackHeadPosition() == 0; + } + + /** + * Converts the provided buffer into 16-bit PCM. + * + * @param buffer The buffer containing the data to convert. + * @param offset The offset of the data in the buffer. + * @param size The size in bytes of the data in the buffer. + * @param sourceEncoding The data encoding. + * @param out A buffer into which the output should be written, if its capacity is sufficient. + * @return The 16-bit PCM output. Different to the out parameter if null was passed, or if the + * capacity was insufficient for the output. + */ + private static ByteBuffer resampleTo16BitPcm(ByteBuffer buffer, int offset, int size, + int sourceEncoding, ByteBuffer out) { + int resampledSize; + switch (sourceEncoding) { + case C.ENCODING_PCM_8BIT: + resampledSize = size * 2; + break; + case C.ENCODING_PCM_24BIT: + resampledSize = (size / 3) * 2; + break; + case C.ENCODING_PCM_32BIT: + resampledSize = size / 2; + break; + default: + // Never happens. + throw new IllegalStateException(); + } + + ByteBuffer resampledBuffer = out; + if (resampledBuffer == null || resampledBuffer.capacity() < resampledSize) { + resampledBuffer = ByteBuffer.allocateDirect(resampledSize); + } + resampledBuffer.position(0); + resampledBuffer.limit(resampledSize); + + // Samples are little endian. + int limit = offset + size; + switch (sourceEncoding) { + case C.ENCODING_PCM_8BIT: + // 8->16 bit resampling. Shift each byte from [0, 256) to [-128, 128) and scale up. + for (int i = offset; i < limit; i++) { + resampledBuffer.put((byte) 0); + resampledBuffer.put((byte) ((buffer.get(i) & 0xFF) - 128)); + } + break; + case C.ENCODING_PCM_24BIT: + // 24->16 bit resampling. Drop the least significant byte. + for (int i = offset; i < limit; i += 3) { + resampledBuffer.put(buffer.get(i + 1)); + resampledBuffer.put(buffer.get(i + 2)); + } + break; + case C.ENCODING_PCM_32BIT: + // 32->16 bit resampling. Drop the two least significant bytes. + for (int i = offset; i < limit; i += 4) { + resampledBuffer.put(buffer.get(i + 2)); + resampledBuffer.put(buffer.get(i + 3)); + } + break; + default: + // Never happens. + throw new IllegalStateException(); + } + + resampledBuffer.position(0); + return resampledBuffer; + } + + private static int getEncodingForMimeType(String mimeType) { + switch (mimeType) { + case MimeTypes.AUDIO_AC3: + return C.ENCODING_AC3; + case MimeTypes.AUDIO_E_AC3: + return C.ENCODING_E_AC3; + case MimeTypes.AUDIO_DTS: + return C.ENCODING_DTS; + case MimeTypes.AUDIO_DTS_HD: + return C.ENCODING_DTS_HD; + default: + return C.ENCODING_INVALID; + } + } + + private static int getFramesPerEncodedSample(int encoding, ByteBuffer buffer) { + if (encoding == C.ENCODING_DTS || encoding == C.ENCODING_DTS_HD) { + return DtsUtil.parseDtsAudioSampleCount(buffer); + } else if (encoding == C.ENCODING_AC3) { + return Ac3Util.getAc3SyncframeAudioSampleCount(); + } else if (encoding == C.ENCODING_E_AC3) { + return Ac3Util.parseEAc3SyncframeAudioSampleCount(buffer); + } else { + throw new IllegalStateException("Unexpected audio encoding: " + encoding); + } + } + + @TargetApi(21) + private static int writeNonBlockingV21( + android.media.AudioTrack audioTrack, ByteBuffer buffer, int size) { + return audioTrack.write(buffer, size, android.media.AudioTrack.WRITE_NON_BLOCKING); + } + + @TargetApi(21) + private static void setAudioTrackVolumeV21(android.media.AudioTrack audioTrack, float volume) { + audioTrack.setVolume(volume); + } + + @SuppressWarnings("deprecation") + private static void setAudioTrackVolumeV3(android.media.AudioTrack audioTrack, float volume) { + audioTrack.setStereoVolume(volume, volume); + } + + /** + * Wraps an {@link android.media.AudioTrack} to expose useful utility methods. + */ + private static class AudioTrackUtil { + + protected android.media.AudioTrack audioTrack; + private boolean needsPassthroughWorkaround; + private int sampleRate; + private long lastRawPlaybackHeadPosition; + private long rawPlaybackHeadWrapCount; + private long passthroughWorkaroundPauseOffset; + + private long stopTimestampUs; + private long stopPlaybackHeadPosition; + private long endPlaybackHeadPosition; + + /** + * Reconfigures the audio track utility helper to use the specified {@code audioTrack}. + * + * @param audioTrack The audio track to wrap. + * @param needsPassthroughWorkaround Whether to workaround issues with pausing AC-3 passthrough + * audio tracks on platform API version 21/22. + */ + public void reconfigure(android.media.AudioTrack audioTrack, + boolean needsPassthroughWorkaround) { + this.audioTrack = audioTrack; + this.needsPassthroughWorkaround = needsPassthroughWorkaround; + stopTimestampUs = -1; + lastRawPlaybackHeadPosition = 0; + rawPlaybackHeadWrapCount = 0; + passthroughWorkaroundPauseOffset = 0; + if (audioTrack != null) { + sampleRate = audioTrack.getSampleRate(); + } + } + + /** + * Stops the audio track in a way that ensures media written to it is played out in full, and + * that {@link #getPlaybackHeadPosition()} and {@link #getPlaybackHeadPositionUs()} continue to + * increment as the remaining media is played out. + * + * @param submittedFrames The total number of frames that have been submitted. + */ + public void handleEndOfStream(long submittedFrames) { + stopPlaybackHeadPosition = getPlaybackHeadPosition(); + stopTimestampUs = SystemClock.elapsedRealtime() * 1000; + endPlaybackHeadPosition = submittedFrames; + audioTrack.stop(); + } + + /** + * Pauses the audio track unless the end of the stream has been handled, in which case calling + * this method does nothing. + */ + public void pause() { + if (stopTimestampUs != -1) { + // We don't want to knock the audio track back into the paused state. + return; + } + audioTrack.pause(); + } + + /** + * {@link android.media.AudioTrack#getPlaybackHeadPosition()} returns a value intended to be + * interpreted as an unsigned 32 bit integer, which also wraps around periodically. This method + * returns the playback head position as a long that will only wrap around if the value exceeds + * {@link Long#MAX_VALUE} (which in practice will never happen). + * + * @return {@link android.media.AudioTrack#getPlaybackHeadPosition()} of {@link #audioTrack} + * expressed as a long. + */ + public long getPlaybackHeadPosition() { + if (stopTimestampUs != -1) { + // Simulate the playback head position up to the total number of frames submitted. + long elapsedTimeSinceStopUs = (SystemClock.elapsedRealtime() * 1000) - stopTimestampUs; + long framesSinceStop = (elapsedTimeSinceStopUs * sampleRate) / C.MICROS_PER_SECOND; + return Math.min(endPlaybackHeadPosition, stopPlaybackHeadPosition + framesSinceStop); + } + + int state = audioTrack.getPlayState(); + if (state == android.media.AudioTrack.PLAYSTATE_STOPPED) { + // The audio track hasn't been started. + return 0; + } + + long rawPlaybackHeadPosition = 0xFFFFFFFFL & audioTrack.getPlaybackHeadPosition(); + if (needsPassthroughWorkaround) { + // Work around an issue with passthrough/direct AudioTracks on platform API versions 21/22 + // where the playback head position jumps back to zero on paused passthrough/direct audio + // tracks. See [Internal: b/19187573]. + if (state == android.media.AudioTrack.PLAYSTATE_PAUSED && rawPlaybackHeadPosition == 0) { + passthroughWorkaroundPauseOffset = lastRawPlaybackHeadPosition; + } + rawPlaybackHeadPosition += passthroughWorkaroundPauseOffset; + } + if (lastRawPlaybackHeadPosition > rawPlaybackHeadPosition) { + // The value must have wrapped around. + rawPlaybackHeadWrapCount++; + } + lastRawPlaybackHeadPosition = rawPlaybackHeadPosition; + return rawPlaybackHeadPosition + (rawPlaybackHeadWrapCount << 32); + } + + /** + * Returns {@link #getPlaybackHeadPosition()} expressed as microseconds. + */ + public long getPlaybackHeadPositionUs() { + return (getPlaybackHeadPosition() * C.MICROS_PER_SECOND) / sampleRate; + } + + /** + * Updates the values returned by {@link #getTimestampNanoTime()} and + * {@link #getTimestampFramePosition()}. + * + * @return True if the timestamp values were updated. False otherwise. + */ + public boolean updateTimestamp() { + return false; + } + + /** + * Returns the {@link android.media.AudioTimestamp#nanoTime} obtained during the most recent + * call to {@link #updateTimestamp()} that returned true. + * + * @return The nanoTime obtained during the most recent call to {@link #updateTimestamp()} that + * returned true. + * @throws UnsupportedOperationException If the implementation does not support audio timestamp + * queries. {@link #updateTimestamp()} will always return false in this case. + */ + public long getTimestampNanoTime() { + // Should never be called if updateTimestamp() returned false. + throw new UnsupportedOperationException(); + } + + /** + * Returns the {@link android.media.AudioTimestamp#framePosition} obtained during the most + * recent call to {@link #updateTimestamp()} that returned true. The value is adjusted so that + * wrap around only occurs if the value exceeds {@link Long#MAX_VALUE} (which in practice will + * never happen). + * + * @return The framePosition obtained during the most recent call to {@link #updateTimestamp()} + * that returned true. + * @throws UnsupportedOperationException If the implementation does not support audio timestamp + * queries. {@link #updateTimestamp()} will always return false in this case. + */ + public long getTimestampFramePosition() { + // Should never be called if updateTimestamp() returned false. + throw new UnsupportedOperationException(); + } + + /** + * Sets the Playback Parameters to be used by the underlying {@link android.media.AudioTrack}. + * + * @param playbackParams to be used by the {@link android.media.AudioTrack}. + * @throws UnsupportedOperationException If Playback Parameters are not supported + * (i.e. SDK_INT < 23). + */ + public void setPlaybackParameters(PlaybackParams playbackParams) { + throw new UnsupportedOperationException(); + } + + /** + * Returns the configured playback speed according to the used Playback Parameters. If these are + * not supported, 1.0f(normal speed) is returned. + * + * @return The speed factor used by the underlying {@link android.media.AudioTrack}. + */ + public float getPlaybackSpeed() { + return 1.0f; + } + + } + + @TargetApi(19) + private static class AudioTrackUtilV19 extends AudioTrackUtil { + + private final AudioTimestamp audioTimestamp; + + private long rawTimestampFramePositionWrapCount; + private long lastRawTimestampFramePosition; + private long lastTimestampFramePosition; + + public AudioTrackUtilV19() { + audioTimestamp = new AudioTimestamp(); + } + + @Override + public void reconfigure(android.media.AudioTrack audioTrack, + boolean needsPassthroughWorkaround) { + super.reconfigure(audioTrack, needsPassthroughWorkaround); + rawTimestampFramePositionWrapCount = 0; + lastRawTimestampFramePosition = 0; + lastTimestampFramePosition = 0; + } + + @Override + public boolean updateTimestamp() { + boolean updated = audioTrack.getTimestamp(audioTimestamp); + if (updated) { + long rawFramePosition = audioTimestamp.framePosition; + if (lastRawTimestampFramePosition > rawFramePosition) { + // The value must have wrapped around. + rawTimestampFramePositionWrapCount++; + } + lastRawTimestampFramePosition = rawFramePosition; + lastTimestampFramePosition = rawFramePosition + (rawTimestampFramePositionWrapCount << 32); + } + return updated; + } + + @Override + public long getTimestampNanoTime() { + return audioTimestamp.nanoTime; + } + + @Override + public long getTimestampFramePosition() { + return lastTimestampFramePosition; + } + + } + + @TargetApi(23) + private static class AudioTrackUtilV23 extends AudioTrackUtilV19 { + + private PlaybackParams playbackParams; + private float playbackSpeed; + + public AudioTrackUtilV23() { + playbackSpeed = 1.0f; + } + + @Override + public void reconfigure(android.media.AudioTrack audioTrack, + boolean needsPassthroughWorkaround) { + super.reconfigure(audioTrack, needsPassthroughWorkaround); + maybeApplyPlaybackParams(); + } + + @Override + public void setPlaybackParameters(PlaybackParams playbackParams) { + playbackParams = (playbackParams != null ? playbackParams : new PlaybackParams()) + .allowDefaults(); + this.playbackParams = playbackParams; + this.playbackSpeed = playbackParams.getSpeed(); + maybeApplyPlaybackParams(); + } + + @Override + public float getPlaybackSpeed() { + return playbackSpeed; + } + + private void maybeApplyPlaybackParams() { + if (audioTrack != null && playbackParams != null) { + audioTrack.setPlaybackParams(playbackParams); + } + } + + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/BaseChunkSampleSourceEventListener.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/BaseChunkSampleSourceEventListener.java new file mode 100755 index 000000000..3484d509c --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/BaseChunkSampleSourceEventListener.java @@ -0,0 +1,104 @@ +/* + * 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.messenger.exoplayer.chunk; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.SampleSource; +import org.telegram.messenger.exoplayer.SampleSource.SampleSourceReader; + +import java.io.IOException; + +/** + * Interface for callbacks to be notified of chunk based {@link SampleSource} events. + */ +public interface BaseChunkSampleSourceEventListener { + + /** + * Invoked when an upstream load is started. + * + * @param sourceId The id of the reporting {@link SampleSource}. + * @param length The length of the data being loaded in bytes, or {@link C#LENGTH_UNBOUNDED} if + * the length of the data is not known in advance. + * @param type The type of the data being loaded. + * @param trigger The reason for the data being loaded. + * @param format The particular format to which this data corresponds, or null if the data being + * loaded does not correspond to a format. + * @param mediaStartTimeMs The media time of the start of the data being loaded, or -1 if this + * load is for initialization data. + * @param mediaEndTimeMs The media time of the end of the data being loaded, or -1 if this + * load is for initialization data. + */ + void onLoadStarted(int sourceId, long length, int type, int trigger, Format format, + long mediaStartTimeMs, long mediaEndTimeMs); + + /** + * Invoked when the current load operation completes. + * + * @param sourceId The id of the reporting {@link SampleSource}. + * @param bytesLoaded The number of bytes that were loaded. + * @param type The type of the loaded data. + * @param trigger The reason for the data being loaded. + * @param format The particular format to which this data corresponds, or null if the loaded data + * does not correspond to a format. + * @param mediaStartTimeMs The media time of the start of the loaded data, or -1 if this load was + * for initialization data. + * @param mediaEndTimeMs The media time of the end of the loaded data, or -1 if this load was for + * initialization data. + * @param elapsedRealtimeMs {@code elapsedRealtime} timestamp of when the load finished. + * @param loadDurationMs Amount of time taken to load the data. + */ + void onLoadCompleted(int sourceId, long bytesLoaded, int type, int trigger, Format format, + long mediaStartTimeMs, long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs); + + /** + * Invoked when the current upstream load operation is canceled. + * + * @param sourceId The id of the reporting {@link SampleSource}. + * @param bytesLoaded The number of bytes that were loaded prior to the cancellation. + */ + void onLoadCanceled(int sourceId, long bytesLoaded); + + /** + * Invoked when an error occurs loading media data. + * + * @param sourceId The id of the reporting {@link SampleSource}. + * @param e The cause of the failure. + */ + void onLoadError(int sourceId, IOException e); + + /** + * Invoked when data is removed from the back of the buffer, typically so that it can be + * re-buffered using a different representation. + * + * @param sourceId The id of the reporting {@link SampleSource}. + * @param mediaStartTimeMs The media time of the start of the discarded data. + * @param mediaEndTimeMs The media time of the end of the discarded data. + */ + void onUpstreamDiscarded(int sourceId, long mediaStartTimeMs, long mediaEndTimeMs); + + /** + * Invoked when the downstream format changes (i.e. when the format being supplied to the + * caller of {@link SampleSourceReader#readData} changes). + * + * @param sourceId The id of the reporting {@link SampleSource}. + * @param format The format. + * @param trigger The trigger specified in the corresponding upstream load, as specified by the + * {@link ChunkSource}. + * @param mediaTimeMs The media time at which the change occurred. + */ + void onDownstreamFormatChanged(int sourceId, Format format, int trigger, long mediaTimeMs); + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/BaseMediaChunk.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/BaseMediaChunk.java new file mode 100755 index 000000000..05b85e123 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/BaseMediaChunk.java @@ -0,0 +1,107 @@ +/* + * 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.messenger.exoplayer.chunk; + +import org.telegram.messenger.exoplayer.MediaFormat; +import org.telegram.messenger.exoplayer.drm.DrmInitData; +import org.telegram.messenger.exoplayer.extractor.DefaultTrackOutput; +import org.telegram.messenger.exoplayer.upstream.DataSource; +import org.telegram.messenger.exoplayer.upstream.DataSpec; + +/** + * A base implementation of {@link MediaChunk}, for chunks that contain a single track. + *

+ * Loaded samples are output to a {@link DefaultTrackOutput}. + */ +public abstract class BaseMediaChunk extends MediaChunk { + + /** + * Whether {@link #getMediaFormat()} and {@link #getDrmInitData()} can be called at any time to + * obtain the chunk's media format and drm initialization data. If false, these methods are only + * guaranteed to return correct data after the first sample data has been output from the chunk. + */ + public final boolean isMediaFormatFinal; + + private DefaultTrackOutput output; + private int firstSampleIndex; + + /** + * @param dataSource A {@link DataSource} for loading the data. + * @param dataSpec Defines the data to be loaded. + * @param trigger The reason for this chunk being selected. + * @param format The format of the stream to which this chunk belongs. + * @param startTimeUs The start time of the media contained by the chunk, in microseconds. + * @param endTimeUs The end time of the media contained by the chunk, in microseconds. + * @param chunkIndex The index of the chunk. + * @param isMediaFormatFinal True if {@link #getMediaFormat()} and {@link #getDrmInitData()} can + * be called at any time to obtain the media format and drm initialization data. False if + * these methods are only guaranteed to return correct data after the first sample data has + * been output from the chunk. + * @param parentId Identifier for a parent from which this chunk originates. + */ + public BaseMediaChunk(DataSource dataSource, DataSpec dataSpec, int trigger, Format format, + long startTimeUs, long endTimeUs, int chunkIndex, boolean isMediaFormatFinal, int parentId) { + super(dataSource, dataSpec, trigger, format, startTimeUs, endTimeUs, chunkIndex, parentId); + this.isMediaFormatFinal = isMediaFormatFinal; + } + + /** + * Initializes the chunk for loading, setting the {@link DefaultTrackOutput} that will receive + * samples as they are loaded. + * + * @param output The output that will receive the loaded samples. + */ + public void init(DefaultTrackOutput output) { + this.output = output; + this.firstSampleIndex = output.getWriteIndex(); + } + + /** + * Returns the index of the first sample in the output that was passed to + * {@link #init(DefaultTrackOutput)} that will originate from this chunk. + */ + public final int getFirstSampleIndex() { + return firstSampleIndex; + } + + /** + * Gets the {@link MediaFormat} corresponding to the chunk. + *

+ * See {@link #isMediaFormatFinal} for information about when this method is guaranteed to return + * correct data. + * + * @return The {@link MediaFormat} corresponding to this chunk. + */ + public abstract MediaFormat getMediaFormat(); + + /** + * Gets the {@link DrmInitData} corresponding to the chunk. + *

+ * See {@link #isMediaFormatFinal} for information about when this method is guaranteed to return + * correct data. + * + * @return The {@link DrmInitData} corresponding to this chunk. + */ + public abstract DrmInitData getDrmInitData(); + + /** + * Returns the output most recently passed to {@link #init(DefaultTrackOutput)}. + */ + protected final DefaultTrackOutput getOutput() { + return output; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/Chunk.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/Chunk.java new file mode 100755 index 000000000..e1c74410f --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/Chunk.java @@ -0,0 +1,135 @@ +/* + * 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.messenger.exoplayer.chunk; + +import org.telegram.messenger.exoplayer.upstream.DataSource; +import org.telegram.messenger.exoplayer.upstream.DataSpec; +import org.telegram.messenger.exoplayer.upstream.Loader.Loadable; +import org.telegram.messenger.exoplayer.util.Assertions; + +/** + * An abstract base class for {@link Loadable} implementations that load chunks of data required + * for the playback of streams. + */ +public abstract class Chunk implements Loadable { + + /** + * Value of {@link #type} for chunks containing unspecified data. + */ + public static final int TYPE_UNSPECIFIED = 0; + /** + * Value of {@link #type} for chunks containing media data. + */ + public static final int TYPE_MEDIA = 1; + /** + * Value of {@link #type} for chunks containing media initialization data. + */ + public static final int TYPE_MEDIA_INITIALIZATION = 2; + /** + * Value of {@link #type} for chunks containing drm related data. + */ + public static final int TYPE_DRM = 3; + /** + * Value of {@link #type} for chunks containing manifest or playlist data. + */ + public static final int TYPE_MANIFEST = 4; + /** + * Implementations may define custom {@link #type} codes greater than or equal to this value. + */ + public static final int TYPE_CUSTOM_BASE = 10000; + + /** + * Value of {@link #trigger} for a load whose reason is unspecified. + */ + public static final int TRIGGER_UNSPECIFIED = 0; + /** + * Value of {@link #trigger} for a load triggered by an initial format selection. + */ + public static final int TRIGGER_INITIAL = 1; + /** + * Value of {@link #trigger} for a load triggered by a user initiated format selection. + */ + public static final int TRIGGER_MANUAL = 2; + /** + * Value of {@link #trigger} for a load triggered by an adaptive format selection. + */ + public static final int TRIGGER_ADAPTIVE = 3; + /** + * Value of {@link #trigger} for a load triggered whilst in a trick play mode. + */ + public static final int TRIGGER_TRICK_PLAY = 4; + /** + * Implementations may define custom {@link #trigger} codes greater than or equal to this value. + */ + public static final int TRIGGER_CUSTOM_BASE = 10000; + /** + * Value of {@link #parentId} if no parent id need be specified. + */ + public static final int NO_PARENT_ID = -1; + + /** + * The type of the chunk. For reporting only. + */ + public final int type; + /** + * The reason why the chunk was generated. For reporting only. + */ + public final int trigger; + /** + * The format associated with the data being loaded, or null if the data being loaded is not + * associated with a specific format. + */ + public final Format format; + /** + * The {@link DataSpec} that defines the data to be loaded. + */ + public final DataSpec dataSpec; + /** + * Optional identifier for a parent from which this chunk originates. + */ + public final int parentId; + + protected final DataSource dataSource; + + /** + * @param dataSource The source from which the data should be loaded. + * @param dataSpec Defines the data to be loaded. {@code dataSpec.length} must not exceed + * {@link Integer#MAX_VALUE}. If {@code dataSpec.length == C.LENGTH_UNBOUNDED} then + * the length resolved by {@code dataSource.open(dataSpec)} must not exceed + * {@link Integer#MAX_VALUE}. + * @param type See {@link #type}. + * @param trigger See {@link #trigger}. + * @param format See {@link #format}. + * @param parentId See {@link #parentId}. + */ + public Chunk(DataSource dataSource, DataSpec dataSpec, int type, int trigger, Format format, + int parentId) { + this.dataSource = Assertions.checkNotNull(dataSource); + this.dataSpec = Assertions.checkNotNull(dataSpec); + this.type = type; + this.trigger = trigger; + this.format = format; + this.parentId = parentId; + } + + /** + * Gets the number of bytes that have been loaded. + * + * @return The number of bytes that have been loaded. + */ + public abstract long bytesLoaded(); + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/ChunkExtractorWrapper.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/ChunkExtractorWrapper.java new file mode 100755 index 000000000..cb0f27691 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/ChunkExtractorWrapper.java @@ -0,0 +1,145 @@ +/* + * 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.messenger.exoplayer.chunk; + +import org.telegram.messenger.exoplayer.MediaFormat; +import org.telegram.messenger.exoplayer.drm.DrmInitData; +import org.telegram.messenger.exoplayer.extractor.Extractor; +import org.telegram.messenger.exoplayer.extractor.ExtractorInput; +import org.telegram.messenger.exoplayer.extractor.ExtractorOutput; +import org.telegram.messenger.exoplayer.extractor.SeekMap; +import org.telegram.messenger.exoplayer.extractor.TrackOutput; +import org.telegram.messenger.exoplayer.util.Assertions; +import org.telegram.messenger.exoplayer.util.ParsableByteArray; + +import java.io.IOException; + +/** + * An {@link Extractor} wrapper for loading chunks containing a single track. + *

+ * The wrapper allows switching of the {@link SingleTrackOutput} that receives parsed data. + */ +public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput { + + /** + * Receives stream level data extracted by the wrapped {@link Extractor}. + */ + public interface SingleTrackOutput extends TrackOutput { + + /** + * @see ExtractorOutput#seekMap(SeekMap) + */ + void seekMap(SeekMap seekMap); + + /** + * @see ExtractorOutput#drmInitData(DrmInitData) + */ + void drmInitData(DrmInitData drmInitData); + + } + + private final Extractor extractor; + private boolean extractorInitialized; + private SingleTrackOutput output; + + // Accessed only on the loader thread. + private boolean seenTrack; + + /** + * @param extractor The extractor to wrap. + */ + public ChunkExtractorWrapper(Extractor extractor) { + this.extractor = extractor; + } + + /** + * Initializes the extractor to output to the provided {@link SingleTrackOutput}, and configures + * it to receive data from a new chunk. + * + * @param output The {@link SingleTrackOutput} that will receive the parsed data. + */ + public void init(SingleTrackOutput output) { + this.output = output; + if (!extractorInitialized) { + extractor.init(this); + extractorInitialized = true; + } else { + extractor.seek(); + } + } + + /** + * Reads from the provided {@link ExtractorInput}. + * + * @param input The {@link ExtractorInput} from which to read. + * @return One of {@link Extractor#RESULT_CONTINUE} and {@link Extractor#RESULT_END_OF_INPUT}. + * @throws IOException If an error occurred reading from the source. + * @throws InterruptedException If the thread was interrupted. + */ + public int read(ExtractorInput input) throws IOException, InterruptedException { + int result = extractor.read(input, null); + Assertions.checkState(result != Extractor.RESULT_SEEK); + return result; + } + + // ExtractorOutput implementation. + + @Override + public TrackOutput track(int id) { + Assertions.checkState(!seenTrack); + seenTrack = true; + return this; + } + + @Override + public void endTracks() { + Assertions.checkState(seenTrack); + } + + @Override + public void seekMap(SeekMap seekMap) { + output.seekMap(seekMap); + } + + @Override + public void drmInitData(DrmInitData drmInitData) { + output.drmInitData(drmInitData); + } + + // TrackOutput implementation. + + @Override + public void format(MediaFormat format) { + output.format(format); + } + + @Override + public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput) + throws IOException, InterruptedException { + return output.sampleData(input, length, allowEndOfInput); + } + + @Override + public void sampleData(ParsableByteArray data, int length) { + output.sampleData(data, length); + } + + @Override + public void sampleMetadata(long timeUs, int flags, int size, int offset, byte[] encryptionKey) { + output.sampleMetadata(timeUs, flags, size, offset, encryptionKey); + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/ChunkOperationHolder.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/ChunkOperationHolder.java new file mode 100755 index 000000000..fc5639f64 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/ChunkOperationHolder.java @@ -0,0 +1,53 @@ +/* + * 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.messenger.exoplayer.chunk; + +/** + * Holds a chunk operation, which consists of a either: + *

    + *
  • The number of {@link MediaChunk}s that should be retained on the queue ({@link #queueSize}) + * together with the next {@link Chunk} to load ({@link #chunk}). {@link #chunk} may be null if the + * next chunk cannot be provided yet.
  • + *
  • A flag indicating that the end of the stream has been reached ({@link #endOfStream}).
  • + *
+ */ +public final class ChunkOperationHolder { + + /** + * The number of {@link MediaChunk}s to retain in a queue. + */ + public int queueSize; + + /** + * The chunk. + */ + public Chunk chunk; + + /** + * Indicates that the end of the stream has been reached. + */ + public boolean endOfStream; + + /** + * Clears the holder. + */ + public void clear() { + queueSize = 0; + chunk = null; + endOfStream = false; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/ChunkSampleSource.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/ChunkSampleSource.java new file mode 100755 index 000000000..0b90c2177 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/ChunkSampleSource.java @@ -0,0 +1,688 @@ +/* + * 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.messenger.exoplayer.chunk; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.LoadControl; +import org.telegram.messenger.exoplayer.MediaFormat; +import org.telegram.messenger.exoplayer.MediaFormatHolder; +import org.telegram.messenger.exoplayer.SampleHolder; +import org.telegram.messenger.exoplayer.SampleSource; +import org.telegram.messenger.exoplayer.SampleSource.SampleSourceReader; +import org.telegram.messenger.exoplayer.TrackRenderer; +import org.telegram.messenger.exoplayer.extractor.DefaultTrackOutput; +import org.telegram.messenger.exoplayer.upstream.Loader; +import org.telegram.messenger.exoplayer.upstream.Loader.Loadable; +import org.telegram.messenger.exoplayer.util.Assertions; + +import android.os.Handler; +import android.os.SystemClock; + +import java.io.IOException; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +/** + * A {@link SampleSource} that loads media in {@link Chunk}s, which are themselves obtained from a + * {@link ChunkSource}. + */ +public class ChunkSampleSource implements SampleSource, SampleSourceReader, Loader.Callback { + + /** + * Interface definition for a callback to be notified of {@link ChunkSampleSource} events. + */ + public interface EventListener extends BaseChunkSampleSourceEventListener {} + + /** + * The default minimum number of times to retry loading data prior to failing. + */ + public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 3; + + protected final DefaultTrackOutput sampleQueue; + + private static final int STATE_IDLE = 0; + private static final int STATE_INITIALIZED = 1; + private static final int STATE_PREPARED = 2; + private static final int STATE_ENABLED = 3; + + private static final long NO_RESET_PENDING = Long.MIN_VALUE; + + private final int eventSourceId; + private final LoadControl loadControl; + private final ChunkSource chunkSource; + private final ChunkOperationHolder currentLoadableHolder; + private final LinkedList mediaChunks; + private final List readOnlyMediaChunks; + private final int bufferSizeContribution; + private final Handler eventHandler; + private final EventListener eventListener; + private final int minLoadableRetryCount; + + private int state; + private long downstreamPositionUs; + private long lastSeekPositionUs; + private long pendingResetPositionUs; + private long lastPerformedBufferOperation; + private boolean pendingDiscontinuity; + + private Loader loader; + private boolean loadingFinished; + private IOException currentLoadableException; + private int enabledTrackCount; + private int currentLoadableExceptionCount; + private long currentLoadableExceptionTimestamp; + private long currentLoadStartTimeMs; + + private MediaFormat downstreamMediaFormat; + private Format downstreamFormat; + + /** + * @param chunkSource A {@link ChunkSource} from which chunks to load are obtained. + * @param loadControl Controls when the source is permitted to load data. + * @param bufferSizeContribution The contribution of this source to the media buffer, in bytes. + */ + public ChunkSampleSource(ChunkSource chunkSource, LoadControl loadControl, + int bufferSizeContribution) { + this(chunkSource, loadControl, bufferSizeContribution, null, null, 0); + } + + /** + * @param chunkSource A {@link ChunkSource} from which chunks to load are obtained. + * @param loadControl Controls when the source is permitted to load data. + * @param bufferSizeContribution The contribution of this source to the media buffer, in bytes. + * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be + * null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + * @param eventSourceId An identifier that gets passed to {@code eventListener} methods. + */ + public ChunkSampleSource(ChunkSource chunkSource, LoadControl loadControl, + int bufferSizeContribution, Handler eventHandler, EventListener eventListener, + int eventSourceId) { + this(chunkSource, loadControl, bufferSizeContribution, eventHandler, eventListener, + eventSourceId, DEFAULT_MIN_LOADABLE_RETRY_COUNT); + } + + /** + * @param chunkSource A {@link ChunkSource} from which chunks to load are obtained. + * @param loadControl Controls when the source is permitted to load data. + * @param bufferSizeContribution The contribution of this source to the media buffer, in bytes. + * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be + * null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + * @param eventSourceId An identifier that gets passed to {@code eventListener} methods. + * @param minLoadableRetryCount The minimum number of times that the source should retry a load + * before propagating an error. + */ + public ChunkSampleSource(ChunkSource chunkSource, LoadControl loadControl, + int bufferSizeContribution, Handler eventHandler, EventListener eventListener, + int eventSourceId, int minLoadableRetryCount) { + this.chunkSource = chunkSource; + this.loadControl = loadControl; + this.bufferSizeContribution = bufferSizeContribution; + this.eventHandler = eventHandler; + this.eventListener = eventListener; + this.eventSourceId = eventSourceId; + this.minLoadableRetryCount = minLoadableRetryCount; + currentLoadableHolder = new ChunkOperationHolder(); + mediaChunks = new LinkedList<>(); + readOnlyMediaChunks = Collections.unmodifiableList(mediaChunks); + sampleQueue = new DefaultTrackOutput(loadControl.getAllocator()); + state = STATE_IDLE; + pendingResetPositionUs = NO_RESET_PENDING; + } + + @Override + public SampleSourceReader register() { + Assertions.checkState(state == STATE_IDLE); + state = STATE_INITIALIZED; + return this; + } + + @Override + public boolean prepare(long positionUs) { + Assertions.checkState(state == STATE_INITIALIZED || state == STATE_PREPARED); + if (state == STATE_PREPARED) { + return true; + } else if (!chunkSource.prepare()) { + return false; + } + if (chunkSource.getTrackCount() > 0) { + loader = new Loader("Loader:" + chunkSource.getFormat(0).mimeType); + } + state = STATE_PREPARED; + return true; + } + + @Override + public int getTrackCount() { + Assertions.checkState(state == STATE_PREPARED || state == STATE_ENABLED); + return chunkSource.getTrackCount(); + } + + @Override + public MediaFormat getFormat(int track) { + Assertions.checkState(state == STATE_PREPARED || state == STATE_ENABLED); + return chunkSource.getFormat(track); + } + + @Override + public void enable(int track, long positionUs) { + Assertions.checkState(state == STATE_PREPARED); + Assertions.checkState(enabledTrackCount++ == 0); + state = STATE_ENABLED; + chunkSource.enable(track); + loadControl.register(this, bufferSizeContribution); + downstreamFormat = null; + downstreamMediaFormat = null; + downstreamPositionUs = positionUs; + lastSeekPositionUs = positionUs; + pendingDiscontinuity = false; + restartFrom(positionUs); + } + + @Override + public void disable(int track) { + Assertions.checkState(state == STATE_ENABLED); + Assertions.checkState(--enabledTrackCount == 0); + state = STATE_PREPARED; + try { + chunkSource.disable(mediaChunks); + } finally { + loadControl.unregister(this); + if (loader.isLoading()) { + loader.cancelLoading(); + } else { + sampleQueue.clear(); + mediaChunks.clear(); + clearCurrentLoadable(); + loadControl.trimAllocator(); + } + } + } + + @Override + public boolean continueBuffering(int track, long positionUs) { + Assertions.checkState(state == STATE_ENABLED); + downstreamPositionUs = positionUs; + chunkSource.continueBuffering(positionUs); + updateLoadControl(); + return loadingFinished || !sampleQueue.isEmpty(); + } + + @Override + public long readDiscontinuity(int track) { + if (pendingDiscontinuity) { + pendingDiscontinuity = false; + return lastSeekPositionUs; + } + return NO_DISCONTINUITY; + } + + @Override + public int readData(int track, long positionUs, MediaFormatHolder formatHolder, + SampleHolder sampleHolder) { + Assertions.checkState(state == STATE_ENABLED); + downstreamPositionUs = positionUs; + + if (pendingDiscontinuity || isPendingReset()) { + return NOTHING_READ; + } + + boolean haveSamples = !sampleQueue.isEmpty(); + BaseMediaChunk currentChunk = mediaChunks.getFirst(); + while (haveSamples && mediaChunks.size() > 1 + && mediaChunks.get(1).getFirstSampleIndex() <= sampleQueue.getReadIndex()) { + mediaChunks.removeFirst(); + currentChunk = mediaChunks.getFirst(); + } + + Format format = currentChunk.format; + if (!format.equals(downstreamFormat)) { + notifyDownstreamFormatChanged(format, currentChunk.trigger, currentChunk.startTimeUs); + } + downstreamFormat = format; + + if (haveSamples || currentChunk.isMediaFormatFinal) { + MediaFormat mediaFormat = currentChunk.getMediaFormat(); + if (!mediaFormat.equals(downstreamMediaFormat)) { + formatHolder.format = mediaFormat; + formatHolder.drmInitData = currentChunk.getDrmInitData(); + downstreamMediaFormat = mediaFormat; + return FORMAT_READ; + } + // If mediaFormat and downstreamMediaFormat are equal but different objects then the equality + // check above will have been expensive, comparing the fields in each format. We update + // downstreamMediaFormat here so that referential equality can be cheaply established during + // subsequent calls. + downstreamMediaFormat = mediaFormat; + } + + if (!haveSamples) { + if (loadingFinished) { + return END_OF_STREAM; + } + return NOTHING_READ; + } + + if (sampleQueue.getSample(sampleHolder)) { + boolean decodeOnly = sampleHolder.timeUs < lastSeekPositionUs; + sampleHolder.flags |= decodeOnly ? C.SAMPLE_FLAG_DECODE_ONLY : 0; + onSampleRead(currentChunk, sampleHolder); + return SAMPLE_READ; + } + + return NOTHING_READ; + } + + @Override + public void seekToUs(long positionUs) { + Assertions.checkState(state == STATE_ENABLED); + + long currentPositionUs = isPendingReset() ? pendingResetPositionUs : downstreamPositionUs; + downstreamPositionUs = positionUs; + lastSeekPositionUs = positionUs; + if (currentPositionUs == positionUs) { + return; + } + + // If we're not pending a reset, see if we can seek within the sample queue. + boolean seekInsideBuffer = !isPendingReset() && sampleQueue.skipToKeyframeBefore(positionUs); + if (seekInsideBuffer) { + // We succeeded. All we need to do is discard any chunks that we've moved past. + boolean haveSamples = !sampleQueue.isEmpty(); + while (haveSamples && mediaChunks.size() > 1 + && mediaChunks.get(1).getFirstSampleIndex() <= sampleQueue.getReadIndex()) { + mediaChunks.removeFirst(); + } + } else { + // We failed, and need to restart. + restartFrom(positionUs); + } + // Either way, we need to send a discontinuity to the downstream components. + pendingDiscontinuity = true; + } + + @Override + public void maybeThrowError() throws IOException { + if (currentLoadableException != null && currentLoadableExceptionCount > minLoadableRetryCount) { + throw currentLoadableException; + } else if (currentLoadableHolder.chunk == null) { + chunkSource.maybeThrowError(); + } + } + + @Override + public long getBufferedPositionUs() { + Assertions.checkState(state == STATE_ENABLED); + if (isPendingReset()) { + return pendingResetPositionUs; + } else if (loadingFinished) { + return TrackRenderer.END_OF_TRACK_US; + } else { + long largestParsedTimestampUs = sampleQueue.getLargestParsedTimestampUs(); + return largestParsedTimestampUs == Long.MIN_VALUE ? downstreamPositionUs + : largestParsedTimestampUs; + } + } + + @Override + public void release() { + Assertions.checkState(state != STATE_ENABLED); + if (loader != null) { + loader.release(); + loader = null; + } + state = STATE_IDLE; + } + + @Override + public void onLoadCompleted(Loadable loadable) { + long now = SystemClock.elapsedRealtime(); + long loadDurationMs = now - currentLoadStartTimeMs; + Chunk currentLoadable = currentLoadableHolder.chunk; + chunkSource.onChunkLoadCompleted(currentLoadable); + if (isMediaChunk(currentLoadable)) { + BaseMediaChunk mediaChunk = (BaseMediaChunk) currentLoadable; + notifyLoadCompleted(currentLoadable.bytesLoaded(), mediaChunk.type, mediaChunk.trigger, + mediaChunk.format, mediaChunk.startTimeUs, mediaChunk.endTimeUs, now, loadDurationMs); + } else { + notifyLoadCompleted(currentLoadable.bytesLoaded(), currentLoadable.type, + currentLoadable.trigger, currentLoadable.format, -1, -1, now, loadDurationMs); + } + clearCurrentLoadable(); + updateLoadControl(); + } + + @Override + public void onLoadCanceled(Loadable loadable) { + Chunk currentLoadable = currentLoadableHolder.chunk; + notifyLoadCanceled(currentLoadable.bytesLoaded()); + clearCurrentLoadable(); + if (state == STATE_ENABLED) { + restartFrom(pendingResetPositionUs); + } else { + sampleQueue.clear(); + mediaChunks.clear(); + clearCurrentLoadable(); + loadControl.trimAllocator(); + } + } + + @Override + public void onLoadError(Loadable loadable, IOException e) { + currentLoadableException = e; + currentLoadableExceptionCount++; + currentLoadableExceptionTimestamp = SystemClock.elapsedRealtime(); + notifyLoadError(e); + chunkSource.onChunkLoadError(currentLoadableHolder.chunk, e); + updateLoadControl(); + } + + /** + * Called when a sample has been read. Can be used to perform any modifications necessary before + * the sample is returned. + * + * @param mediaChunk The chunk from which the sample was obtained. + * @param sampleHolder Holds the read sample. + */ + protected void onSampleRead(MediaChunk mediaChunk, SampleHolder sampleHolder) { + // Do nothing. + } + + private void restartFrom(long positionUs) { + pendingResetPositionUs = positionUs; + loadingFinished = false; + if (loader.isLoading()) { + loader.cancelLoading(); + } else { + sampleQueue.clear(); + mediaChunks.clear(); + clearCurrentLoadable(); + updateLoadControl(); + } + } + + private void clearCurrentLoadable() { + currentLoadableHolder.chunk = null; + clearCurrentLoadableException(); + } + + private void clearCurrentLoadableException() { + currentLoadableException = null; + currentLoadableExceptionCount = 0; + } + + private void updateLoadControl() { + long now = SystemClock.elapsedRealtime(); + long nextLoadPositionUs = getNextLoadPositionUs(); + boolean isBackedOff = currentLoadableException != null; + boolean loadingOrBackedOff = loader.isLoading() || isBackedOff; + + // If we're not loading or backed off, evaluate the operation if (a) we don't have the next + // chunk yet and we're not finished, or (b) if the last evaluation was over 2000ms ago. + if (!loadingOrBackedOff && ((currentLoadableHolder.chunk == null && nextLoadPositionUs != -1) + || (now - lastPerformedBufferOperation > 2000))) { + // Perform the evaluation. + lastPerformedBufferOperation = now; + doChunkOperation(); + boolean chunksDiscarded = discardUpstreamMediaChunks(currentLoadableHolder.queueSize); + // Update the next load position as appropriate. + if (currentLoadableHolder.chunk == null) { + // Set loadPosition to -1 to indicate that we don't have anything to load. + nextLoadPositionUs = -1; + } else if (chunksDiscarded) { + // Chunks were discarded, so we need to re-evaluate the load position. + nextLoadPositionUs = getNextLoadPositionUs(); + } + } + + // Update the control with our current state, and determine whether we're the next loader. + boolean nextLoader = loadControl.update(this, downstreamPositionUs, nextLoadPositionUs, + loadingOrBackedOff); + + if (isBackedOff) { + long elapsedMillis = now - currentLoadableExceptionTimestamp; + if (elapsedMillis >= getRetryDelayMillis(currentLoadableExceptionCount)) { + resumeFromBackOff(); + } + return; + } + + if (!loader.isLoading() && nextLoader) { + maybeStartLoading(); + } + } + + /** + * Gets the next load time, assuming that the next load starts where the previous chunk ended (or + * from the pending reset time, if there is one). + */ + private long getNextLoadPositionUs() { + if (isPendingReset()) { + return pendingResetPositionUs; + } else { + return loadingFinished ? -1 : mediaChunks.getLast().endTimeUs; + } + } + + /** + * Resumes loading. + *

+ * If the {@link ChunkSource} returns a chunk equivalent to the backed off chunk B, then the + * loading of B will be resumed. In all other cases B will be discarded and the new chunk will + * be loaded. + */ + private void resumeFromBackOff() { + currentLoadableException = null; + + Chunk backedOffChunk = currentLoadableHolder.chunk; + if (!isMediaChunk(backedOffChunk)) { + doChunkOperation(); + discardUpstreamMediaChunks(currentLoadableHolder.queueSize); + if (currentLoadableHolder.chunk == backedOffChunk) { + // Chunk was unchanged. Resume loading. + loader.startLoading(backedOffChunk, this); + } else { + // Chunk was changed. Notify that the existing load was canceled. + notifyLoadCanceled(backedOffChunk.bytesLoaded()); + // Start loading the replacement. + maybeStartLoading(); + } + return; + } + + if (backedOffChunk == mediaChunks.getFirst()) { + // We're not able to clear the first media chunk, so we have no choice but to continue + // loading it. + loader.startLoading(backedOffChunk, this); + return; + } + + // The current loadable is the last media chunk. Remove it before we invoke the chunk source, + // and add it back again afterwards. + BaseMediaChunk removedChunk = mediaChunks.removeLast(); + Assertions.checkState(backedOffChunk == removedChunk); + doChunkOperation(); + mediaChunks.add(removedChunk); + + if (currentLoadableHolder.chunk == backedOffChunk) { + // Chunk was unchanged. Resume loading. + loader.startLoading(backedOffChunk, this); + } else { + // Chunk was changed. Notify that the existing load was canceled. + notifyLoadCanceled(backedOffChunk.bytesLoaded()); + // This call will remove and release at least one chunk from the end of mediaChunks. Since + // the current loadable is the last media chunk, it is guaranteed to be removed. + discardUpstreamMediaChunks(currentLoadableHolder.queueSize); + clearCurrentLoadableException(); + maybeStartLoading(); + } + } + + private void maybeStartLoading() { + Chunk currentLoadable = currentLoadableHolder.chunk; + if (currentLoadable == null) { + // Nothing to load. + return; + } + currentLoadStartTimeMs = SystemClock.elapsedRealtime(); + if (isMediaChunk(currentLoadable)) { + BaseMediaChunk mediaChunk = (BaseMediaChunk) currentLoadable; + mediaChunk.init(sampleQueue); + mediaChunks.add(mediaChunk); + if (isPendingReset()) { + pendingResetPositionUs = NO_RESET_PENDING; + } + notifyLoadStarted(mediaChunk.dataSpec.length, mediaChunk.type, mediaChunk.trigger, + mediaChunk.format, mediaChunk.startTimeUs, mediaChunk.endTimeUs); + } else { + notifyLoadStarted(currentLoadable.dataSpec.length, currentLoadable.type, + currentLoadable.trigger, currentLoadable.format, -1, -1); + } + loader.startLoading(currentLoadable, this); + } + + /** + * Sets up the {@link #currentLoadableHolder}, passes it to the chunk source to cause it to be + * updated with the next operation, and updates {@link #loadingFinished} if the end of the stream + * is reached. + */ + private void doChunkOperation() { + currentLoadableHolder.endOfStream = false; + currentLoadableHolder.queueSize = readOnlyMediaChunks.size(); + chunkSource.getChunkOperation(readOnlyMediaChunks, + pendingResetPositionUs != NO_RESET_PENDING ? pendingResetPositionUs : downstreamPositionUs, + currentLoadableHolder); + loadingFinished = currentLoadableHolder.endOfStream; + } + + /** + * Discard upstream media chunks until the queue length is equal to the length specified. + * + * @param queueLength The desired length of the queue. + * @return True if chunks were discarded. False otherwise. + */ + private boolean discardUpstreamMediaChunks(int queueLength) { + if (mediaChunks.size() <= queueLength) { + return false; + } + long startTimeUs = 0; + long endTimeUs = mediaChunks.getLast().endTimeUs; + + BaseMediaChunk removed = null; + while (mediaChunks.size() > queueLength) { + removed = mediaChunks.removeLast(); + startTimeUs = removed.startTimeUs; + loadingFinished = false; + } + sampleQueue.discardUpstreamSamples(removed.getFirstSampleIndex()); + + notifyUpstreamDiscarded(startTimeUs, endTimeUs); + return true; + } + + private boolean isMediaChunk(Chunk chunk) { + return chunk instanceof BaseMediaChunk; + } + + private boolean isPendingReset() { + return pendingResetPositionUs != NO_RESET_PENDING; + } + + private long getRetryDelayMillis(long errorCount) { + return Math.min((errorCount - 1) * 1000, 5000); + } + + protected final long usToMs(long timeUs) { + return timeUs / 1000; + } + + private void notifyLoadStarted(final long length, final int type, final int trigger, + final Format format, final long mediaStartTimeUs, final long mediaEndTimeUs) { + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onLoadStarted(eventSourceId, length, type, trigger, format, + usToMs(mediaStartTimeUs), usToMs(mediaEndTimeUs)); + } + }); + } + } + + private void notifyLoadCompleted(final long bytesLoaded, final int type, final int trigger, + final Format format, final long mediaStartTimeUs, final long mediaEndTimeUs, + final long elapsedRealtimeMs, final long loadDurationMs) { + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onLoadCompleted(eventSourceId, bytesLoaded, type, trigger, format, + usToMs(mediaStartTimeUs), usToMs(mediaEndTimeUs), elapsedRealtimeMs, loadDurationMs); + } + }); + } + } + + private void notifyLoadCanceled(final long bytesLoaded) { + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onLoadCanceled(eventSourceId, bytesLoaded); + } + }); + } + } + + private void notifyLoadError(final IOException e) { + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onLoadError(eventSourceId, e); + } + }); + } + } + + private void notifyUpstreamDiscarded(final long mediaStartTimeUs, final long mediaEndTimeUs) { + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onUpstreamDiscarded(eventSourceId, usToMs(mediaStartTimeUs), + usToMs(mediaEndTimeUs)); + } + }); + } + } + + private void notifyDownstreamFormatChanged(final Format format, final int trigger, + final long positionUs) { + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onDownstreamFormatChanged(eventSourceId, format, trigger, + usToMs(positionUs)); + } + }); + } + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/ChunkSource.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/ChunkSource.java new file mode 100755 index 000000000..8d72e1538 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/ChunkSource.java @@ -0,0 +1,139 @@ +/* + * 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.messenger.exoplayer.chunk; + +import org.telegram.messenger.exoplayer.MediaFormat; + +import java.io.IOException; +import java.util.List; + +/** + * A provider of {@link Chunk}s for a {@link ChunkSampleSource} to load. + */ +/* + * TODO: Share more state between this interface and {@link ChunkSampleSource}. In particular + * implementations of this class needs to know about errors, and should be more tightly integrated + * into the process of resuming loading of a chunk after an error occurs. + */ +public interface ChunkSource { + + /** + * If the source is currently having difficulty preparing or providing chunks, then this method + * throws the underlying error. Otherwise does nothing. + * + * @throws IOException The underlying error. + */ + void maybeThrowError() throws IOException; + + /** + * Prepares the source. + *

+ * The method can be called repeatedly until the return value indicates success. + * + * @return True if the source was prepared, false otherwise. + */ + boolean prepare(); + + /** + * Returns the number of tracks exposed by the source. + *

+ * This method should only be called after the source has been prepared. + * + * @return The number of tracks. + */ + int getTrackCount(); + + /** + * Gets the format of the specified track. + *

+ * This method should only be called after the source has been prepared. + * + * @param track The track index. + * @return The format of the track. + */ + MediaFormat getFormat(int track); + + /** + * Enable the source for the specified track. + *

+ * This method should only be called after the source has been prepared, and when the source is + * disabled. + * + * @param track The track index. + */ + void enable(int track); + + /** + * Indicates to the source that it should still be checking for updates to the stream. + *

+ * This method should only be called when the source is enabled. + * + * @param playbackPositionUs The current playback position. + */ + void continueBuffering(long playbackPositionUs); + + /** + * Updates the provided {@link ChunkOperationHolder} to contain the next operation that should + * be performed by the calling {@link ChunkSampleSource}. + *

+ * This method should only be called when the source is enabled. + * + * @param queue A representation of the currently buffered {@link MediaChunk}s. + * @param playbackPositionUs The current playback position. If the queue is empty then this + * parameter is the position from which playback is expected to start (or restart) and hence + * should be interpreted as a seek position. + * @param out A holder for the next operation, whose {@link ChunkOperationHolder#endOfStream} is + * initially set to false, whose {@link ChunkOperationHolder#queueSize} is initially equal to + * the length of the queue, and whose {@link ChunkOperationHolder#chunk} is initially equal to + * null or a {@link Chunk} previously supplied by the {@link ChunkSource} that the caller has + * not yet finished loading. In the latter case the chunk can either be replaced or left + * unchanged. Note that leaving the chunk unchanged is both preferred and more efficient than + * replacing it with a new but identical chunk. + */ + void getChunkOperation(List queue, long playbackPositionUs, + ChunkOperationHolder out); + + /** + * Invoked when the {@link ChunkSampleSource} has finished loading a chunk obtained from this + * source. + *

+ * This method should only be called when the source is enabled. + * + * @param chunk The chunk whose load has been completed. + */ + void onChunkLoadCompleted(Chunk chunk); + + /** + * Invoked when the {@link ChunkSampleSource} encounters an error loading a chunk obtained from + * this source. + *

+ * This method should only be called when the source is enabled. + * + * @param chunk The chunk whose load encountered the error. + * @param e The error. + */ + void onChunkLoadError(Chunk chunk, Exception e); + + /** + * Disables the source. + *

+ * This method should only be called when the source is enabled. + * + * @param queue A representation of the currently buffered {@link MediaChunk}s. + */ + void disable(List queue); + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/ContainerMediaChunk.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/ContainerMediaChunk.java new file mode 100755 index 000000000..b4a96388c --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/ContainerMediaChunk.java @@ -0,0 +1,191 @@ +/* + * 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.messenger.exoplayer.chunk; + +import org.telegram.messenger.exoplayer.MediaFormat; +import org.telegram.messenger.exoplayer.chunk.ChunkExtractorWrapper.SingleTrackOutput; +import org.telegram.messenger.exoplayer.drm.DrmInitData; +import org.telegram.messenger.exoplayer.extractor.DefaultExtractorInput; +import org.telegram.messenger.exoplayer.extractor.Extractor; +import org.telegram.messenger.exoplayer.extractor.ExtractorInput; +import org.telegram.messenger.exoplayer.extractor.SeekMap; +import org.telegram.messenger.exoplayer.upstream.DataSource; +import org.telegram.messenger.exoplayer.upstream.DataSpec; +import org.telegram.messenger.exoplayer.util.ParsableByteArray; +import org.telegram.messenger.exoplayer.util.Util; + +import java.io.IOException; + +/** + * A {@link BaseMediaChunk} that uses an {@link Extractor} to parse sample data. + */ +public class ContainerMediaChunk extends BaseMediaChunk implements SingleTrackOutput { + + private final ChunkExtractorWrapper extractorWrapper; + private final long sampleOffsetUs; + private final int adaptiveMaxWidth; + private final int adaptiveMaxHeight; + + private MediaFormat mediaFormat; + private DrmInitData drmInitData; + + private volatile int bytesLoaded; + private volatile boolean loadCanceled; + + /** + * @param dataSource A {@link DataSource} for loading the data. + * @param dataSpec Defines the data to be loaded. + * @param trigger The reason for this chunk being selected. + * @param format The format of the stream to which this chunk belongs. + * @param startTimeUs The start time of the media contained by the chunk, in microseconds. + * @param endTimeUs The end time of the media contained by the chunk, in microseconds. + * @param chunkIndex The index of the chunk. + * @param sampleOffsetUs An offset to add to the sample timestamps parsed by the extractor. + * @param extractorWrapper A wrapped extractor to use for parsing the data. + * @param mediaFormat The {@link MediaFormat} of the chunk, if known. May be null if the data is + * known to define its own format. + * @param adaptiveMaxWidth If this chunk contains video and is part of an adaptive playback, this + * is the maximum width of the video in pixels that will be encountered during the playback. + * {@link MediaFormat#NO_VALUE} otherwise. + * @param adaptiveMaxHeight If this chunk contains video and is part of an adaptive playback, this + * is the maximum height of the video in pixels that will be encountered during the playback. + * {@link MediaFormat#NO_VALUE} otherwise. + * @param drmInitData The {@link DrmInitData} for the chunk. Null if the media is not drm + * protected. May also be null if the data is known to define its own initialization data. + * @param isMediaFormatFinal True if {@code mediaFormat} and {@code drmInitData} are known to be + * correct and final. False if the data may define its own format or initialization data. + * @param parentId Identifier for a parent from which this chunk originates. + */ + public ContainerMediaChunk(DataSource dataSource, DataSpec dataSpec, int trigger, Format format, + long startTimeUs, long endTimeUs, int chunkIndex, long sampleOffsetUs, + ChunkExtractorWrapper extractorWrapper, MediaFormat mediaFormat, int adaptiveMaxWidth, + int adaptiveMaxHeight, DrmInitData drmInitData, boolean isMediaFormatFinal, int parentId) { + super(dataSource, dataSpec, trigger, format, startTimeUs, endTimeUs, chunkIndex, + isMediaFormatFinal, parentId); + this.extractorWrapper = extractorWrapper; + this.sampleOffsetUs = sampleOffsetUs; + this.adaptiveMaxWidth = adaptiveMaxWidth; + this.adaptiveMaxHeight = adaptiveMaxHeight; + this.mediaFormat = getAdjustedMediaFormat(mediaFormat, sampleOffsetUs, adaptiveMaxWidth, + adaptiveMaxHeight); + this.drmInitData = drmInitData; + } + + @Override + public final long bytesLoaded() { + return bytesLoaded; + } + + @Override + public final MediaFormat getMediaFormat() { + return mediaFormat; + } + + @Override + public final DrmInitData getDrmInitData() { + return drmInitData; + } + + // SingleTrackOutput implementation. + + @Override + public final void seekMap(SeekMap seekMap) { + // Do nothing. + } + + @Override + public final void drmInitData(DrmInitData drmInitData) { + this.drmInitData = drmInitData; + } + + @Override + public final void format(MediaFormat mediaFormat) { + this.mediaFormat = getAdjustedMediaFormat(mediaFormat, sampleOffsetUs, adaptiveMaxWidth, + adaptiveMaxHeight); + } + + @Override + public final int sampleData(ExtractorInput input, int length, boolean allowEndOfInput) + throws IOException, InterruptedException { + return getOutput().sampleData(input, length, allowEndOfInput); + } + + @Override + public final void sampleData(ParsableByteArray data, int length) { + getOutput().sampleData(data, length); + } + + @Override + public final void sampleMetadata(long timeUs, int flags, int size, int offset, + byte[] encryptionKey) { + getOutput().sampleMetadata(timeUs + sampleOffsetUs, flags, size, offset, encryptionKey); + } + + // Loadable implementation. + + @Override + public final void cancelLoad() { + loadCanceled = true; + } + + @Override + public final boolean isLoadCanceled() { + return loadCanceled; + } + + @SuppressWarnings("NonAtomicVolatileUpdate") + @Override + public final void load() throws IOException, InterruptedException { + DataSpec loadDataSpec = Util.getRemainderDataSpec(dataSpec, bytesLoaded); + try { + // Create and open the input. + ExtractorInput input = new DefaultExtractorInput(dataSource, + loadDataSpec.absoluteStreamPosition, dataSource.open(loadDataSpec)); + if (bytesLoaded == 0) { + // Set the target to ourselves. + extractorWrapper.init(this); + } + // Load and parse the sample data. + try { + int result = Extractor.RESULT_CONTINUE; + while (result == Extractor.RESULT_CONTINUE && !loadCanceled) { + result = extractorWrapper.read(input); + } + } finally { + bytesLoaded = (int) (input.getPosition() - dataSpec.absoluteStreamPosition); + } + } finally { + dataSource.close(); + } + } + + // Private methods. + + private static MediaFormat getAdjustedMediaFormat(MediaFormat format, long sampleOffsetUs, + int adaptiveMaxWidth, int adaptiveMaxHeight) { + if (format == null) { + return null; + } + if (sampleOffsetUs != 0 && format.subsampleOffsetUs != MediaFormat.OFFSET_SAMPLE_RELATIVE) { + format = format.copyWithSubsampleOffsetUs(format.subsampleOffsetUs + sampleOffsetUs); + } + if (adaptiveMaxWidth != MediaFormat.NO_VALUE || adaptiveMaxHeight != MediaFormat.NO_VALUE) { + format = format.copyWithMaxVideoDimensions(adaptiveMaxWidth, adaptiveMaxHeight); + } + return format; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/DataChunk.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/DataChunk.java new file mode 100755 index 000000000..966c7a143 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/DataChunk.java @@ -0,0 +1,124 @@ +/* + * 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.messenger.exoplayer.chunk; + +import org.telegram.messenger.exoplayer.upstream.DataSource; +import org.telegram.messenger.exoplayer.upstream.DataSpec; + +import java.io.IOException; +import java.util.Arrays; + +/** + * A base class for {@link Chunk} implementations where the data should be loaded into a + * {@code byte[]} before being consumed. + */ +public abstract class DataChunk extends Chunk { + + private static final int READ_GRANULARITY = 16 * 1024; + + private byte[] data; + private int limit; + + private volatile boolean loadCanceled; + + /** + * @param dataSource The source from which the data should be loaded. + * @param dataSpec Defines the data to be loaded. {@code dataSpec.length} must not exceed + * {@link Integer#MAX_VALUE}. If {@code dataSpec.length == C.LENGTH_UNBOUNDED} then + * the length resolved by {@code dataSource.open(dataSpec)} must not exceed + * {@link Integer#MAX_VALUE}. + * @param type See {@link #type}. + * @param trigger See {@link #trigger}. + * @param format See {@link #format}. + * @param parentId Identifier for a parent from which this chunk originates. + * @param data An optional recycled array that can be used as a holder for the data. + */ + public DataChunk(DataSource dataSource, DataSpec dataSpec, int type, int trigger, Format format, + int parentId, byte[] data) { + super(dataSource, dataSpec, type, trigger, format, parentId); + this.data = data; + } + + /** + * Returns the array in which the data is held. + *

+ * This method should be used for recycling the holder only, and not for reading the data. + * + * @return The array in which the data is held. + */ + public byte[] getDataHolder() { + return data; + } + + @Override + public long bytesLoaded() { + return limit; + } + + // Loadable implementation + + @Override + public final void cancelLoad() { + loadCanceled = true; + } + + @Override + public final boolean isLoadCanceled() { + return loadCanceled; + } + + @Override + public final void load() throws IOException, InterruptedException { + try { + dataSource.open(dataSpec); + limit = 0; + int bytesRead = 0; + while (bytesRead != -1 && !loadCanceled) { + maybeExpandData(); + bytesRead = dataSource.read(data, limit, READ_GRANULARITY); + if (bytesRead != -1) { + limit += bytesRead; + } + } + if (!loadCanceled) { + consume(data, limit); + } + } finally { + dataSource.close(); + } + } + + /** + * Invoked by {@link #load()}. Implementations should override this method to consume the loaded + * data. + * + * @param data An array containing the data. + * @param limit The limit of the data. + * @throws IOException If an error occurs consuming the loaded data. + */ + protected abstract void consume(byte[] data, int limit) throws IOException; + + private void maybeExpandData() { + if (data == null) { + data = new byte[READ_GRANULARITY]; + } else if (data.length < limit + READ_GRANULARITY) { + // The new length is calculated as (data.length + READ_GRANULARITY) rather than + // (limit + READ_GRANULARITY) in order to avoid small increments in the length. + data = Arrays.copyOf(data, data.length + READ_GRANULARITY); + } + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/Format.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/Format.java new file mode 100755 index 000000000..cd0331233 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/Format.java @@ -0,0 +1,173 @@ +/* + * 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.messenger.exoplayer.chunk; + +import org.telegram.messenger.exoplayer.util.Assertions; + +import java.util.Comparator; + +/** + * Defines the high level format of a media stream. + */ +public class Format { + + /** + * Sorts {@link Format} objects in order of decreasing bandwidth. + */ + public static final class DecreasingBandwidthComparator implements Comparator { + + @Override + public int compare(Format a, Format b) { + return b.bitrate - a.bitrate; + } + + } + + /** + * An identifier for the format. + */ + public final String id; + + /** + * The mime type of the format. + */ + public final String mimeType; + + /** + * The average bandwidth in bits per second. + */ + public final int bitrate; + + /** + * The width of the video in pixels, or -1 if unknown or not applicable. + */ + public final int width; + + /** + * The height of the video in pixels, or -1 if unknown or not applicable. + */ + public final int height; + + /** + * The video frame rate in frames per second, or -1 if unknown or not applicable. + */ + public final float frameRate; + + /** + * The number of audio channels, or -1 if unknown or not applicable. + */ + public final int audioChannels; + + /** + * The audio sampling rate in Hz, or -1 if unknown or not applicable. + */ + public final int audioSamplingRate; + + /** + * The codecs used to decode the format. Can be {@code null} if unknown. + */ + public final String codecs; + + /** + * The language of the format. Can be null if unknown. + *

+ * The language codes are two-letter lowercase ISO language codes (such as "en") as defined by + * ISO 639-1. + */ + public final String language; + + /** + * @param id The format identifier. + * @param mimeType The format mime type. + * @param width The width of the video in pixels, or -1 if unknown or not applicable. + * @param height The height of the video in pixels, or -1 if unknown or not applicable. + * @param frameRate The frame rate of the video in frames per second, or -1 if unknown or not + * applicable. + * @param numChannels The number of audio channels, or -1 if unknown or not applicable. + * @param audioSamplingRate The audio sampling rate in Hz, or -1 if unknown or not applicable. + * @param bitrate The average bandwidth of the format in bits per second. + */ + public Format(String id, String mimeType, int width, int height, float frameRate, int numChannels, + int audioSamplingRate, int bitrate) { + this(id, mimeType, width, height, frameRate, numChannels, audioSamplingRate, bitrate, null); + } + + /** + * @param id The format identifier. + * @param mimeType The format mime type. + * @param width The width of the video in pixels, or -1 if unknown or not applicable. + * @param height The height of the video in pixels, or -1 if unknown or not applicable. + * @param frameRate The frame rate of the video in frames per second, or -1 if unknown or not + * applicable. + * @param numChannels The number of audio channels, or -1 if unknown or not applicable. + * @param audioSamplingRate The audio sampling rate in Hz, or -1 if unknown or not applicable. + * @param bitrate The average bandwidth of the format in bits per second. + * @param language The language of the format. + */ + public Format(String id, String mimeType, int width, int height, float frameRate, int numChannels, + int audioSamplingRate, int bitrate, String language) { + this(id, mimeType, width, height, frameRate, numChannels, audioSamplingRate, bitrate, language, + null); + } + + /** + * @param id The format identifier. + * @param mimeType The format mime type. + * @param width The width of the video in pixels, or -1 if unknown or not applicable. + * @param height The height of the video in pixels, or -1 if unknown or not applicable. + * @param frameRate The frame rate of the video in frames per second, or -1 if unknown or not + * applicable. + * @param audioChannels The number of audio channels, or -1 if unknown or not applicable. + * @param audioSamplingRate The audio sampling rate in Hz, or -1 if unknown or not applicable. + * @param bitrate The average bandwidth of the format in bits per second. + * @param language The language of the format. + * @param codecs The codecs used to decode the format. + */ + public Format(String id, String mimeType, int width, int height, float frameRate, + int audioChannels, int audioSamplingRate, int bitrate, String language, String codecs) { + this.id = Assertions.checkNotNull(id); + this.mimeType = mimeType; + this.width = width; + this.height = height; + this.frameRate = frameRate; + this.audioChannels = audioChannels; + this.audioSamplingRate = audioSamplingRate; + this.bitrate = bitrate; + this.language = language; + this.codecs = codecs; + } + + @Override + public int hashCode() { + return id.hashCode(); + } + + /** + * Implements equality based on {@link #id} only. + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + Format other = (Format) obj; + return other.id.equals(id); + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/FormatEvaluator.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/FormatEvaluator.java new file mode 100755 index 000000000..442e58bc0 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/FormatEvaluator.java @@ -0,0 +1,284 @@ +/* + * 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.messenger.exoplayer.chunk; + +import org.telegram.messenger.exoplayer.upstream.BandwidthMeter; + +import java.util.List; +import java.util.Random; + +/** + * Selects from a number of available formats during playback. + */ +public interface FormatEvaluator { + + /** + * Enables the evaluator. + */ + void enable(); + + /** + * Disables the evaluator. + */ + void disable(); + + /** + * Update the supplied evaluation. + *

+ * When the method is invoked, {@code evaluation} will contain the currently selected + * format (null for the first evaluation), the most recent trigger (TRIGGER_INITIAL for the + * first evaluation) and the current queue size. The implementation should update these + * fields as necessary. + *

+ * The trigger should be considered "sticky" for as long as a given representation is selected, + * and so should only be changed if the representation is also changed. + * + * @param queue A read only representation of the currently buffered {@link MediaChunk}s. + * @param playbackPositionUs The current playback position. + * @param formats The formats from which to select, ordered by decreasing bandwidth. + * @param evaluation The evaluation. + */ + // TODO: Pass more useful information into this method, and finalize the interface. + void evaluate(List queue, long playbackPositionUs, Format[] formats, + Evaluation evaluation); + + /** + * A format evaluation. + */ + public static final class Evaluation { + + /** + * The desired size of the queue. + */ + public int queueSize; + + /** + * The sticky reason for the format selection. + */ + public int trigger; + + /** + * The selected format. + */ + public Format format; + + public Evaluation() { + trigger = Chunk.TRIGGER_INITIAL; + } + + } + + /** + * Always selects the first format. + */ + public static final class FixedEvaluator implements FormatEvaluator { + + @Override + public void enable() { + // Do nothing. + } + + @Override + public void disable() { + // Do nothing. + } + + @Override + public void evaluate(List queue, long playbackPositionUs, + Format[] formats, Evaluation evaluation) { + evaluation.format = formats[0]; + } + + } + + /** + * Selects randomly between the available formats. + */ + public static final class RandomEvaluator implements FormatEvaluator { + + private final Random random; + + public RandomEvaluator() { + this.random = new Random(); + } + + /** + * @param seed A seed for the underlying random number generator. + */ + public RandomEvaluator(int seed) { + this.random = new Random(seed); + } + + @Override + public void enable() { + // Do nothing. + } + + @Override + public void disable() { + // Do nothing. + } + + @Override + public void evaluate(List queue, long playbackPositionUs, + Format[] formats, Evaluation evaluation) { + Format newFormat = formats[random.nextInt(formats.length)]; + if (evaluation.format != null && !evaluation.format.equals(newFormat)) { + evaluation.trigger = Chunk.TRIGGER_ADAPTIVE; + } + evaluation.format = newFormat; + } + + } + + /** + * An adaptive evaluator for video formats, which attempts to select the best quality possible + * given the current network conditions and state of the buffer. + *

+ * This implementation should be used for video only, and should not be used for audio. It is a + * reference implementation only. It is recommended that application developers implement their + * own adaptive evaluator to more precisely suit their use case. + */ + public static final class AdaptiveEvaluator implements FormatEvaluator { + + public static final int DEFAULT_MAX_INITIAL_BITRATE = 800000; + + public static final int DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS = 10000; + public static final int DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS = 25000; + public static final int DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS = 25000; + public static final float DEFAULT_BANDWIDTH_FRACTION = 0.75f; + + private final BandwidthMeter bandwidthMeter; + + private final int maxInitialBitrate; + private final long minDurationForQualityIncreaseUs; + private final long maxDurationForQualityDecreaseUs; + private final long minDurationToRetainAfterDiscardUs; + private final float bandwidthFraction; + + /** + * @param bandwidthMeter Provides an estimate of the currently available bandwidth. + */ + public AdaptiveEvaluator(BandwidthMeter bandwidthMeter) { + this (bandwidthMeter, DEFAULT_MAX_INITIAL_BITRATE, + DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS, + DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS, + DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, DEFAULT_BANDWIDTH_FRACTION); + } + + /** + * @param bandwidthMeter Provides an estimate of the currently available bandwidth. + * @param maxInitialBitrate The maximum bitrate in bits per second that should be assumed + * when bandwidthMeter cannot provide an estimate due to playback having only just started. + * @param minDurationForQualityIncreaseMs The minimum duration of buffered data required for + * the evaluator to consider switching to a higher quality format. + * @param maxDurationForQualityDecreaseMs The maximum duration of buffered data required for + * the evaluator to consider switching to a lower quality format. + * @param minDurationToRetainAfterDiscardMs When switching to a significantly higher quality + * format, the evaluator may discard some of the media that it has already buffered at the + * lower quality, so as to switch up to the higher quality faster. This is the minimum + * duration of media that must be retained at the lower quality. + * @param bandwidthFraction The fraction of the available bandwidth that the evaluator should + * consider available for use. Setting to a value less than 1 is recommended to account + * for inaccuracies in the bandwidth estimator. + */ + public AdaptiveEvaluator(BandwidthMeter bandwidthMeter, + int maxInitialBitrate, + int minDurationForQualityIncreaseMs, + int maxDurationForQualityDecreaseMs, + int minDurationToRetainAfterDiscardMs, + float bandwidthFraction) { + this.bandwidthMeter = bandwidthMeter; + this.maxInitialBitrate = maxInitialBitrate; + this.minDurationForQualityIncreaseUs = minDurationForQualityIncreaseMs * 1000L; + this.maxDurationForQualityDecreaseUs = maxDurationForQualityDecreaseMs * 1000L; + this.minDurationToRetainAfterDiscardUs = minDurationToRetainAfterDiscardMs * 1000L; + this.bandwidthFraction = bandwidthFraction; + } + + @Override + public void enable() { + // Do nothing. + } + + @Override + public void disable() { + // Do nothing. + } + + @Override + public void evaluate(List queue, long playbackPositionUs, + Format[] formats, Evaluation evaluation) { + long bufferedDurationUs = queue.isEmpty() ? 0 + : queue.get(queue.size() - 1).endTimeUs - playbackPositionUs; + Format current = evaluation.format; + Format ideal = determineIdealFormat(formats, bandwidthMeter.getBitrateEstimate()); + boolean isHigher = ideal != null && current != null && ideal.bitrate > current.bitrate; + boolean isLower = ideal != null && current != null && ideal.bitrate < current.bitrate; + if (isHigher) { + if (bufferedDurationUs < minDurationForQualityIncreaseUs) { + // The ideal format is a higher quality, but we have insufficient buffer to + // safely switch up. Defer switching up for now. + ideal = current; + } else if (bufferedDurationUs >= minDurationToRetainAfterDiscardUs) { + // We're switching from an SD stream to a stream of higher resolution. Consider + // discarding already buffered media chunks. Specifically, discard media chunks starting + // from the first one that is of lower bandwidth, lower resolution and that is not HD. + for (int i = 1; i < queue.size(); i++) { + MediaChunk thisChunk = queue.get(i); + long durationBeforeThisSegmentUs = thisChunk.startTimeUs - playbackPositionUs; + if (durationBeforeThisSegmentUs >= minDurationToRetainAfterDiscardUs + && thisChunk.format.bitrate < ideal.bitrate + && thisChunk.format.height < ideal.height + && thisChunk.format.height < 720 + && thisChunk.format.width < 1280) { + // Discard chunks from this one onwards. + evaluation.queueSize = i; + break; + } + } + } + } else if (isLower && current != null + && bufferedDurationUs >= maxDurationForQualityDecreaseUs) { + // The ideal format is a lower quality, but we have sufficient buffer to defer switching + // down for now. + ideal = current; + } + if (current != null && ideal != current) { + evaluation.trigger = Chunk.TRIGGER_ADAPTIVE; + } + evaluation.format = ideal; + } + + /** + * Compute the ideal format ignoring buffer health. + */ + private Format determineIdealFormat(Format[] formats, long bitrateEstimate) { + long effectiveBitrate = bitrateEstimate == BandwidthMeter.NO_ESTIMATE + ? maxInitialBitrate : (long) (bitrateEstimate * bandwidthFraction); + for (int i = 0; i < formats.length; i++) { + Format format = formats[i]; + if (format.bitrate <= effectiveBitrate) { + return format; + } + } + // We didn't manage to calculate a suitable format. Return the lowest quality format. + return formats[formats.length - 1]; + } + + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/FloatEvaluator.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/FormatWrapper.java old mode 100644 new mode 100755 similarity index 60% rename from TMessagesProj/src/main/java/org/telegram/messenger/Animation/FloatEvaluator.java rename to TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/FormatWrapper.java index 31d289390..4ee8e4723 --- a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/FloatEvaluator.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/FormatWrapper.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2010 The Android Open Source Project + * 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. @@ -13,12 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package org.telegram.messenger.exoplayer.chunk; -package org.telegram.messenger.Animation; +/** + * Represents an object that wraps a {@link Format}. + */ +public interface FormatWrapper { -public class FloatEvaluator implements TypeEvaluator { - public Float evaluate(float fraction, Number startValue, Number endValue) { - float startFloat = startValue.floatValue(); - return startFloat + fraction * (endValue.floatValue() - startFloat); - } -} \ No newline at end of file + /** + * Returns the wrapped format. + */ + Format getFormat(); + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/InitializationChunk.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/InitializationChunk.java new file mode 100755 index 000000000..814c5062a --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/InitializationChunk.java @@ -0,0 +1,200 @@ +/* + * 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.messenger.exoplayer.chunk; + +import org.telegram.messenger.exoplayer.MediaFormat; +import org.telegram.messenger.exoplayer.chunk.ChunkExtractorWrapper.SingleTrackOutput; +import org.telegram.messenger.exoplayer.drm.DrmInitData; +import org.telegram.messenger.exoplayer.extractor.DefaultExtractorInput; +import org.telegram.messenger.exoplayer.extractor.Extractor; +import org.telegram.messenger.exoplayer.extractor.ExtractorInput; +import org.telegram.messenger.exoplayer.extractor.SeekMap; +import org.telegram.messenger.exoplayer.upstream.DataSource; +import org.telegram.messenger.exoplayer.upstream.DataSpec; +import org.telegram.messenger.exoplayer.util.ParsableByteArray; +import org.telegram.messenger.exoplayer.util.Util; + +import java.io.IOException; + +/** + * A {@link Chunk} that uses an {@link Extractor} to parse initialization data for single track. + */ +public final class InitializationChunk extends Chunk implements SingleTrackOutput { + + private final ChunkExtractorWrapper extractorWrapper; + + // Initialization results. Set by the loader thread and read by any thread that knows loading + // has completed. These variables do not need to be volatile, since a memory barrier must occur + // for the reading thread to know that loading has completed. + private MediaFormat mediaFormat; + private DrmInitData drmInitData; + private SeekMap seekMap; + + private volatile int bytesLoaded; + private volatile boolean loadCanceled; + + public InitializationChunk(DataSource dataSource, DataSpec dataSpec, int trigger, Format format, + ChunkExtractorWrapper extractorWrapper) { + this(dataSource, dataSpec, trigger, format, extractorWrapper, Chunk.NO_PARENT_ID); + } + + /** + * Constructor for a chunk of media samples. + * + * @param dataSource A {@link DataSource} for loading the initialization data. + * @param dataSpec Defines the initialization data to be loaded. + * @param trigger The reason for this chunk being selected. + * @param format The format of the stream to which this chunk belongs. + * @param extractorWrapper A wrapped extractor to use for parsing the initialization data. + * @param parentId Identifier for a parent from which this chunk originates. + */ + public InitializationChunk(DataSource dataSource, DataSpec dataSpec, int trigger, Format format, + ChunkExtractorWrapper extractorWrapper, int parentId) { + super(dataSource, dataSpec, Chunk.TYPE_MEDIA_INITIALIZATION, trigger, format, parentId); + this.extractorWrapper = extractorWrapper; + } + + @Override + public long bytesLoaded() { + return bytesLoaded; + } + + /** + * True if a {@link MediaFormat} was parsed from the chunk. False otherwise. + *

+ * Should be called after loading has completed. + */ + public boolean hasFormat() { + return mediaFormat != null; + } + + /** + * Returns a {@link MediaFormat} parsed from the chunk, or null. + *

+ * Should be called after loading has completed. + */ + public MediaFormat getFormat() { + return mediaFormat; + } + + /** + * True if a {@link DrmInitData} was parsed from the chunk. False otherwise. + *

+ * Should be called after loading has completed. + */ + public boolean hasDrmInitData() { + return drmInitData != null; + } + + /** + * Returns a {@link DrmInitData} parsed from the chunk, or null. + *

+ * Should be called after loading has completed. + */ + public DrmInitData getDrmInitData() { + return drmInitData; + } + + /** + * True if a {@link SeekMap} was parsed from the chunk. False otherwise. + *

+ * Should be called after loading has completed. + */ + public boolean hasSeekMap() { + return seekMap != null; + } + + /** + * Returns a {@link SeekMap} parsed from the chunk, or null. + *

+ * Should be called after loading has completed. + */ + public SeekMap getSeekMap() { + return seekMap; + } + + // SingleTrackOutput implementation. + + @Override + public void seekMap(SeekMap seekMap) { + this.seekMap = seekMap; + } + + @Override + public void drmInitData(DrmInitData drmInitData) { + this.drmInitData = drmInitData; + } + + @Override + public void format(MediaFormat mediaFormat) { + this.mediaFormat = mediaFormat; + } + + @Override + public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput) + throws IOException, InterruptedException { + throw new IllegalStateException("Unexpected sample data in initialization chunk"); + } + + @Override + public void sampleData(ParsableByteArray data, int length) { + throw new IllegalStateException("Unexpected sample data in initialization chunk"); + } + + @Override + public void sampleMetadata(long timeUs, int flags, int size, int offset, byte[] encryptionKey) { + throw new IllegalStateException("Unexpected sample data in initialization chunk"); + } + + // Loadable implementation. + + @Override + public void cancelLoad() { + loadCanceled = true; + } + + @Override + public boolean isLoadCanceled() { + return loadCanceled; + } + + @SuppressWarnings("NonAtomicVolatileUpdate") + @Override + public void load() throws IOException, InterruptedException { + DataSpec loadDataSpec = Util.getRemainderDataSpec(dataSpec, bytesLoaded); + try { + // Create and open the input. + ExtractorInput input = new DefaultExtractorInput(dataSource, + loadDataSpec.absoluteStreamPosition, dataSource.open(loadDataSpec)); + if (bytesLoaded == 0) { + // Set the target to ourselves. + extractorWrapper.init(this); + } + // Load and parse the initialization data. + try { + int result = Extractor.RESULT_CONTINUE; + while (result == Extractor.RESULT_CONTINUE && !loadCanceled) { + result = extractorWrapper.read(input); + } + } finally { + bytesLoaded = (int) (input.getPosition() - dataSpec.absoluteStreamPosition); + } + } finally { + dataSource.close(); + } + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/MediaChunk.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/MediaChunk.java new file mode 100755 index 000000000..4473cc285 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/MediaChunk.java @@ -0,0 +1,69 @@ +/* + * 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.messenger.exoplayer.chunk; + +import org.telegram.messenger.exoplayer.upstream.DataSource; +import org.telegram.messenger.exoplayer.upstream.DataSpec; +import org.telegram.messenger.exoplayer.util.Assertions; + +/** + * An abstract base class for {@link Chunk}s that contain media samples. + */ +public abstract class MediaChunk extends Chunk { + + /** + * The start time of the media contained by the chunk. + */ + public final long startTimeUs; + /** + * The end time of the media contained by the chunk. + */ + public final long endTimeUs; + /** + * The chunk index. + */ + public final int chunkIndex; + + public MediaChunk(DataSource dataSource, DataSpec dataSpec, int trigger, Format format, + long startTimeUs, long endTimeUs, int chunkIndex) { + this(dataSource, dataSpec, trigger, format, startTimeUs, endTimeUs, chunkIndex, + Chunk.NO_PARENT_ID); + } + + public int getNextChunkIndex() { + return chunkIndex + 1; + } + + /** + * @param dataSource A {@link DataSource} for loading the data. + * @param dataSpec Defines the data to be loaded. + * @param trigger The reason for this chunk being selected. + * @param format The format of the stream to which this chunk belongs. + * @param startTimeUs The start time of the media contained by the chunk, in microseconds. + * @param endTimeUs The end time of the media contained by the chunk, in microseconds. + * @param chunkIndex The index of the chunk. + * @param parentId Identifier for a parent from which this chunk originates. + */ + public MediaChunk(DataSource dataSource, DataSpec dataSpec, int trigger, Format format, + long startTimeUs, long endTimeUs, int chunkIndex, int parentId) { + super(dataSource, dataSpec, Chunk.TYPE_MEDIA, trigger, format, parentId); + Assertions.checkNotNull(format); + this.startTimeUs = startTimeUs; + this.endTimeUs = endTimeUs; + this.chunkIndex = chunkIndex; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/SingleSampleMediaChunk.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/SingleSampleMediaChunk.java new file mode 100755 index 000000000..5061b9032 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/SingleSampleMediaChunk.java @@ -0,0 +1,107 @@ +/* + * 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.messenger.exoplayer.chunk; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.MediaFormat; +import org.telegram.messenger.exoplayer.drm.DrmInitData; +import org.telegram.messenger.exoplayer.upstream.DataSource; +import org.telegram.messenger.exoplayer.upstream.DataSpec; +import org.telegram.messenger.exoplayer.util.Util; + +import java.io.IOException; + +/** + * A {@link BaseMediaChunk} for chunks consisting of a single raw sample. + */ +public final class SingleSampleMediaChunk extends BaseMediaChunk { + + private final MediaFormat sampleFormat; + private final DrmInitData sampleDrmInitData; + + private volatile int bytesLoaded; + private volatile boolean loadCanceled; + + /** + * @param dataSource A {@link DataSource} for loading the data. + * @param dataSpec Defines the data to be loaded. + * @param trigger The reason for this chunk being selected. + * @param format The format of the stream to which this chunk belongs. + * @param startTimeUs The start time of the media contained by the chunk, in microseconds. + * @param endTimeUs The end time of the media contained by the chunk, in microseconds. + * @param chunkIndex The index of the chunk. + * @param sampleFormat The format of the sample. + * @param sampleDrmInitData The {@link DrmInitData} for the sample. Null if the sample is not drm + * protected. + * @param parentId Identifier for a parent from which this chunk originates. + */ + public SingleSampleMediaChunk(DataSource dataSource, DataSpec dataSpec, int trigger, + Format format, long startTimeUs, long endTimeUs, int chunkIndex, MediaFormat sampleFormat, + DrmInitData sampleDrmInitData, int parentId) { + super(dataSource, dataSpec, trigger, format, startTimeUs, endTimeUs, chunkIndex, true, + parentId); + this.sampleFormat = sampleFormat; + this.sampleDrmInitData = sampleDrmInitData; + } + + @Override + public long bytesLoaded() { + return bytesLoaded; + } + + @Override + public MediaFormat getMediaFormat() { + return sampleFormat; + } + + @Override + public DrmInitData getDrmInitData() { + return sampleDrmInitData; + } + + // Loadable implementation. + + @Override + public void cancelLoad() { + loadCanceled = true; + } + + @Override + public boolean isLoadCanceled() { + return loadCanceled; + } + + @SuppressWarnings("NonAtomicVolatileUpdate") + @Override + public void load() throws IOException, InterruptedException { + DataSpec loadDataSpec = Util.getRemainderDataSpec(dataSpec, bytesLoaded); + try { + // Create and open the input. + dataSource.open(loadDataSpec); + // Load the sample data. + int result = 0; + while (result != C.RESULT_END_OF_INPUT) { + bytesLoaded += result; + result = getOutput().sampleData(dataSource, Integer.MAX_VALUE, true); + } + int sampleSize = bytesLoaded; + getOutput().sampleMetadata(startTimeUs, C.SAMPLE_FLAG_SYNC, sampleSize, 0, null); + } finally { + dataSource.close(); + } + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/VideoFormatSelectorUtil.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/VideoFormatSelectorUtil.java new file mode 100755 index 000000000..9f7983e0b --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/chunk/VideoFormatSelectorUtil.java @@ -0,0 +1,250 @@ +/* + * 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.messenger.exoplayer.chunk; + +import org.telegram.messenger.exoplayer.MediaCodecUtil; +import org.telegram.messenger.exoplayer.MediaCodecUtil.DecoderQueryException; +import org.telegram.messenger.exoplayer.util.MimeTypes; +import org.telegram.messenger.exoplayer.util.Util; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Point; +import android.view.Display; +import android.view.WindowManager; + +import java.util.ArrayList; +import java.util.List; + +/** + * Selects from possible video formats. + */ +public final class VideoFormatSelectorUtil { + + /** + * If a dimension (i.e. width or height) of a video is greater or equal to this fraction of the + * corresponding viewport dimension, then the video is considered as filling the viewport (in that + * dimension). + */ + private static final float FRACTION_TO_CONSIDER_FULLSCREEN = 0.98f; + + /** + * Chooses a suitable subset from a number of video formats, to be rendered on the device's + * default display. + * + * @param context A context. + * @param formatWrappers Wrapped formats from which to select. + * @param allowedContainerMimeTypes An array of allowed container mime types. Null allows all + * mime types. + * @param filterHdFormats True to filter HD formats. False otherwise. + * @return An array holding the indices of the selected formats. + * @throws DecoderQueryException Thrown if there was an error querying decoders. + */ + public static int[] selectVideoFormatsForDefaultDisplay(Context context, + List formatWrappers, String[] allowedContainerMimeTypes, + boolean filterHdFormats) throws DecoderQueryException { + Point viewportSize = getViewportSize(context); + return selectVideoFormats(formatWrappers, allowedContainerMimeTypes, filterHdFormats, true, + viewportSize.x, viewportSize.y); + } + + /** + * Chooses a suitable subset from a number of video formats. + *

+ * A format is filtered (i.e. not selected) if: + *

    + *
  • {@code allowedContainerMimeTypes} is non-null and the format does not have one of the + * permitted mime types. + *
  • {@code filterHdFormats} is true and the format is HD. + *
  • It's determined that the video decoder isn't powerful enough to decode the format. + *
  • There exists another format of lower resolution whose resolution exceeds the maximum size + * in pixels that the video can be rendered within the viewport. + *
+ * + * @param formatWrappers Wrapped formats from which to select. + * @param allowedContainerMimeTypes An array of allowed container mime types. Null allows all + * mime types. + * @param filterHdFormats True to filter HD formats. False otherwise. + * @param orientationMayChange True if the video's orientation may change with respect to the + * viewport during playback. + * @param viewportWidth The width in pixels of the viewport within which the video will be + * displayed. If the viewport size may change, this should be set to the maximum possible + * width. -1 if selection should not be constrained by a viewport. + * @param viewportHeight The height in pixels of the viewport within which the video will be + * displayed. If the viewport size may change, this should be set to the maximum possible + * height. -1 if selection should not be constrained by a viewport. + * @return An array holding the indices of the selected formats. + * @throws DecoderQueryException + */ + public static int[] selectVideoFormats(List formatWrappers, + String[] allowedContainerMimeTypes, boolean filterHdFormats, boolean orientationMayChange, + int viewportWidth, int viewportHeight) throws DecoderQueryException { + int maxVideoPixelsToRetain = Integer.MAX_VALUE; + ArrayList selectedIndexList = new ArrayList<>(); + + // First pass to filter out formats that individually fail to meet the selection criteria. + int formatWrapperCount = formatWrappers.size(); + for (int i = 0; i < formatWrapperCount; i++) { + Format format = formatWrappers.get(i).getFormat(); + if (isFormatPlayable(format, allowedContainerMimeTypes, filterHdFormats)) { + // Select the format for now. It may still be filtered in the second pass below. + selectedIndexList.add(i); + // Keep track of the number of pixels of the selected format whose resolution is the + // smallest to exceed the maximum size at which it can be displayed within the viewport. + // We'll discard formats of higher resolution in a second pass. + if (format.width > 0 && format.height > 0 && viewportWidth > 0 && viewportHeight > 0) { + Point maxVideoSizeInViewport = getMaxVideoSizeInViewport(orientationMayChange, + viewportWidth, viewportHeight, format.width, format.height); + int videoPixels = format.width * format.height; + if (format.width >= (int) (maxVideoSizeInViewport.x * FRACTION_TO_CONSIDER_FULLSCREEN) + && format.height >= (int) (maxVideoSizeInViewport.y * FRACTION_TO_CONSIDER_FULLSCREEN) + && videoPixels < maxVideoPixelsToRetain) { + maxVideoPixelsToRetain = videoPixels; + } + } + } + } + + // Second pass to filter out formats that exceed maxVideoPixelsToRetain. These formats are have + // unnecessarily high resolution given the size at which the video will be displayed within the + // viewport. + if (maxVideoPixelsToRetain != Integer.MAX_VALUE) { + for (int i = selectedIndexList.size() - 1; i >= 0; i--) { + Format format = formatWrappers.get(selectedIndexList.get(i)).getFormat(); + if (format.width > 0 && format.height > 0 + && format.width * format.height > maxVideoPixelsToRetain) { + selectedIndexList.remove(i); + } + } + } + + return Util.toArray(selectedIndexList); + } + + /** + * Determines whether an individual format is playable, given an array of allowed container types, + * whether HD formats should be filtered and a maximum decodable frame size in pixels. + */ + private static boolean isFormatPlayable(Format format, String[] allowedContainerMimeTypes, + boolean filterHdFormats) throws DecoderQueryException { + if (allowedContainerMimeTypes != null + && !Util.contains(allowedContainerMimeTypes, format.mimeType)) { + // Filtering format based on its container mime type. + return false; + } + if (filterHdFormats && (format.width >= 1280 || format.height >= 720)) { + // Filtering format because it's HD. + return false; + } + if (format.width > 0 && format.height > 0) { + if (Util.SDK_INT >= 21) { + String videoMediaMimeType = MimeTypes.getVideoMediaMimeType(format.codecs); + if (MimeTypes.VIDEO_UNKNOWN.equals(videoMediaMimeType)) { + // Assume the video is H.264. + videoMediaMimeType = MimeTypes.VIDEO_H264; + } + if (format.frameRate > 0) { + return MediaCodecUtil.isSizeAndRateSupportedV21(videoMediaMimeType, false, format.width, + format.height, format.frameRate); + } else { + return MediaCodecUtil.isSizeSupportedV21(videoMediaMimeType, false, format.width, + format.height); + } + } + // Assume the video is H.264. + if (format.width * format.height > MediaCodecUtil.maxH264DecodableFrameSize()) { + // Filtering format because it exceeds the maximum decodable frame size. + return false; + } + } + return true; + } + + /** + * Given viewport dimensions and video dimensions, computes the maximum size of the video as it + * will be rendered to fit inside of the viewport. + */ + private static Point getMaxVideoSizeInViewport(boolean orientationMayChange, int viewportWidth, + int viewportHeight, int videoWidth, int videoHeight) { + if (orientationMayChange && (videoWidth > videoHeight) != (viewportWidth > viewportHeight)) { + // Rotation is allowed, and the video will be larger in the rotated viewport. + int tempViewportWidth = viewportWidth; + viewportWidth = viewportHeight; + viewportHeight = tempViewportWidth; + } + + if (videoWidth * viewportHeight >= videoHeight * viewportWidth) { + // Horizontal letter-boxing along top and bottom. + return new Point(viewportWidth, Util.ceilDivide(viewportWidth * videoHeight, videoWidth)); + } else { + // Vertical letter-boxing along edges. + return new Point(Util.ceilDivide(viewportHeight * videoWidth, videoHeight), viewportHeight); + } + } + + private static Point getViewportSize(Context context) { + // Before API 23 the platform Display object does not provide a way to identify Android TVs that + // can show 4k resolution in a SurfaceView, so check for supported devices here. + // See also https://developer.sony.com/develop/tvs/android-tv/design-guide/. + if (Util.SDK_INT < 23 && Util.MODEL != null && Util.MODEL.startsWith("BRAVIA") + && context.getPackageManager().hasSystemFeature("com.sony.dtv.hardware.panel.qfhd")) { + return new Point(3840, 2160); + } + + WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + return getDisplaySize(windowManager.getDefaultDisplay()); + } + + private static Point getDisplaySize(Display display) { + Point displaySize = new Point(); + if (Util.SDK_INT >= 23) { + getDisplaySizeV23(display, displaySize); + } else if (Util.SDK_INT >= 17) { + getDisplaySizeV17(display, displaySize); + } else if (Util.SDK_INT >= 16) { + getDisplaySizeV16(display, displaySize); + } else { + getDisplaySizeV9(display, displaySize); + } + return displaySize; + } + + @TargetApi(23) + private static void getDisplaySizeV23(Display display, Point outSize) { + Display.Mode mode = display.getMode(); + outSize.x = mode.getPhysicalWidth(); + outSize.y = mode.getPhysicalHeight(); + } + + @TargetApi(17) + private static void getDisplaySizeV17(Display display, Point outSize) { + display.getRealSize(outSize); + } + + @TargetApi(16) + private static void getDisplaySizeV16(Display display, Point outSize) { + display.getSize(outSize); + } + + @SuppressWarnings("deprecation") + private static void getDisplaySizeV9(Display display, Point outSize) { + outSize.x = display.getWidth(); + outSize.y = display.getHeight(); + } + + private VideoFormatSelectorUtil() {} + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/drm/DrmInitData.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/drm/DrmInitData.java new file mode 100755 index 000000000..e7f756cd7 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/drm/DrmInitData.java @@ -0,0 +1,129 @@ +/* + * 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.messenger.exoplayer.drm; + +import org.telegram.messenger.exoplayer.util.Assertions; + +import android.media.MediaDrm; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * Encapsulates initialization data required by a {@link MediaDrm} instances. + */ +public interface DrmInitData { + + /** + * Retrieves initialization data for a given DRM scheme, specified by its UUID. + * + * @param schemeUuid The DRM scheme's UUID. + * @return The initialization data for the scheme, or null if the scheme is not supported. + */ + public abstract SchemeInitData get(UUID schemeUuid); + + /** + * A {@link DrmInitData} implementation that maps UUID onto scheme specific data. + */ + public static final class Mapped implements DrmInitData { + + private final Map schemeData; + + public Mapped() { + schemeData = new HashMap<>(); + } + + @Override + public SchemeInitData get(UUID schemeUuid) { + return schemeData.get(schemeUuid); + } + + /** + * Inserts scheme specific initialization data. + * + * @param schemeUuid The scheme UUID. + * @param schemeInitData The corresponding initialization data. + */ + public void put(UUID schemeUuid, SchemeInitData schemeInitData) { + schemeData.put(schemeUuid, schemeInitData); + } + + } + + /** + * A {@link DrmInitData} implementation that returns the same initialization data for all schemes. + */ + public static final class Universal implements DrmInitData { + + private SchemeInitData data; + + public Universal(SchemeInitData data) { + this.data = data; + } + + @Override + public SchemeInitData get(UUID schemeUuid) { + return data; + } + + } + + /** + * Scheme initialization data. + */ + public static final class SchemeInitData { + + /** + * The mimeType of {@link #data}. + */ + public final String mimeType; + /** + * The initialization data. + */ + public final byte[] data; + + /** + * @param mimeType The mimeType of the initialization data. + * @param data The initialization data. + */ + public SchemeInitData(String mimeType, byte[] data) { + this.mimeType = Assertions.checkNotNull(mimeType); + this.data = Assertions.checkNotNull(data); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof SchemeInitData)) { + return false; + } + if (obj == this) { + return true; + } + + SchemeInitData other = (SchemeInitData) obj; + return mimeType.equals(other.mimeType) && Arrays.equals(data, other.data); + } + + @Override + public int hashCode() { + return mimeType.hashCode() + 31 * Arrays.hashCode(data); + } + + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/drm/DrmSessionManager.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/drm/DrmSessionManager.java new file mode 100755 index 000000000..67850dff4 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/drm/DrmSessionManager.java @@ -0,0 +1,104 @@ +/* + * 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.messenger.exoplayer.drm; + +import android.annotation.TargetApi; +import android.media.MediaCrypto; + +/** + * Manages a DRM session. + */ +@TargetApi(16) +public interface DrmSessionManager { + + /** + * The error state. {@link #getError()} can be used to retrieve the cause. + */ + public static final int STATE_ERROR = 0; + /** + * The session is closed. + */ + public static final int STATE_CLOSED = 1; + /** + * The session is being opened (i.e. {@link #open(DrmInitData)} has been called, but the session + * is not yet open). + */ + public static final int STATE_OPENING = 2; + /** + * The session is open, but does not yet have the keys required for decryption. + */ + public static final int STATE_OPENED = 3; + /** + * The session is open and has the keys required for decryption. + */ + public static final int STATE_OPENED_WITH_KEYS = 4; + + /** + * Opens the session, possibly asynchronously. + * + * @param drmInitData DRM initialization data. + */ + void open(DrmInitData drmInitData); + + /** + * Closes the session. + */ + void close(); + + /** + * Gets the current state of the session. + * + * @return One of {@link #STATE_ERROR}, {@link #STATE_CLOSED}, {@link #STATE_OPENING}, + * {@link #STATE_OPENED} and {@link #STATE_OPENED_WITH_KEYS}. + */ + int getState(); + + /** + * Gets a {@link MediaCrypto} for the open session. + *

+ * This method may be called when the manager is in the following states: + * {@link #STATE_OPENED}, {@link #STATE_OPENED_WITH_KEYS} + * + * @return A {@link MediaCrypto} for the open session. + * @throws IllegalStateException If called when a session isn't opened. + */ + MediaCrypto getMediaCrypto(); + + /** + * Whether the session requires a secure decoder for the specified mime type. + *

+ * Normally this method should return {@link MediaCrypto#requiresSecureDecoderComponent(String)}, + * however in some cases implementations may wish to modify the return value (i.e. to force a + * secure decoder even when one is not required). + *

+ * This method may be called when the manager is in the following states: + * {@link #STATE_OPENED}, {@link #STATE_OPENED_WITH_KEYS} + * + * @return Whether the open session requires a secure decoder for the specified mime type. + * @throws IllegalStateException If called when a session isn't opened. + */ + boolean requiresSecureDecoderComponent(String mimeType); + + /** + * Gets the cause of the error state. + *

+ * This method may be called when the manager is in any state. + * + * @return An exception if the state is {@link #STATE_ERROR}. Null otherwise. + */ + Exception getError(); + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/TypeEvaluator.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/drm/KeysExpiredException.java old mode 100644 new mode 100755 similarity index 71% rename from TMessagesProj/src/main/java/org/telegram/messenger/Animation/TypeEvaluator.java rename to TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/drm/KeysExpiredException.java index 305f8ad8d..1b83c7eed --- a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/TypeEvaluator.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/drm/KeysExpiredException.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2010 The Android Open Source Project + * 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. @@ -13,9 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package org.telegram.messenger.exoplayer.drm; -package org.telegram.messenger.Animation; - -public interface TypeEvaluator { - T evaluate(float fraction, T startValue, T endValue); +/** + * Thrown when the drm keys loaded into an open session expire. + */ +public final class KeysExpiredException extends Exception { } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/drm/MediaDrmCallback.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/drm/MediaDrmCallback.java new file mode 100755 index 000000000..2fd695dc8 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/drm/MediaDrmCallback.java @@ -0,0 +1,49 @@ +/* + * 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.messenger.exoplayer.drm; + +import android.annotation.TargetApi; +import android.media.MediaDrm; + +import java.util.UUID; + +/** + * Performs {@link MediaDrm} key and provisioning requests. + */ +@TargetApi(18) +public interface MediaDrmCallback { + + /** + * Executes a provisioning request. + * + * @param uuid The UUID of the content protection scheme. + * @param request The request. + * @return The response data. + * @throws Exception If an error occurred executing the request. + */ + byte[] executeProvisionRequest(UUID uuid, MediaDrm.ProvisionRequest request) throws Exception; + + /** + * Executes a key request. + * + * @param uuid The UUID of the content protection scheme. + * @param request The request. + * @return The response data. + * @throws Exception If an error occurred executing the request. + */ + byte[] executeKeyRequest(UUID uuid, MediaDrm.KeyRequest request) throws Exception; + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/drm/StreamingDrmSessionManager.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/drm/StreamingDrmSessionManager.java new file mode 100755 index 000000000..bc8be137a --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/drm/StreamingDrmSessionManager.java @@ -0,0 +1,518 @@ +/* + * 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.messenger.exoplayer.drm; + +import org.telegram.messenger.exoplayer.drm.DrmInitData.SchemeInitData; +import org.telegram.messenger.exoplayer.extractor.mp4.PsshAtomUtil; +import org.telegram.messenger.exoplayer.util.Util; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.media.DeniedByServerException; +import android.media.MediaCrypto; +import android.media.MediaDrm; +import android.media.MediaDrm.KeyRequest; +import android.media.MediaDrm.OnEventListener; +import android.media.MediaDrm.ProvisionRequest; +import android.media.NotProvisionedException; +import android.media.UnsupportedSchemeException; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; +import android.text.TextUtils; + +import java.util.HashMap; +import java.util.UUID; + +/** + * A base class for {@link DrmSessionManager} implementations that support streaming playbacks + * using {@link MediaDrm}. + */ +@TargetApi(18) +public class StreamingDrmSessionManager implements DrmSessionManager { + + /** + * Interface definition for a callback to be notified of {@link StreamingDrmSessionManager} + * events. + */ + public interface EventListener { + + /** + * Invoked each time keys are loaded. + */ + void onDrmKeysLoaded(); + + /** + * Invoked when a drm error occurs. + * + * @param e The corresponding exception. + */ + void onDrmSessionManagerError(Exception e); + + } + + /** + * UUID for the Widevine DRM scheme. + */ + public static final UUID WIDEVINE_UUID = new UUID(0xEDEF8BA979D64ACEL, 0xA3C827DCD51D21EDL); + + /** + * UUID for the PlayReady DRM scheme. + *

+ * Note that PlayReady is unsupported by most Android devices, with the exception of Android TV + * devices, which do provide support. + */ + public static final UUID PLAYREADY_UUID = new UUID(0x9A04F07998404286L, 0xAB92E65BE0885F95L); + + /** + * The key to use when passing CustomData to a PlayReady instance in an optional parameter map. + */ + public static final String PLAYREADY_CUSTOM_DATA_KEY = "PRCustomData"; + + private static final int MSG_PROVISION = 0; + private static final int MSG_KEYS = 1; + + private final Handler eventHandler; + private final EventListener eventListener; + private final MediaDrm mediaDrm; + private final HashMap optionalKeyRequestParameters; + + /* package */ final MediaDrmHandler mediaDrmHandler; + /* package */ final MediaDrmCallback callback; + /* package */ final PostResponseHandler postResponseHandler; + /* package */ final UUID uuid; + + private HandlerThread requestHandlerThread; + private Handler postRequestHandler; + + private int openCount; + private boolean provisioningInProgress; + private int state; + private MediaCrypto mediaCrypto; + private Exception lastException; + private SchemeInitData schemeInitData; + private byte[] sessionId; + + /** + * Instantiates a new instance using the Widevine scheme. + * + * @param playbackLooper The looper associated with the media playback thread. Should usually be + * obtained using {@link org.telegram.messenger.exoplayer.ExoPlayer#getPlaybackLooper()}. + * @param callback Performs key and provisioning requests. + * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument + * to {@link MediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null. + * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be + * null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + * @throws UnsupportedDrmException If the specified DRM scheme is not supported. + */ + public static StreamingDrmSessionManager newWidevineInstance(Looper playbackLooper, + MediaDrmCallback callback, HashMap optionalKeyRequestParameters, + Handler eventHandler, EventListener eventListener) throws UnsupportedDrmException { + return new StreamingDrmSessionManager(WIDEVINE_UUID, playbackLooper, callback, + optionalKeyRequestParameters, eventHandler, eventListener); + } + + /** + * Instantiates a new instance using the PlayReady scheme. + *

+ * Note that PlayReady is unsupported by most Android devices, with the exception of Android TV + * devices, which do provide support. + * + * @param playbackLooper The looper associated with the media playback thread. Should usually be + * obtained using {@link org.telegram.messenger.exoplayer.ExoPlayer#getPlaybackLooper()}. + * @param callback Performs key and provisioning requests. + * @param customData Optional custom data to include in requests generated by the instance. + * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be + * null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + * @throws UnsupportedDrmException If the specified DRM scheme is not supported. + */ + public static StreamingDrmSessionManager newPlayReadyInstance(Looper playbackLooper, + MediaDrmCallback callback, String customData, Handler eventHandler, + EventListener eventListener) throws UnsupportedDrmException { + HashMap optionalKeyRequestParameters; + if (!TextUtils.isEmpty(customData)) { + optionalKeyRequestParameters = new HashMap<>(); + optionalKeyRequestParameters.put(PLAYREADY_CUSTOM_DATA_KEY, customData); + } else { + optionalKeyRequestParameters = null; + } + return new StreamingDrmSessionManager(PLAYREADY_UUID, playbackLooper, callback, + optionalKeyRequestParameters, eventHandler, eventListener); + } + + /** + * @param uuid The UUID of the drm scheme. + * @param playbackLooper The looper associated with the media playback thread. Should usually be + * obtained using {@link org.telegram.messenger.exoplayer.ExoPlayer#getPlaybackLooper()}. + * @param callback Performs key and provisioning requests. + * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument + * to {@link MediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null. + * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be + * null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + * @throws UnsupportedDrmException If the specified DRM scheme is not supported. + */ + public StreamingDrmSessionManager(UUID uuid, Looper playbackLooper, MediaDrmCallback callback, + HashMap optionalKeyRequestParameters, Handler eventHandler, + EventListener eventListener) throws UnsupportedDrmException { + this.uuid = uuid; + this.callback = callback; + this.optionalKeyRequestParameters = optionalKeyRequestParameters; + this.eventHandler = eventHandler; + this.eventListener = eventListener; + try { + mediaDrm = new MediaDrm(uuid); + } catch (UnsupportedSchemeException e) { + throw new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME, e); + } catch (Exception e) { + throw new UnsupportedDrmException(UnsupportedDrmException.REASON_INSTANTIATION_ERROR, e); + } + mediaDrm.setOnEventListener(new MediaDrmEventListener()); + mediaDrmHandler = new MediaDrmHandler(playbackLooper); + postResponseHandler = new PostResponseHandler(playbackLooper); + state = STATE_CLOSED; + } + + @Override + public final int getState() { + return state; + } + + @Override + public final MediaCrypto getMediaCrypto() { + if (state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS) { + throw new IllegalStateException(); + } + return mediaCrypto; + } + + @Override + public boolean requiresSecureDecoderComponent(String mimeType) { + if (state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS) { + throw new IllegalStateException(); + } + return mediaCrypto.requiresSecureDecoderComponent(mimeType); + } + + @Override + public final Exception getError() { + return state == STATE_ERROR ? lastException : null; + } + + /** + * Provides access to {@link MediaDrm#getPropertyString(String)}. + *

+ * This method may be called when the manager is in any state. + * + * @param key The key to request. + * @return The retrieved property. + */ + public final String getPropertyString(String key) { + return mediaDrm.getPropertyString(key); + } + + /** + * Provides access to {@link MediaDrm#setPropertyString(String, String)}. + *

+ * This method may be called when the manager is in any state. + * + * @param key The property to write. + * @param value The value to write. + */ + public final void setPropertyString(String key, String value) { + mediaDrm.setPropertyString(key, value); + } + + /** + * Provides access to {@link MediaDrm#getPropertyByteArray(String)}. + *

+ * This method may be called when the manager is in any state. + * + * @param key The key to request. + * @return The retrieved property. + */ + public final byte[] getPropertyByteArray(String key) { + return mediaDrm.getPropertyByteArray(key); + } + + /** + * Provides access to {@link MediaDrm#setPropertyByteArray(String, byte[])}. + *

+ * This method may be called when the manager is in any state. + * + * @param key The property to write. + * @param value The value to write. + */ + public final void setPropertyByteArray(String key, byte[] value) { + mediaDrm.setPropertyByteArray(key, value); + } + + @Override + public void open(DrmInitData drmInitData) { + if (++openCount != 1) { + return; + } + if (postRequestHandler == null) { + requestHandlerThread = new HandlerThread("DrmRequestHandler"); + requestHandlerThread.start(); + postRequestHandler = new PostRequestHandler(requestHandlerThread.getLooper()); + } + if (schemeInitData == null) { + schemeInitData = drmInitData.get(uuid); + if (schemeInitData == null) { + onError(new IllegalStateException("Media does not support uuid: " + uuid)); + return; + } + if (Util.SDK_INT < 21) { + // Prior to L the Widevine CDM required data to be extracted from the PSSH atom. + byte[] psshData = PsshAtomUtil.parseSchemeSpecificData(schemeInitData.data, WIDEVINE_UUID); + if (psshData == null) { + // Extraction failed. schemeData isn't a Widevine PSSH atom, so leave it unchanged. + } else { + schemeInitData = new SchemeInitData(schemeInitData.mimeType, psshData); + } + } + } + state = STATE_OPENING; + openInternal(true); + } + + @Override + public void close() { + if (--openCount != 0) { + return; + } + state = STATE_CLOSED; + provisioningInProgress = false; + mediaDrmHandler.removeCallbacksAndMessages(null); + postResponseHandler.removeCallbacksAndMessages(null); + postRequestHandler.removeCallbacksAndMessages(null); + postRequestHandler = null; + requestHandlerThread.quit(); + requestHandlerThread = null; + schemeInitData = null; + mediaCrypto = null; + lastException = null; + if (sessionId != null) { + mediaDrm.closeSession(sessionId); + sessionId = null; + } + } + + private void openInternal(boolean allowProvisioning) { + try { + sessionId = mediaDrm.openSession(); + mediaCrypto = new MediaCrypto(uuid, sessionId); + state = STATE_OPENED; + postKeyRequest(); + } catch (NotProvisionedException e) { + if (allowProvisioning) { + postProvisionRequest(); + } else { + onError(e); + } + } catch (Exception e) { + onError(e); + } + } + + private void postProvisionRequest() { + if (provisioningInProgress) { + return; + } + provisioningInProgress = true; + ProvisionRequest request = mediaDrm.getProvisionRequest(); + postRequestHandler.obtainMessage(MSG_PROVISION, request).sendToTarget(); + } + + private void onProvisionResponse(Object response) { + provisioningInProgress = false; + if (state != STATE_OPENING && state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS) { + // This event is stale. + return; + } + + if (response instanceof Exception) { + onError((Exception) response); + return; + } + + try { + mediaDrm.provideProvisionResponse((byte[]) response); + if (state == STATE_OPENING) { + openInternal(false); + } else { + postKeyRequest(); + } + } catch (DeniedByServerException e) { + onError(e); + } + } + + private void postKeyRequest() { + KeyRequest keyRequest; + try { + keyRequest = mediaDrm.getKeyRequest(sessionId, schemeInitData.data, schemeInitData.mimeType, + MediaDrm.KEY_TYPE_STREAMING, optionalKeyRequestParameters); + postRequestHandler.obtainMessage(MSG_KEYS, keyRequest).sendToTarget(); + } catch (NotProvisionedException e) { + onKeysError(e); + } + } + + private void onKeyResponse(Object response) { + if (state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS) { + // This event is stale. + return; + } + + if (response instanceof Exception) { + onKeysError((Exception) response); + return; + } + + try { + mediaDrm.provideKeyResponse(sessionId, (byte[]) response); + state = STATE_OPENED_WITH_KEYS; + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onDrmKeysLoaded(); + } + }); + } + } catch (Exception e) { + onKeysError(e); + } + } + + private void onKeysError(Exception e) { + if (e instanceof NotProvisionedException) { + postProvisionRequest(); + } else { + onError(e); + } + } + + private void onError(final Exception e) { + lastException = e; + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onDrmSessionManagerError(e); + } + }); + } + if (state != STATE_OPENED_WITH_KEYS) { + state = STATE_ERROR; + } + } + + @SuppressLint("HandlerLeak") + private class MediaDrmHandler extends Handler { + + public MediaDrmHandler(Looper looper) { + super(looper); + } + + @SuppressWarnings("deprecation") + @Override + public void handleMessage(Message msg) { + if (openCount == 0 || (state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS)) { + return; + } + switch (msg.what) { + case MediaDrm.EVENT_KEY_REQUIRED: + postKeyRequest(); + return; + case MediaDrm.EVENT_KEY_EXPIRED: + state = STATE_OPENED; + onError(new KeysExpiredException()); + return; + case MediaDrm.EVENT_PROVISION_REQUIRED: + state = STATE_OPENED; + postProvisionRequest(); + return; + } + } + + } + + private class MediaDrmEventListener implements OnEventListener { + + @Override + public void onEvent(MediaDrm md, byte[] sessionId, int event, int extra, byte[] data) { + mediaDrmHandler.sendEmptyMessage(event); + } + + } + + @SuppressLint("HandlerLeak") + private class PostResponseHandler extends Handler { + + public PostResponseHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_PROVISION: + onProvisionResponse(msg.obj); + return; + case MSG_KEYS: + onKeyResponse(msg.obj); + return; + } + } + + } + + @SuppressLint("HandlerLeak") + private class PostRequestHandler extends Handler { + + public PostRequestHandler(Looper backgroundLooper) { + super(backgroundLooper); + } + + @Override + public void handleMessage(Message msg) { + Object response; + try { + switch (msg.what) { + case MSG_PROVISION: + response = callback.executeProvisionRequest(uuid, (ProvisionRequest) msg.obj); + break; + case MSG_KEYS: + response = callback.executeKeyRequest(uuid, (KeyRequest) msg.obj); + break; + default: + throw new RuntimeException(); + } + } catch (Exception e) { + response = e; + } + postResponseHandler.obtainMessage(msg.what, response).sendToTarget(); + } + + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/drm/UnsupportedDrmException.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/drm/UnsupportedDrmException.java new file mode 100755 index 000000000..1f7e533a4 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/drm/UnsupportedDrmException.java @@ -0,0 +1,44 @@ +/* + * 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.messenger.exoplayer.drm; + +/** + * Thrown when the requested DRM scheme is not supported. + */ +public final class UnsupportedDrmException extends Exception { + + /** + * The requested DRM scheme is unsupported by the device. + */ + public static final int REASON_UNSUPPORTED_SCHEME = 1; + /** + * There device advertises support for the requested DRM scheme, but there was an error + * instantiating it. The cause can be retrieved using {@link #getCause()}. + */ + public static final int REASON_INSTANTIATION_ERROR = 2; + + public final int reason; + + public UnsupportedDrmException(int reason) { + this.reason = reason; + } + + public UnsupportedDrmException(int reason, Exception cause) { + super(cause); + this.reason = reason; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ChunkIndex.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ChunkIndex.java new file mode 100755 index 000000000..57b8c8167 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ChunkIndex.java @@ -0,0 +1,86 @@ +/* + * 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.messenger.exoplayer.extractor; + +import org.telegram.messenger.exoplayer.util.Util; + +/** + * Defines chunks of samples within a media stream. + */ +public final class ChunkIndex implements SeekMap { + + /** + * The number of chunks. + */ + public final int length; + + /** + * The chunk sizes, in bytes. + */ + public final int[] sizes; + + /** + * The chunk byte offsets. + */ + public final long[] offsets; + + /** + * The chunk durations, in microseconds. + */ + public final long[] durationsUs; + + /** + * The start time of each chunk, in microseconds. + */ + public final long[] timesUs; + + /** + * @param sizes The chunk sizes, in bytes. + * @param offsets The chunk byte offsets. + * @param durationsUs The chunk durations, in microseconds. + * @param timesUs The start time of each chunk, in microseconds. + */ + public ChunkIndex(int[] sizes, long[] offsets, long[] durationsUs, long[] timesUs) { + this.length = sizes.length; + this.sizes = sizes; + this.offsets = offsets; + this.durationsUs = durationsUs; + this.timesUs = timesUs; + } + + /** + * Obtains the index of the chunk corresponding to a given time. + * + * @param timeUs The time, in microseconds. + * @return The index of the corresponding chunk. + */ + public int getChunkIndex(long timeUs) { + return Util.binarySearchFloor(timesUs, timeUs, true, true); + } + + // SeekMap implementation. + + @Override + public boolean isSeekable() { + return true; + } + + @Override + public long getPosition(long timeUs) { + return offsets[getChunkIndex(timeUs)]; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/DefaultExtractorInput.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/DefaultExtractorInput.java new file mode 100755 index 000000000..7a4f6612e --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/DefaultExtractorInput.java @@ -0,0 +1,261 @@ +/* + * 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.messenger.exoplayer.extractor; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.upstream.DataSource; + +import java.io.EOFException; +import java.io.IOException; +import java.util.Arrays; + +/** + * An {@link ExtractorInput} that wraps a {@link DataSource}. + */ +public final class DefaultExtractorInput implements ExtractorInput { + + private static final byte[] SCRATCH_SPACE = new byte[4096]; + + private final DataSource dataSource; + private final long streamLength; + + private long position; + private byte[] peekBuffer; + private int peekBufferPosition; + private int peekBufferLength; + + /** + * @param dataSource The wrapped {@link DataSource}. + * @param position The initial position in the stream. + * @param length The length of the stream, or {@link C#LENGTH_UNBOUNDED} if it is unknown. + */ + public DefaultExtractorInput(DataSource dataSource, long position, long length) { + this.dataSource = dataSource; + this.position = position; + this.streamLength = length; + peekBuffer = new byte[8 * 1024]; + } + + @Override + public int read(byte[] target, int offset, int length) throws IOException, InterruptedException { + int bytesRead = readFromPeekBuffer(target, offset, length); + if (bytesRead == 0) { + bytesRead = readFromDataSource(target, offset, length, 0, true); + } + commitBytesRead(bytesRead); + return bytesRead; + } + + @Override + public boolean readFully(byte[] target, int offset, int length, boolean allowEndOfInput) + throws IOException, InterruptedException { + int bytesRead = readFromPeekBuffer(target, offset, length); + while (bytesRead < length && bytesRead != C.RESULT_END_OF_INPUT) { + bytesRead = readFromDataSource(target, offset, length, bytesRead, allowEndOfInput); + } + commitBytesRead(bytesRead); + return bytesRead != C.RESULT_END_OF_INPUT; + } + + @Override + public void readFully(byte[] target, int offset, int length) + throws IOException, InterruptedException { + readFully(target, offset, length, false); + } + + @Override + public int skip(int length) throws IOException, InterruptedException { + int bytesSkipped = skipFromPeekBuffer(length); + if (bytesSkipped == 0) { + bytesSkipped = + readFromDataSource(SCRATCH_SPACE, 0, Math.min(length, SCRATCH_SPACE.length), 0, true); + } + commitBytesRead(bytesSkipped); + return bytesSkipped; + } + + @Override + public boolean skipFully(int length, boolean allowEndOfInput) + throws IOException, InterruptedException { + int bytesSkipped = skipFromPeekBuffer(length); + while (bytesSkipped < length && bytesSkipped != C.RESULT_END_OF_INPUT) { + bytesSkipped = readFromDataSource(SCRATCH_SPACE, -bytesSkipped, + Math.min(length, bytesSkipped + SCRATCH_SPACE.length), bytesSkipped, allowEndOfInput); + } + commitBytesRead(bytesSkipped); + return bytesSkipped != C.RESULT_END_OF_INPUT; + } + + @Override + public void skipFully(int length) throws IOException, InterruptedException { + skipFully(length, false); + } + + @Override + public boolean peekFully(byte[] target, int offset, int length, boolean allowEndOfInput) + throws IOException, InterruptedException { + if (!advancePeekPosition(length, allowEndOfInput)) { + return false; + } + System.arraycopy(peekBuffer, peekBufferPosition - length, target, offset, length); + return true; + } + + @Override + public void peekFully(byte[] target, int offset, int length) + throws IOException, InterruptedException { + peekFully(target, offset, length, false); + } + + @Override + public boolean advancePeekPosition(int length, boolean allowEndOfInput) + throws IOException, InterruptedException { + ensureSpaceForPeek(length); + int bytesPeeked = Math.min(peekBufferLength - peekBufferPosition, length); + peekBufferLength += length - bytesPeeked; + while (bytesPeeked < length) { + bytesPeeked = readFromDataSource(peekBuffer, peekBufferPosition, length, bytesPeeked, + allowEndOfInput); + if (bytesPeeked == C.RESULT_END_OF_INPUT) { + return false; + } + } + peekBufferPosition += length; + return true; + } + + @Override + public void advancePeekPosition(int length) throws IOException, InterruptedException { + advancePeekPosition(length, false); + } + + @Override + public void resetPeekPosition() { + peekBufferPosition = 0; + } + + @Override + public long getPeekPosition() { + return position + peekBufferPosition; + } + + @Override + public long getPosition() { + return position; + } + + @Override + public long getLength() { + return streamLength; + } + + /** + * Ensures {@code peekBuffer} is large enough to store at least {@code length} bytes from the + * current peek position. + */ + private void ensureSpaceForPeek(int length) { + int requiredLength = peekBufferPosition + length; + if (requiredLength > peekBuffer.length) { + peekBuffer = Arrays.copyOf(peekBuffer, Math.max(peekBuffer.length * 2, requiredLength)); + } + } + + /** + * Skips from the peek buffer. + * + * @param length The maximum number of bytes to skip from the peek buffer. + * @return The number of bytes skipped. + */ + private int skipFromPeekBuffer(int length) { + int bytesSkipped = Math.min(peekBufferLength, length); + updatePeekBuffer(bytesSkipped); + return bytesSkipped; + } + + /** + * Reads from the peek buffer + * + * @param target A target array into which data should be written. + * @param offset The offset into the target array at which to write. + * @param length The maximum number of bytes to read from the peek buffer. + * @return The number of bytes read. + */ + private int readFromPeekBuffer(byte[] target, int offset, int length) { + if (peekBufferLength == 0) { + return 0; + } + int peekBytes = Math.min(peekBufferLength, length); + System.arraycopy(peekBuffer, 0, target, offset, peekBytes); + updatePeekBuffer(peekBytes); + return peekBytes; + } + + /** + * Updates the peek buffer's length, position and contents after consuming data. + * + * @param bytesConsumed The number of bytes consumed from the peek buffer. + */ + private void updatePeekBuffer(int bytesConsumed) { + peekBufferLength -= bytesConsumed; + peekBufferPosition = 0; + System.arraycopy(peekBuffer, bytesConsumed, peekBuffer, 0, peekBufferLength); + } + + /** + * Starts or continues a read from the data source. + * + * @param target A target array into which data should be written. + * @param offset The offset into the target array at which to write. + * @param length The maximum number of bytes to read from the input. + * @param bytesAlreadyRead The number of bytes already read from the input. + * @param allowEndOfInput True if encountering the end of the input having read no data is + * allowed, and should result in {@link C#RESULT_END_OF_INPUT} being returned. False if it + * should be considered an error, causing an {@link EOFException} to be thrown. + * @return The total number of bytes read so far, or {@link C#RESULT_END_OF_INPUT} if + * {@code allowEndOfInput} is true and the input has ended having read no bytes. + * @throws EOFException If the end of input was encountered having partially satisfied the read + * (i.e. having read at least one byte, but fewer than {@code length}), or if no bytes were + * read and {@code allowEndOfInput} is false. + * @throws IOException If an error occurs reading from the input. + * @throws InterruptedException If the thread is interrupted. + */ + private int readFromDataSource(byte[] target, int offset, int length, int bytesAlreadyRead, + boolean allowEndOfInput) throws InterruptedException, IOException { + if (Thread.interrupted()) { + throw new InterruptedException(); + } + int bytesRead = dataSource.read(target, offset + bytesAlreadyRead, length - bytesAlreadyRead); + if (bytesRead == C.RESULT_END_OF_INPUT) { + if (bytesAlreadyRead == 0 && allowEndOfInput) { + return C.RESULT_END_OF_INPUT; + } + throw new EOFException(); + } + return bytesAlreadyRead + bytesRead; + } + + /** + * Advances the position by the specified number of bytes read. + * + * @param bytesRead The number of bytes read. + */ + private void commitBytesRead(int bytesRead) { + if (bytesRead != C.RESULT_END_OF_INPUT) { + position += bytesRead; + } + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/DefaultTrackOutput.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/DefaultTrackOutput.java new file mode 100755 index 000000000..c769e8d51 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/DefaultTrackOutput.java @@ -0,0 +1,271 @@ +/* + * 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.messenger.exoplayer.extractor; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.MediaFormat; +import org.telegram.messenger.exoplayer.SampleHolder; +import org.telegram.messenger.exoplayer.upstream.Allocator; +import org.telegram.messenger.exoplayer.upstream.DataSource; +import org.telegram.messenger.exoplayer.util.ParsableByteArray; + +import java.io.EOFException; +import java.io.IOException; + +/** + * A {@link TrackOutput} that buffers extracted samples in a queue, and allows for consumption from + * that queue. + */ +public class DefaultTrackOutput implements TrackOutput { + + private final RollingSampleBuffer rollingBuffer; + private final SampleHolder sampleInfoHolder; + + // Accessed only by the consuming thread. + private boolean needKeyframe; + private long lastReadTimeUs; + private long spliceOutTimeUs; + + // Accessed by both the loading and consuming threads. + private volatile long largestParsedTimestampUs; + private volatile MediaFormat format; + + /** + * @param allocator An {@link Allocator} from which allocations for sample data can be obtained. + */ + public DefaultTrackOutput(Allocator allocator) { + rollingBuffer = new RollingSampleBuffer(allocator); + sampleInfoHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_DISABLED); + needKeyframe = true; + lastReadTimeUs = Long.MIN_VALUE; + spliceOutTimeUs = Long.MIN_VALUE; + largestParsedTimestampUs = Long.MIN_VALUE; + } + + // Called by the consuming thread, but only when there is no loading thread. + + /** + * Clears the queue, returning all allocations to the allocator. + */ + public void clear() { + rollingBuffer.clear(); + needKeyframe = true; + lastReadTimeUs = Long.MIN_VALUE; + spliceOutTimeUs = Long.MIN_VALUE; + largestParsedTimestampUs = Long.MIN_VALUE; + } + + /** + * Returns the current absolute write index. + */ + public int getWriteIndex() { + return rollingBuffer.getWriteIndex(); + } + + /** + * Discards samples from the write side of the queue. + * + * @param discardFromIndex The absolute index of the first sample to be discarded. + */ + public void discardUpstreamSamples(int discardFromIndex) { + rollingBuffer.discardUpstreamSamples(discardFromIndex); + largestParsedTimestampUs = rollingBuffer.peekSample(sampleInfoHolder) ? sampleInfoHolder.timeUs + : Long.MIN_VALUE; + } + + // Called by the consuming thread. + + /** + * Returns the current absolute read index. + */ + public int getReadIndex() { + return rollingBuffer.getReadIndex(); + } + + /** + * True if the output has received a format. False otherwise. + */ + public boolean hasFormat() { + return format != null; + } + + /** + * The format most recently received by the output, or null if a format has yet to be received. + */ + public MediaFormat getFormat() { + return format; + } + + /** + * The largest timestamp of any sample received by the output, or {@link Long#MIN_VALUE} if a + * sample has yet to be received. + */ + public long getLargestParsedTimestampUs() { + return largestParsedTimestampUs; + } + + /** + * True if at least one sample can be read from the queue. False otherwise. + */ + public boolean isEmpty() { + return !advanceToEligibleSample(); + } + + /** + * Removes the next sample from the head of the queue, writing it into the provided holder. + *

+ * The first sample returned is guaranteed to be a keyframe, since any non-keyframe samples + * queued prior to the first keyframe are discarded. + * + * @param holder A {@link SampleHolder} into which the sample should be read. + * @return True if a sample was read. False otherwise. + */ + public boolean getSample(SampleHolder holder) { + boolean foundEligibleSample = advanceToEligibleSample(); + if (!foundEligibleSample) { + return false; + } + // Write the sample into the holder. + rollingBuffer.readSample(holder); + needKeyframe = false; + lastReadTimeUs = holder.timeUs; + return true; + } + + /** + * Discards samples from the queue up to the specified time. + * + * @param timeUs The time up to which samples should be discarded, in microseconds. + */ + public void discardUntil(long timeUs) { + while (rollingBuffer.peekSample(sampleInfoHolder) && sampleInfoHolder.timeUs < timeUs) { + rollingBuffer.skipSample(); + // We're discarding one or more samples. A subsequent read will need to start at a keyframe. + needKeyframe = true; + } + lastReadTimeUs = Long.MIN_VALUE; + } + + /** + * Attempts to skip to the keyframe before the specified time, if it's present in the buffer. + * + * @param timeUs The seek time. + * @return True if the skip was successful. False otherwise. + */ + public boolean skipToKeyframeBefore(long timeUs) { + return rollingBuffer.skipToKeyframeBefore(timeUs); + } + + /** + * Attempts to configure a splice from this queue to the next. + * + * @param nextQueue The queue being spliced to. + * @return Whether the splice was configured successfully. + */ + public boolean configureSpliceTo(DefaultTrackOutput nextQueue) { + if (spliceOutTimeUs != Long.MIN_VALUE) { + // We've already configured the splice. + return true; + } + long firstPossibleSpliceTime; + if (rollingBuffer.peekSample(sampleInfoHolder)) { + firstPossibleSpliceTime = sampleInfoHolder.timeUs; + } else { + firstPossibleSpliceTime = lastReadTimeUs + 1; + } + RollingSampleBuffer nextRollingBuffer = nextQueue.rollingBuffer; + while (nextRollingBuffer.peekSample(sampleInfoHolder) + && (sampleInfoHolder.timeUs < firstPossibleSpliceTime || !sampleInfoHolder.isSyncFrame())) { + // Discard samples from the next queue for as long as they are before the earliest possible + // splice time, or not keyframes. + nextRollingBuffer.skipSample(); + } + if (nextRollingBuffer.peekSample(sampleInfoHolder)) { + // We've found a keyframe in the next queue that can serve as the splice point. Set the + // splice point now. + spliceOutTimeUs = sampleInfoHolder.timeUs; + return true; + } + return false; + } + + /** + * Advances the underlying buffer to the next sample that is eligible to be returned. + * + * @return True if an eligible sample was found. False otherwise, in which case the underlying + * buffer has been emptied. + */ + private boolean advanceToEligibleSample() { + boolean haveNext = rollingBuffer.peekSample(sampleInfoHolder); + if (needKeyframe) { + while (haveNext && !sampleInfoHolder.isSyncFrame()) { + rollingBuffer.skipSample(); + haveNext = rollingBuffer.peekSample(sampleInfoHolder); + } + } + if (!haveNext) { + return false; + } + if (spliceOutTimeUs != Long.MIN_VALUE && sampleInfoHolder.timeUs >= spliceOutTimeUs) { + return false; + } + return true; + } + + // Called by the loading thread. + + /** + * Invoked to write sample data to the output. + * + * @param dataSource A {@link DataSource} from which to read the sample data. + * @param length The maximum length to read from the input. + * @param allowEndOfInput True if encountering the end of the input having read no data is + * allowed, and should result in {@link C#RESULT_END_OF_INPUT} being returned. False if it + * should be considered an error, causing an {@link EOFException} to be thrown. + * @return The number of bytes appended. + * @throws IOException If an error occurred reading from the input. + */ + public int sampleData(DataSource dataSource, int length, boolean allowEndOfInput) + throws IOException { + return rollingBuffer.appendData(dataSource, length, allowEndOfInput); + } + + // TrackOutput implementation. Called by the loading thread. + + @Override + public void format(MediaFormat format) { + this.format = format; + } + + @Override + public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput) + throws IOException, InterruptedException { + return rollingBuffer.appendData(input, length, allowEndOfInput); + } + + @Override + public void sampleData(ParsableByteArray buffer, int length) { + rollingBuffer.appendData(buffer, length); + } + + @Override + public void sampleMetadata(long timeUs, int flags, int size, int offset, byte[] encryptionKey) { + largestParsedTimestampUs = Math.max(largestParsedTimestampUs, timeUs); + rollingBuffer.commitSample(timeUs, flags, rollingBuffer.getWritePosition() - size - offset, + size, encryptionKey); + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/DummyTrackOutput.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/DummyTrackOutput.java new file mode 100755 index 000000000..5a2441480 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/DummyTrackOutput.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2015 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.messenger.exoplayer.extractor; + +import org.telegram.messenger.exoplayer.MediaFormat; +import org.telegram.messenger.exoplayer.util.ParsableByteArray; + +import java.io.IOException; + +/** + * A dummy {@link TrackOutput} implementation. + */ +public class DummyTrackOutput implements TrackOutput { + @Override + public void format(MediaFormat format) { + // Do nothing. + } + + @Override + public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput) + throws IOException, InterruptedException { + return input.skip(length); + } + + @Override + public void sampleData(ParsableByteArray data, int length) { + data.skipBytes(length); + } + + @Override + public void sampleMetadata(long timeUs, int flags, int size, int offset, byte[] encryptionKey) { + // Do nothing. + } +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/Extractor.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/Extractor.java new file mode 100755 index 000000000..b7e18b7ea --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/Extractor.java @@ -0,0 +1,105 @@ +/* + * 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.messenger.exoplayer.extractor; + +import org.telegram.messenger.exoplayer.C; + +import java.io.IOException; + +/** + * Facilitates extraction of data from a container format. + */ +public interface Extractor { + + /** + * Returned by {@link #read(ExtractorInput, PositionHolder)} if the {@link ExtractorInput} passed + * to the next {@link #read(ExtractorInput, PositionHolder)} is required to provide data + * continuing from the position in the stream reached by the returning call. + */ + public static final int RESULT_CONTINUE = 0; + /** + * Returned by {@link #read(ExtractorInput, PositionHolder)} if the {@link ExtractorInput} passed + * to the next {@link #read(ExtractorInput, PositionHolder)} is required to provide data starting + * from a specified position in the stream. + */ + public static final int RESULT_SEEK = 1; + /** + * Returned by {@link #read(ExtractorInput, PositionHolder)} if the end of the + * {@link ExtractorInput} was reached. Equal to {@link C#RESULT_END_OF_INPUT}. + */ + public static final int RESULT_END_OF_INPUT = C.RESULT_END_OF_INPUT; + + /** + * Initializes the extractor with an {@link ExtractorOutput}. + * + * @param output An {@link ExtractorOutput} to receive extracted data. + */ + void init(ExtractorOutput output); + + /** + * Returns whether this extractor can extract samples from the {@link ExtractorInput}, which must + * provide data from the start of the stream. + *

+ * If {@code true} is returned, the {@code input}'s reading position may have been modified. + * Otherwise, only its peek position may have been modified. + * + * @param input The {@link ExtractorInput} from which data should be peeked/read. + * @return Whether this extractor can read the provided input. + * @throws IOException If an error occurred reading from the input. + * @throws InterruptedException If the thread was interrupted. + */ + boolean sniff(ExtractorInput input) throws IOException, InterruptedException; + + /** + * Extracts data read from a provided {@link ExtractorInput}. + *

+ * A single call to this method will block until some progress has been made, but will not block + * for longer than this. Hence each call will consume only a small amount of input data. + *

+ * In the common case, {@link #RESULT_CONTINUE} is returned to indicate that the + * {@link ExtractorInput} passed to the next read is required to provide data continuing from the + * position in the stream reached by the returning call. If the extractor requires data to be + * provided from a different position, then that position is set in {@code seekPosition} and + * {@link #RESULT_SEEK} is returned. If the extractor reached the end of the data provided by the + * {@link ExtractorInput}, then {@link #RESULT_END_OF_INPUT} is returned. + * + * @param input The {@link ExtractorInput} from which data should be read. + * @param seekPosition If {@link #RESULT_SEEK} is returned, this holder is updated to hold the + * position of the required data. + * @return One of the {@code RESULT_} values defined in this interface. + * @throws IOException If an error occurred reading from the input. + * @throws InterruptedException If the thread was interrupted. + */ + int read(ExtractorInput input, PositionHolder seekPosition) + throws IOException, InterruptedException; + + /** + * Notifies the extractor that a seek has occurred. + *

+ * Following a call to this method, the {@link ExtractorInput} passed to the next invocation of + * {@link #read(ExtractorInput, PositionHolder)} is required to provide data starting from a + * random access position in the stream. Valid random access positions are the start of the + * stream and positions that can be obtained from any {@link SeekMap} passed to the + * {@link ExtractorOutput}. + */ + void seek(); + + /** + * Releases all kept resources. + */ + void release(); + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ExtractorInput.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ExtractorInput.java new file mode 100755 index 000000000..e21c4abaa --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ExtractorInput.java @@ -0,0 +1,226 @@ +/* + * 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.messenger.exoplayer.extractor; + +import org.telegram.messenger.exoplayer.C; + +import java.io.EOFException; +import java.io.IOException; + +/** + * Provides data to be consumed by an {@link Extractor}. + */ +public interface ExtractorInput { + + /** + * Reads up to {@code length} bytes from the input and resets the peek position. + *

+ * This method blocks until at least one byte of data can be read, the end of the input is + * detected, or an exception is thrown. + * + * @param target A target array into which data should be written. + * @param offset The offset into the target array at which to write. + * @param length The maximum number of bytes to read from the input. + * @return The number of bytes read, or {@link C#RESULT_END_OF_INPUT} if the input has ended. + * @throws IOException If an error occurs reading from the input. + * @throws InterruptedException If the thread has been interrupted. + */ + int read(byte[] target, int offset, int length) throws IOException, InterruptedException; + + /** + * Like {@link #read(byte[], int, int)}, but reads the requested {@code length} in full. + *

+ * If the end of the input is found having read no data, then behavior is dependent on + * {@code allowEndOfInput}. If {@code allowEndOfInput == true} then {@code false} is returned. + * Otherwise an {@link EOFException} is thrown. + *

+ * Encountering the end of input having partially satisfied the read is always considered an + * error, and will result in an {@link EOFException} being thrown. + * + * @param target A target array into which data should be written. + * @param offset The offset into the target array at which to write. + * @param length The number of bytes to read from the input. + * @param allowEndOfInput True if encountering the end of the input having read no data is + * allowed, and should result in {@code false} being returned. False if it should be + * considered an error, causing an {@link EOFException} to be thrown. + * @return True if the read was successful. False if the end of the input was encountered having + * read no data. + * @throws EOFException If the end of input was encountered having partially satisfied the read + * (i.e. having read at least one byte, but fewer than {@code length}), or if no bytes were + * read and {@code allowEndOfInput} is false. + * @throws IOException If an error occurs reading from the input. + * @throws InterruptedException If the thread has been interrupted. + */ + boolean readFully(byte[] target, int offset, int length, boolean allowEndOfInput) + throws IOException, InterruptedException; + + /** + * Equivalent to {@code readFully(target, offset, length, false)}. + * + * @param target A target array into which data should be written. + * @param offset The offset into the target array at which to write. + * @param length The number of bytes to read from the input. + * @throws EOFException If the end of input was encountered. + * @throws IOException If an error occurs reading from the input. + * @throws InterruptedException If the thread is interrupted. + */ + void readFully(byte[] target, int offset, int length) throws IOException, InterruptedException; + + /** + * Like {@link #read(byte[], int, int)}, except the data is skipped instead of read. + * + * @param length The maximum number of bytes to skip from the input. + * @return The number of bytes skipped, or {@link C#RESULT_END_OF_INPUT} if the input has ended. + * @throws IOException If an error occurs reading from the input. + * @throws InterruptedException If the thread has been interrupted. + */ + int skip(int length) throws IOException, InterruptedException; + + /** + * Like {@link #readFully(byte[], int, int, boolean)}, except the data is skipped instead of read. + * + * @param length The number of bytes to skip from the input. + * @param allowEndOfInput True if encountering the end of the input having skipped no data is + * allowed, and should result in {@code false} being returned. False if it should be + * considered an error, causing an {@link EOFException} to be thrown. + * @return True if the skip was successful. False if the end of the input was encountered having + * skipped no data. + * @throws EOFException If the end of input was encountered having partially satisfied the skip + * (i.e. having skipped at least one byte, but fewer than {@code length}), or if no bytes were + * skipped and {@code allowEndOfInput} is false. + * @throws IOException If an error occurs reading from the input. + * @throws InterruptedException If the thread has been interrupted. + */ + boolean skipFully(int length, boolean allowEndOfInput) throws IOException, InterruptedException; + + /** + * Like {@link #readFully(byte[], int, int)}, except the data is skipped instead of read. + *

+ * Encountering the end of input is always considered an error, and will result in an + * {@link EOFException} being thrown. + * + * @param length The number of bytes to skip from the input. + * @throws EOFException If the end of input was encountered. + * @throws IOException If an error occurs reading from the input. + * @throws InterruptedException If the thread is interrupted. + */ + void skipFully(int length) throws IOException, InterruptedException; + + /** + * Peeks {@code length} bytes from the peek position, writing them into {@code target} at index + * {@code offset}. The current read position is left unchanged. + *

+ * If the end of the input is found having peeked no data, then behavior is dependent on + * {@code allowEndOfInput}. If {@code allowEndOfInput == true} then {@code false} is returned. + * Otherwise an {@link EOFException} is thrown. + *

+ * Calling {@link #resetPeekPosition()} resets the peek position to equal the current read + * position, so the caller can peek the same data again. Reading and skipping also reset the peek + * position. + * + * @param target A target array into which data should be written. + * @param offset The offset into the target array at which to write. + * @param length The number of bytes to peek from the input. + * @param allowEndOfInput True if encountering the end of the input having peeked no data is + * allowed, and should result in {@code false} being returned. False if it should be + * considered an error, causing an {@link EOFException} to be thrown. + * @return True if the peek was successful. False if the end of the input was encountered having + * peeked no data. + * @throws EOFException If the end of input was encountered having partially satisfied the peek + * (i.e. having peeked at least one byte, but fewer than {@code length}), or if no bytes were + * peeked and {@code allowEndOfInput} is false. + * @throws IOException If an error occurs peeking from the input. + * @throws InterruptedException If the thread is interrupted. + */ + boolean peekFully(byte[] target, int offset, int length, boolean allowEndOfInput) + throws IOException, InterruptedException; + + /** + * Peeks {@code length} bytes from the peek position, writing them into {@code target} at index + * {@code offset}. The current read position is left unchanged. + *

+ * Calling {@link #resetPeekPosition()} resets the peek position to equal the current read + * position, so the caller can peek the same data again. Reading or skipping also resets the peek + * position. + * + * @param target A target array into which data should be written. + * @param offset The offset into the target array at which to write. + * @param length The number of bytes to peek from the input. + * @throws EOFException If the end of input was encountered. + * @throws IOException If an error occurs peeking from the input. + * @throws InterruptedException If the thread is interrupted. + */ + void peekFully(byte[] target, int offset, int length) throws IOException, InterruptedException; + + /** + * Advances the peek position by {@code length} bytes. + *

+ * If the end of the input is encountered before advancing the peek position, then behavior is + * dependent on {@code allowEndOfInput}. If {@code allowEndOfInput == true} then {@code false} is + * returned. Otherwise an {@link EOFException} is thrown. + * + * @param length The number of bytes by which to advance the peek position. + * @param allowEndOfInput True if encountering the end of the input before advancing is allowed, + * and should result in {@code false} being returned. False if it should be considered an + * error, causing an {@link EOFException} to be thrown. + * @return True if advancing the peek position was successful. False if the end of the input was + * encountered before the peek position could be advanced. + * @throws EOFException If the end of input was encountered having partially advanced (i.e. having + * advanced by at least one byte, but fewer than {@code length}), or if the end of input was + * encountered before advancing and {@code allowEndOfInput} is false. + * @throws IOException If an error occurs advancing the peek position. + * @throws InterruptedException If the thread is interrupted. + */ + boolean advancePeekPosition(int length, boolean allowEndOfInput) + throws IOException, InterruptedException; + + /** + * Advances the peek position by {@code length} bytes. + * + * @param length The number of bytes to peek from the input. + * @throws EOFException If the end of input was encountered. + * @throws IOException If an error occurs peeking from the input. + * @throws InterruptedException If the thread is interrupted. + */ + void advancePeekPosition(int length) throws IOException, InterruptedException; + + /** + * Resets the peek position to equal the current read position. + */ + void resetPeekPosition(); + + /** + * Returns the current peek position (byte offset) in the stream. + * + * @return The peek position (byte offset) in the stream. + */ + long getPeekPosition(); + + /** + * Returns the current read position (byte offset) in the stream. + * + * @return The read position (byte offset) in the stream. + */ + long getPosition(); + + /** + * Returns the length of the source stream, or {@link C#LENGTH_UNBOUNDED} if it is unknown. + * + * @return The length of the source stream, or {@link C#LENGTH_UNBOUNDED}. + */ + long getLength(); + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ExtractorOutput.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ExtractorOutput.java new file mode 100755 index 000000000..6b2ee095a --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ExtractorOutput.java @@ -0,0 +1,55 @@ +/* + * 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.messenger.exoplayer.extractor; + +import org.telegram.messenger.exoplayer.drm.DrmInitData; + +/** + * Receives stream level data extracted by an {@link Extractor}. + */ +public interface ExtractorOutput { + + /** + * Invoked when the {@link Extractor} identifies the existence of a track in the stream. + *

+ * Returns a {@link TrackOutput} that will receive track level data belonging to the track. + * + * @param trackId A track identifier. + * @return The {@link TrackOutput} that should receive track level data belonging to the track. + */ + TrackOutput track(int trackId); + + /** + * Invoked when all tracks have been identified, meaning that {@link #track(int)} will not be + * invoked again. + */ + void endTracks(); + + /** + * Invoked when a {@link SeekMap} has been extracted from the stream. + * + * @param seekMap The extracted {@link SeekMap}. + */ + void seekMap(SeekMap seekMap); + + /** + * Invoked when {@link DrmInitData} has been extracted from the stream. + * + * @param drmInitData The extracted {@link DrmInitData}. + */ + void drmInitData(DrmInitData drmInitData); + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ExtractorSampleSource.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ExtractorSampleSource.java new file mode 100755 index 000000000..bd1ea6c68 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ExtractorSampleSource.java @@ -0,0 +1,907 @@ +/* + * 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.messenger.exoplayer.extractor; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.MediaFormat; +import org.telegram.messenger.exoplayer.MediaFormatHolder; +import org.telegram.messenger.exoplayer.ParserException; +import org.telegram.messenger.exoplayer.SampleHolder; +import org.telegram.messenger.exoplayer.SampleSource; +import org.telegram.messenger.exoplayer.SampleSource.SampleSourceReader; +import org.telegram.messenger.exoplayer.TrackRenderer; +import org.telegram.messenger.exoplayer.drm.DrmInitData; +import org.telegram.messenger.exoplayer.upstream.Allocator; +import org.telegram.messenger.exoplayer.upstream.DataSource; +import org.telegram.messenger.exoplayer.upstream.DataSpec; +import org.telegram.messenger.exoplayer.upstream.Loader; +import org.telegram.messenger.exoplayer.upstream.Loader.Loadable; +import org.telegram.messenger.exoplayer.util.Assertions; +import org.telegram.messenger.exoplayer.util.Util; + +import android.net.Uri; +import android.os.Handler; +import android.os.SystemClock; +import android.util.SparseArray; + +import java.io.EOFException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * A {@link SampleSource} that extracts sample data using an {@link Extractor}. + * + *

If no {@link Extractor} instances are passed to the constructor, the input stream container + * format will be detected automatically from the following supported formats: + * + *

    + *
  • MP4, including M4A ({@link org.telegram.messenger.exoplayer.extractor.mp4.Mp4Extractor})
  • + *
  • fMP4 ({@link org.telegram.messenger.exoplayer.extractor.mp4.FragmentedMp4Extractor})
  • + *
  • Matroska and WebM ({@link org.telegram.messenger.exoplayer.extractor.webm.WebmExtractor})
  • + *
  • Ogg Vorbis/FLAC ({@link org.telegram.messenger.exoplayer.extractor.ogg.OggExtractor}
  • + *
  • MP3 ({@link org.telegram.messenger.exoplayer.extractor.mp3.Mp3Extractor})
  • + *
  • AAC ({@link org.telegram.messenger.exoplayer.extractor.ts.AdtsExtractor})
  • + *
  • MPEG TS ({@link org.telegram.messenger.exoplayer.extractor.ts.TsExtractor})
  • + *
  • MPEG PS ({@link org.telegram.messenger.exoplayer.extractor.ts.PsExtractor})
  • + *
  • FLV ({@link org.telegram.messenger.exoplayer.extractor.flv.FlvExtractor})
  • + *
  • WAV ({@link org.telegram.messenger.exoplayer.extractor.wav.WavExtractor})
  • + *
  • FLAC (only available if the FLAC extension is built and included)
  • + *
+ * + *

Seeking in AAC, MPEG TS and FLV streams is not supported. + * + *

To override the default extractors, pass one or more {@link Extractor} instances to the + * constructor. When reading a new stream, the first {@link Extractor} that returns {@code true} + * from {@link Extractor#sniff(ExtractorInput)} will be used. + */ +public final class ExtractorSampleSource implements SampleSource, SampleSourceReader, + ExtractorOutput, Loader.Callback { + + /** + * Interface definition for a callback to be notified of {@link ExtractorSampleSource} events. + */ + public interface EventListener { + + /** + * Invoked when an error occurs loading media data. + * + * @param sourceId The id of the reporting {@link SampleSource}. + * @param e The cause of the failure. + */ + void onLoadError(int sourceId, IOException e); + + } + + /** + * Thrown if the input format could not recognized. + */ + public static final class UnrecognizedInputFormatException extends ParserException { + + public UnrecognizedInputFormatException(Extractor[] extractors) { + super("None of the available extractors (" + + Util.getCommaDelimitedSimpleClassNames(extractors) + ") could read the stream."); + } + + } + + /** + * The default minimum number of times to retry loading prior to failing for on-demand streams. + */ + public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT_ON_DEMAND = 3; + + /** + * The default minimum number of times to retry loading prior to failing for live streams. + */ + public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT_LIVE = 6; + + private static final int MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA = -1; + private static final long NO_RESET_PENDING = Long.MIN_VALUE; + + /** + * Default extractor classes in priority order. They are referred to indirectly so that it is + * possible to remove unused extractors. + */ + private static final List> DEFAULT_EXTRACTOR_CLASSES; + static { + DEFAULT_EXTRACTOR_CLASSES = new ArrayList<>(); + // Load extractors using reflection so that they can be deleted cleanly. + // Class.forName() appears for each extractor so that automated tools like proguard + // can detect the use of reflection (see http://proguard.sourceforge.net/FAQ.html#forname). + try { + DEFAULT_EXTRACTOR_CLASSES.add( + Class.forName("org.telegram.messenger.exoplayer.extractor.webm.WebmExtractor") + .asSubclass(Extractor.class)); + } catch (ClassNotFoundException e) { + // Extractor not found. + } + try { + DEFAULT_EXTRACTOR_CLASSES.add( + Class.forName("org.telegram.messenger.exoplayer.extractor.mp4.FragmentedMp4Extractor") + .asSubclass(Extractor.class)); + } catch (ClassNotFoundException e) { + // Extractor not found. + } + try { + DEFAULT_EXTRACTOR_CLASSES.add( + Class.forName("org.telegram.messenger.exoplayer.extractor.mp4.Mp4Extractor") + .asSubclass(Extractor.class)); + } catch (ClassNotFoundException e) { + // Extractor not found. + } + try { + DEFAULT_EXTRACTOR_CLASSES.add( + Class.forName("org.telegram.messenger.exoplayer.extractor.mp3.Mp3Extractor") + .asSubclass(Extractor.class)); + } catch (ClassNotFoundException e) { + // Extractor not found. + } + try { + DEFAULT_EXTRACTOR_CLASSES.add( + Class.forName("org.telegram.messenger.exoplayer.extractor.ts.AdtsExtractor") + .asSubclass(Extractor.class)); + } catch (ClassNotFoundException e) { + // Extractor not found. + } + try { + DEFAULT_EXTRACTOR_CLASSES.add( + Class.forName("org.telegram.messenger.exoplayer.extractor.ts.TsExtractor") + .asSubclass(Extractor.class)); + } catch (ClassNotFoundException e) { + // Extractor not found. + } + try { + DEFAULT_EXTRACTOR_CLASSES.add( + Class.forName("org.telegram.messenger.exoplayer.extractor.flv.FlvExtractor") + .asSubclass(Extractor.class)); + } catch (ClassNotFoundException e) { + // Extractor not found. + } + try { + DEFAULT_EXTRACTOR_CLASSES.add( + Class.forName("org.telegram.messenger.exoplayer.extractor.ogg.OggExtractor") + .asSubclass(Extractor.class)); + } catch (ClassNotFoundException e) { + // Extractor not found. + } + try { + DEFAULT_EXTRACTOR_CLASSES.add( + Class.forName("org.telegram.messenger.exoplayer.extractor.ts.PsExtractor") + .asSubclass(Extractor.class)); + } catch (ClassNotFoundException e) { + // Extractor not found. + } + try { + DEFAULT_EXTRACTOR_CLASSES.add( + Class.forName("org.telegram.messenger.exoplayer.extractor.wav.WavExtractor") + .asSubclass(Extractor.class)); + } catch (ClassNotFoundException e) { + // Extractor not found. + } + try { + DEFAULT_EXTRACTOR_CLASSES.add( + Class.forName("org.telegram.messenger.exoplayer.ext.flac.FlacExtractor") + .asSubclass(Extractor.class)); + } catch (ClassNotFoundException e) { + // Extractor not found. + } + } + + private final ExtractorHolder extractorHolder; + private final Allocator allocator; + private final int requestedBufferSize; + private final SparseArray sampleQueues; + private final int minLoadableRetryCount; + private final Uri uri; + private final DataSource dataSource; + private final Handler eventHandler; + private final EventListener eventListener; + private final int eventSourceId; + + private volatile boolean tracksBuilt; + private volatile SeekMap seekMap; + private volatile DrmInitData drmInitData; + + private boolean prepared; + private int enabledTrackCount; + private MediaFormat[] mediaFormats; + private long maxTrackDurationUs; + private boolean[] pendingMediaFormat; + private boolean[] pendingDiscontinuities; + private boolean[] trackEnabledStates; + + private int remainingReleaseCount; + private long downstreamPositionUs; + private long lastSeekPositionUs; + private long pendingResetPositionUs; + + private boolean havePendingNextSampleUs; + private long pendingNextSampleUs; + private long sampleTimeOffsetUs; + + private Loader loader; + private ExtractingLoadable loadable; + private IOException currentLoadableException; + // TODO: Set this back to 0 in the correct place (some place indicative of making progress). + private int currentLoadableExceptionCount; + private long currentLoadableExceptionTimestamp; + private boolean loadingFinished; + + private int extractedSampleCount; + private int extractedSampleCountAtStartOfLoad; + + /** + * @param uri The {@link Uri} of the media stream. + * @param dataSource A data source to read the media stream. + * @param allocator An {@link Allocator} from which to obtain memory allocations. + * @param requestedBufferSize The requested total buffer size for storing sample data, in bytes. + * The actual allocated size may exceed the value passed in if the implementation requires it. + * @param extractors {@link Extractor}s to extract the media stream, in order of decreasing + * priority. If omitted, the default extractors will be used. + */ + public ExtractorSampleSource(Uri uri, DataSource dataSource, Allocator allocator, + int requestedBufferSize, Extractor... extractors) { + this(uri, dataSource, allocator, requestedBufferSize, MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA, + extractors); + } + + /** + * @param uri The {@link Uri} of the media stream. + * @param dataSource A data source to read the media stream. + * @param allocator An {@link Allocator} from which to obtain memory allocations. + * @param requestedBufferSize The requested total buffer size for storing sample data, in bytes. + * The actual allocated size may exceed the value passed in if the implementation requires it. + * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be + * null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + * @param eventSourceId An identifier that gets passed to {@code eventListener} methods. + * @param extractors {@link Extractor}s to extract the media stream, in order of decreasing + * priority. If omitted, the default extractors will be used. + */ + public ExtractorSampleSource(Uri uri, DataSource dataSource, Allocator allocator, + int requestedBufferSize, Handler eventHandler, EventListener eventListener, + int eventSourceId, Extractor... extractors) { + this(uri, dataSource, allocator, requestedBufferSize, MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA, + eventHandler, eventListener, eventSourceId, extractors); + } + + /** + * @param uri The {@link Uri} of the media stream. + * @param dataSource A data source to read the media stream. + * @param allocator An {@link Allocator} from which to obtain memory allocations. + * @param requestedBufferSize The requested total buffer size for storing sample data, in bytes. + * The actual allocated size may exceed the value passed in if the implementation requires it. + * @param minLoadableRetryCount The minimum number of times that the sample source will retry + * if a loading error occurs. + * @param extractors {@link Extractor}s to extract the media stream, in order of decreasing + * priority. If omitted, the default extractors will be used. + */ + public ExtractorSampleSource(Uri uri, DataSource dataSource, Allocator allocator, + int requestedBufferSize, int minLoadableRetryCount, Extractor... extractors) { + this(uri, dataSource, allocator, requestedBufferSize, minLoadableRetryCount, null, null, 0, + extractors); + } + + /** + * @param uri The {@link Uri} of the media stream. + * @param dataSource A data source to read the media stream. + * @param allocator An {@link Allocator} from which to obtain memory allocations. + * @param requestedBufferSize The requested total buffer size for storing sample data, in bytes. + * The actual allocated size may exceed the value passed in if the implementation requires it. + * @param minLoadableRetryCount The minimum number of times that the sample source will retry + * if a loading error occurs. + * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be + * null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + * @param eventSourceId An identifier that gets passed to {@code eventListener} methods. + * @param extractors {@link Extractor}s to extract the media stream, in order of decreasing + * priority. If omitted, the default extractors will be used. + */ + public ExtractorSampleSource(Uri uri, DataSource dataSource, Allocator allocator, + int requestedBufferSize, int minLoadableRetryCount, Handler eventHandler, + EventListener eventListener, int eventSourceId, Extractor... extractors) { + this.uri = uri; + this.dataSource = dataSource; + this.eventListener = eventListener; + this.eventHandler = eventHandler; + this.eventSourceId = eventSourceId; + this.allocator = allocator; + this.requestedBufferSize = requestedBufferSize; + this.minLoadableRetryCount = minLoadableRetryCount; + if (extractors == null || extractors.length == 0) { + extractors = new Extractor[DEFAULT_EXTRACTOR_CLASSES.size()]; + for (int i = 0; i < extractors.length; i++) { + try { + extractors[i] = DEFAULT_EXTRACTOR_CLASSES.get(i).newInstance(); + } catch (InstantiationException e) { + throw new IllegalStateException("Unexpected error creating default extractor", e); + } catch (IllegalAccessException e) { + throw new IllegalStateException("Unexpected error creating default extractor", e); + } + } + } + extractorHolder = new ExtractorHolder(extractors, this); + sampleQueues = new SparseArray<>(); + pendingResetPositionUs = NO_RESET_PENDING; + } + + @Override + public SampleSourceReader register() { + remainingReleaseCount++; + return this; + } + + @Override + public boolean prepare(long positionUs) { + if (prepared) { + return true; + } + if (loader == null) { + loader = new Loader("Loader:ExtractorSampleSource"); + } + + maybeStartLoading(); + + if (seekMap != null && tracksBuilt && haveFormatsForAllTracks()) { + int trackCount = sampleQueues.size(); + trackEnabledStates = new boolean[trackCount]; + pendingDiscontinuities = new boolean[trackCount]; + pendingMediaFormat = new boolean[trackCount]; + mediaFormats = new MediaFormat[trackCount]; + maxTrackDurationUs = C.UNKNOWN_TIME_US; + for (int i = 0; i < trackCount; i++) { + MediaFormat format = sampleQueues.valueAt(i).getFormat(); + mediaFormats[i] = format; + if (format.durationUs != C.UNKNOWN_TIME_US && format.durationUs > maxTrackDurationUs) { + maxTrackDurationUs = format.durationUs; + } + } + prepared = true; + return true; + } + + return false; + } + + @Override + public int getTrackCount() { + return sampleQueues.size(); + } + + @Override + public MediaFormat getFormat(int track) { + Assertions.checkState(prepared); + return mediaFormats[track]; + } + + @Override + public void enable(int track, long positionUs) { + Assertions.checkState(prepared); + Assertions.checkState(!trackEnabledStates[track]); + enabledTrackCount++; + trackEnabledStates[track] = true; + pendingMediaFormat[track] = true; + pendingDiscontinuities[track] = false; + if (enabledTrackCount == 1) { + // Treat all enables in non-seekable media as being from t=0. + positionUs = !seekMap.isSeekable() ? 0 : positionUs; + downstreamPositionUs = positionUs; + lastSeekPositionUs = positionUs; + restartFrom(positionUs); + } + } + + @Override + public void disable(int track) { + Assertions.checkState(prepared); + Assertions.checkState(trackEnabledStates[track]); + enabledTrackCount--; + trackEnabledStates[track] = false; + if (enabledTrackCount == 0) { + downstreamPositionUs = Long.MIN_VALUE; + if (loader.isLoading()) { + loader.cancelLoading(); + } else { + clearState(); + allocator.trim(0); + } + } + } + + @Override + public boolean continueBuffering(int track, long playbackPositionUs) { + Assertions.checkState(prepared); + Assertions.checkState(trackEnabledStates[track]); + downstreamPositionUs = playbackPositionUs; + discardSamplesForDisabledTracks(downstreamPositionUs); + if (loadingFinished) { + return true; + } + maybeStartLoading(); + if (isPendingReset()) { + return false; + } + return !sampleQueues.valueAt(track).isEmpty(); + } + + @Override + public long readDiscontinuity(int track) { + if (pendingDiscontinuities[track]) { + pendingDiscontinuities[track] = false; + return lastSeekPositionUs; + } + return NO_DISCONTINUITY; + } + + @Override + public int readData(int track, long playbackPositionUs, MediaFormatHolder formatHolder, + SampleHolder sampleHolder) { + downstreamPositionUs = playbackPositionUs; + + if (pendingDiscontinuities[track] || isPendingReset()) { + return NOTHING_READ; + } + + InternalTrackOutput sampleQueue = sampleQueues.valueAt(track); + if (pendingMediaFormat[track]) { + formatHolder.format = sampleQueue.getFormat(); + formatHolder.drmInitData = drmInitData; + pendingMediaFormat[track] = false; + return FORMAT_READ; + } + + if (sampleQueue.getSample(sampleHolder)) { + boolean decodeOnly = sampleHolder.timeUs < lastSeekPositionUs; + sampleHolder.flags |= decodeOnly ? C.SAMPLE_FLAG_DECODE_ONLY : 0; + if (havePendingNextSampleUs) { + // Set the offset to make the timestamp of this sample equal to pendingNextSampleUs. + sampleTimeOffsetUs = pendingNextSampleUs - sampleHolder.timeUs; + havePendingNextSampleUs = false; + } + sampleHolder.timeUs += sampleTimeOffsetUs; + return SAMPLE_READ; + } + + if (loadingFinished) { + return END_OF_STREAM; + } + + return NOTHING_READ; + } + + @Override + public void maybeThrowError() throws IOException { + if (currentLoadableException == null) { + return; + } + if (isCurrentLoadableExceptionFatal()) { + throw currentLoadableException; + } + int minLoadableRetryCountForMedia; + if (minLoadableRetryCount != MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA) { + minLoadableRetryCountForMedia = minLoadableRetryCount; + } else { + minLoadableRetryCountForMedia = seekMap != null && !seekMap.isSeekable() + ? DEFAULT_MIN_LOADABLE_RETRY_COUNT_LIVE + : DEFAULT_MIN_LOADABLE_RETRY_COUNT_ON_DEMAND; + } + if (currentLoadableExceptionCount > minLoadableRetryCountForMedia) { + throw currentLoadableException; + } + } + + @Override + public void seekToUs(long positionUs) { + Assertions.checkState(prepared); + Assertions.checkState(enabledTrackCount > 0); + // Treat all seeks into non-seekable media as being to t=0. + positionUs = !seekMap.isSeekable() ? 0 : positionUs; + + long currentPositionUs = isPendingReset() ? pendingResetPositionUs : downstreamPositionUs; + downstreamPositionUs = positionUs; + lastSeekPositionUs = positionUs; + if (currentPositionUs == positionUs) { + return; + } + + // If we're not pending a reset, see if we can seek within the sample queues. + boolean seekInsideBuffer = !isPendingReset(); + for (int i = 0; seekInsideBuffer && i < sampleQueues.size(); i++) { + seekInsideBuffer &= sampleQueues.valueAt(i).skipToKeyframeBefore(positionUs); + } + + // If we failed to seek within the sample queues, we need to restart. + if (!seekInsideBuffer) { + restartFrom(positionUs); + } + + // Either way, we need to send discontinuities to the downstream components. + for (int i = 0; i < pendingDiscontinuities.length; i++) { + pendingDiscontinuities[i] = true; + } + } + + @Override + public long getBufferedPositionUs() { + if (loadingFinished) { + return TrackRenderer.END_OF_TRACK_US; + } else if (isPendingReset()) { + return pendingResetPositionUs; + } else { + long largestParsedTimestampUs = Long.MIN_VALUE; + for (int i = 0; i < sampleQueues.size(); i++) { + largestParsedTimestampUs = Math.max(largestParsedTimestampUs, + sampleQueues.valueAt(i).getLargestParsedTimestampUs()); + } + return largestParsedTimestampUs == Long.MIN_VALUE ? downstreamPositionUs + : largestParsedTimestampUs; + } + } + + @Override + public void release() { + Assertions.checkState(remainingReleaseCount > 0); + if (--remainingReleaseCount == 0) { + if (loader != null) { + loader.release(); + loader = null; + } + if (extractorHolder.extractor != null) { + extractorHolder.extractor.release(); + extractorHolder.extractor = null; + } + } + } + + // Loader.Callback implementation. + + @Override + public void onLoadCompleted(Loadable loadable) { + loadingFinished = true; + } + + @Override + public void onLoadCanceled(Loadable loadable) { + if (enabledTrackCount > 0) { + restartFrom(pendingResetPositionUs); + } else { + clearState(); + allocator.trim(0); + } + } + + @Override + public void onLoadError(Loadable ignored, IOException e) { + currentLoadableException = e; + currentLoadableExceptionCount = extractedSampleCount > extractedSampleCountAtStartOfLoad ? 1 + : currentLoadableExceptionCount + 1; + currentLoadableExceptionTimestamp = SystemClock.elapsedRealtime(); + notifyLoadError(e); + maybeStartLoading(); + } + + // ExtractorOutput implementation. + + @Override + public TrackOutput track(int id) { + InternalTrackOutput sampleQueue = sampleQueues.get(id); + if (sampleQueue == null) { + sampleQueue = new InternalTrackOutput(allocator); + sampleQueues.put(id, sampleQueue); + } + return sampleQueue; + } + + @Override + public void endTracks() { + tracksBuilt = true; + } + + @Override + public void seekMap(SeekMap seekMap) { + this.seekMap = seekMap; + } + + @Override + public void drmInitData(DrmInitData drmInitData) { + this.drmInitData = drmInitData; + } + + // Internal stuff. + + private void restartFrom(long positionUs) { + pendingResetPositionUs = positionUs; + loadingFinished = false; + if (loader.isLoading()) { + loader.cancelLoading(); + } else { + clearState(); + maybeStartLoading(); + } + } + + private void maybeStartLoading() { + if (loadingFinished || loader.isLoading()) { + return; + } + + if (currentLoadableException != null) { + if (isCurrentLoadableExceptionFatal()) { + return; + } + Assertions.checkState(loadable != null); + long elapsedMillis = SystemClock.elapsedRealtime() - currentLoadableExceptionTimestamp; + if (elapsedMillis >= getRetryDelayMillis(currentLoadableExceptionCount)) { + currentLoadableException = null; + if (!prepared) { + // We don't know whether we're playing an on-demand or a live stream. For a live stream + // we need to load from the start, as outlined below. Since we might be playing a live + // stream, play it safe and load from the start. + for (int i = 0; i < sampleQueues.size(); i++) { + sampleQueues.valueAt(i).clear(); + } + loadable = createLoadableFromStart(); + } else if (!seekMap.isSeekable() && maxTrackDurationUs == C.UNKNOWN_TIME_US) { + // We're playing a non-seekable stream with unknown duration. Assume it's live, and + // therefore that the data at the uri is a continuously shifting window of the latest + // available media. For this case there's no way to continue loading from where a previous + // load finished, so it's necessary to load from the start whenever commencing a new load. + for (int i = 0; i < sampleQueues.size(); i++) { + sampleQueues.valueAt(i).clear(); + } + loadable = createLoadableFromStart(); + // To avoid introducing a discontinuity, we shift the sample timestamps so that they will + // continue from the current downstream position. + pendingNextSampleUs = downstreamPositionUs; + havePendingNextSampleUs = true; + } else { + // We're playing a seekable on-demand stream. Resume the current loadable, which will + // request data starting from the point it left off. + } + extractedSampleCountAtStartOfLoad = extractedSampleCount; + loader.startLoading(loadable, this); + } + return; + } + + // We're not retrying, so we're either starting a playback or responding to an explicit seek. + // In both cases sampleTimeOffsetUs should be reset to zero, and any pending adjustment to + // sample timestamps should be discarded. + sampleTimeOffsetUs = 0; + havePendingNextSampleUs = false; + + if (!prepared) { + loadable = createLoadableFromStart(); + } else { + Assertions.checkState(isPendingReset()); + if (maxTrackDurationUs != C.UNKNOWN_TIME_US && pendingResetPositionUs >= maxTrackDurationUs) { + loadingFinished = true; + pendingResetPositionUs = NO_RESET_PENDING; + return; + } + loadable = createLoadableFromPositionUs(pendingResetPositionUs); + pendingResetPositionUs = NO_RESET_PENDING; + } + extractedSampleCountAtStartOfLoad = extractedSampleCount; + loader.startLoading(loadable, this); + } + + private ExtractingLoadable createLoadableFromStart() { + return new ExtractingLoadable(uri, dataSource, extractorHolder, allocator, requestedBufferSize, + 0); + } + + private ExtractingLoadable createLoadableFromPositionUs(long positionUs) { + return new ExtractingLoadable(uri, dataSource, extractorHolder, allocator, requestedBufferSize, + seekMap.getPosition(positionUs)); + } + + private boolean haveFormatsForAllTracks() { + for (int i = 0; i < sampleQueues.size(); i++) { + if (!sampleQueues.valueAt(i).hasFormat()) { + return false; + } + } + return true; + } + + private void discardSamplesForDisabledTracks(long timeUs) { + for (int i = 0; i < trackEnabledStates.length; i++) { + if (!trackEnabledStates[i]) { + sampleQueues.valueAt(i).discardUntil(timeUs); + } + } + } + + private void clearState() { + for (int i = 0; i < sampleQueues.size(); i++) { + sampleQueues.valueAt(i).clear(); + } + loadable = null; + currentLoadableException = null; + currentLoadableExceptionCount = 0; + } + + private boolean isPendingReset() { + return pendingResetPositionUs != NO_RESET_PENDING; + } + + private boolean isCurrentLoadableExceptionFatal() { + return currentLoadableException instanceof UnrecognizedInputFormatException; + } + + private long getRetryDelayMillis(long errorCount) { + return Math.min((errorCount - 1) * 1000, 5000); + } + + private void notifyLoadError(final IOException e) { + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onLoadError(eventSourceId, e); + } + }); + } + } + + /** + * Extension of {@link DefaultTrackOutput} that increments a shared counter of the total number + * of extracted samples. + */ + private class InternalTrackOutput extends DefaultTrackOutput { + + public InternalTrackOutput(Allocator allocator) { + super(allocator); + } + + @Override + public void sampleMetadata(long timeUs, int flags, int size, int offset, byte[] encryptionKey) { + super.sampleMetadata(timeUs, flags, size, offset, encryptionKey); + extractedSampleCount++; + } + + } + + /** + * Loads the media stream and extracts sample data from it. + */ + private static class ExtractingLoadable implements Loadable { + + private final Uri uri; + private final DataSource dataSource; + private final ExtractorHolder extractorHolder; + private final Allocator allocator; + private final int requestedBufferSize; + private final PositionHolder positionHolder; + + private volatile boolean loadCanceled; + + private boolean pendingExtractorSeek; + + public ExtractingLoadable(Uri uri, DataSource dataSource, ExtractorHolder extractorHolder, + Allocator allocator, int requestedBufferSize, long position) { + this.uri = Assertions.checkNotNull(uri); + this.dataSource = Assertions.checkNotNull(dataSource); + this.extractorHolder = Assertions.checkNotNull(extractorHolder); + this.allocator = Assertions.checkNotNull(allocator); + this.requestedBufferSize = requestedBufferSize; + positionHolder = new PositionHolder(); + positionHolder.position = position; + pendingExtractorSeek = true; + } + + @Override + public void cancelLoad() { + loadCanceled = true; + } + + @Override + public boolean isLoadCanceled() { + return loadCanceled; + } + + @Override + public void load() throws IOException, InterruptedException { + int result = Extractor.RESULT_CONTINUE; + while (result == Extractor.RESULT_CONTINUE && !loadCanceled) { + ExtractorInput input = null; + try { + long position = positionHolder.position; + long length = dataSource.open(new DataSpec(uri, position, C.LENGTH_UNBOUNDED, null)); + if (length != C.LENGTH_UNBOUNDED) { + length += position; + } + input = new DefaultExtractorInput(dataSource, position, length); + Extractor extractor = extractorHolder.selectExtractor(input); + if (pendingExtractorSeek) { + extractor.seek(); + pendingExtractorSeek = false; + } + while (result == Extractor.RESULT_CONTINUE && !loadCanceled) { + allocator.blockWhileTotalBytesAllocatedExceeds(requestedBufferSize); + result = extractor.read(input, positionHolder); + // TODO: Implement throttling to stop us from buffering data too often. + } + } finally { + if (result == Extractor.RESULT_SEEK) { + result = Extractor.RESULT_CONTINUE; + } else if (input != null) { + positionHolder.position = input.getPosition(); + } + dataSource.close(); + } + } + } + + } + + /** + * Stores a list of extractors and a selected extractor when the format has been detected. + */ + private static final class ExtractorHolder { + + private final Extractor[] extractors; + private final ExtractorOutput extractorOutput; + private Extractor extractor; + + /** + * Creates a holder that will select an extractor and initialize it using the specified output. + * + * @param extractors One or more extractors to choose from. + * @param extractorOutput The output that will be used to initialize the selected extractor. + */ + public ExtractorHolder(Extractor[] extractors, ExtractorOutput extractorOutput) { + this.extractors = extractors; + this.extractorOutput = extractorOutput; + } + + /** + * Returns an initialized extractor for reading {@code input}, and returns the same extractor on + * later calls. + * + * @param input The {@link ExtractorInput} from which data should be read. + * @throws UnrecognizedInputFormatException Thrown if the input format could not be detected. + * @throws IOException Thrown if the input could not be read. + * @throws InterruptedException Thrown if the thread was interrupted. + */ + public Extractor selectExtractor(ExtractorInput input) + throws UnrecognizedInputFormatException, IOException, InterruptedException { + if (extractor != null) { + return extractor; + } + for (Extractor extractor : extractors) { + try { + if (extractor.sniff(input)) { + this.extractor = extractor; + break; + } + } catch (EOFException e) { + // Do nothing. + } finally { + input.resetPeekPosition(); + } + } + if (extractor == null) { + throw new UnrecognizedInputFormatException(extractors); + } + extractor.init(extractorOutput); + return extractor; + } + + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/GaplessInfo.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/GaplessInfo.java new file mode 100755 index 000000000..70025ebd3 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/GaplessInfo.java @@ -0,0 +1,88 @@ +/* + * 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.messenger.exoplayer.extractor; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Utility for parsing and representing gapless playback information. + */ +public final class GaplessInfo { + + private static final String GAPLESS_COMMENT_ID = "iTunSMPB"; + private static final Pattern GAPLESS_COMMENT_PATTERN = + Pattern.compile("^ [0-9a-fA-F]{8} ([0-9a-fA-F]{8}) ([0-9a-fA-F]{8})"); + + /** + * Parses a gapless playback comment (stored in an ID3 header or MPEG 4 user data). + * + * @param name The comment's identifier. + * @param data The comment's payload data. + * @return Parsed gapless playback information, if present and non-zero. {@code null} otherwise. + */ + public static GaplessInfo createFromComment(String name, String data) { + if (!GAPLESS_COMMENT_ID.equals(name)) { + return null; + } + Matcher matcher = GAPLESS_COMMENT_PATTERN.matcher(data); + if (matcher.find()) { + try { + int encoderDelay = Integer.parseInt(matcher.group(1), 16); + int encoderPadding = Integer.parseInt(matcher.group(2), 16); + return encoderDelay == 0 && encoderPadding == 0 ? null + : new GaplessInfo(encoderDelay, encoderPadding); + } catch (NumberFormatException e) { + // Ignore incorrectly formatted comments. + } + } + return null; + } + + /** + * Parses gapless playback information associated with an MP3 Xing header. + * + * @param value The 24-bit value to parse. + * @return Parsed gapless playback information, if non-zero. {@code null} otherwise. + */ + public static GaplessInfo createFromXingHeaderValue(int value) { + int encoderDelay = value >> 12; + int encoderPadding = value & 0x0FFF; + return encoderDelay == 0 && encoderPadding == 0 ? null + : new GaplessInfo(encoderDelay, encoderPadding); + } + + /** + * The number of samples to trim from the start of the decoded audio stream. + */ + public final int encoderDelay; + /** + * The number of samples to trim from the end of the decoded audio stream. + */ + public final int encoderPadding; + + /** + * Creates a new {@link GaplessInfo} with the specified encoder delay and padding. + * + * @param encoderDelay The encoder delay. + * @param encoderPadding The encoder padding. + */ + private GaplessInfo(int encoderDelay, int encoderPadding) { + this.encoderDelay = encoderDelay; + this.encoderPadding = encoderPadding; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/NoSuchPropertyException.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/PositionHolder.java similarity index 69% rename from TMessagesProj/src/main/java/org/telegram/messenger/Animation/NoSuchPropertyException.java rename to TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/PositionHolder.java index 114660287..e34b7d77e 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/NoSuchPropertyException.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/PositionHolder.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011 The Android Open Source Project + * 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. @@ -13,12 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.telegram.messenger.Animation; +package org.telegram.messenger.exoplayer.extractor; -public class NoSuchPropertyException extends RuntimeException { +/** + * Holds a position in the stream. + */ +public final class PositionHolder { - public NoSuchPropertyException(String s) { - super(s); - } + /** + * The held position. + */ + public long position; } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/RollingSampleBuffer.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/RollingSampleBuffer.java new file mode 100755 index 000000000..26e4b1ae8 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/RollingSampleBuffer.java @@ -0,0 +1,673 @@ +/* + * 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.messenger.exoplayer.extractor; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.SampleHolder; +import org.telegram.messenger.exoplayer.upstream.Allocation; +import org.telegram.messenger.exoplayer.upstream.Allocator; +import org.telegram.messenger.exoplayer.upstream.DataSource; +import org.telegram.messenger.exoplayer.util.Assertions; +import org.telegram.messenger.exoplayer.util.ParsableByteArray; + +import java.io.EOFException; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.concurrent.LinkedBlockingDeque; + +/** + * A rolling buffer of sample data and corresponding sample information. + */ +/* package */ final class RollingSampleBuffer { + + private static final int INITIAL_SCRATCH_SIZE = 32; + + private final Allocator allocator; + private final int allocationLength; + + private final InfoQueue infoQueue; + private final LinkedBlockingDeque dataQueue; + private final SampleExtrasHolder extrasHolder; + private final ParsableByteArray scratch; + + // Accessed only by the consuming thread. + private long totalBytesDropped; + + // Accessed only by the loading thread. + private long totalBytesWritten; + private Allocation lastAllocation; + private int lastAllocationOffset; + + /** + * @param allocator An {@link Allocator} from which allocations for sample data can be obtained. + */ + public RollingSampleBuffer(Allocator allocator) { + this.allocator = allocator; + allocationLength = allocator.getIndividualAllocationLength(); + infoQueue = new InfoQueue(); + dataQueue = new LinkedBlockingDeque<>(); + extrasHolder = new SampleExtrasHolder(); + scratch = new ParsableByteArray(INITIAL_SCRATCH_SIZE); + lastAllocationOffset = allocationLength; + } + + // Called by the consuming thread, but only when there is no loading thread. + + /** + * Clears the buffer, returning all allocations to the allocator. + */ + public void clear() { + infoQueue.clear(); + while (!dataQueue.isEmpty()) { + allocator.release(dataQueue.remove()); + } + totalBytesDropped = 0; + totalBytesWritten = 0; + lastAllocation = null; + lastAllocationOffset = allocationLength; + } + + /** + * Returns the current absolute write index. + */ + public int getWriteIndex() { + return infoQueue.getWriteIndex(); + } + + /** + * Discards samples from the write side of the buffer. + * + * @param discardFromIndex The absolute index of the first sample to be discarded. + */ + public void discardUpstreamSamples(int discardFromIndex) { + totalBytesWritten = infoQueue.discardUpstreamSamples(discardFromIndex); + dropUpstreamFrom(totalBytesWritten); + } + + /** + * Discards data from the write side of the buffer. Data is discarded from the specified absolute + * position. Any allocations that are fully discarded are returned to the allocator. + * + * @param absolutePosition The absolute position (inclusive) from which to discard data. + */ + private void dropUpstreamFrom(long absolutePosition) { + int relativePosition = (int) (absolutePosition - totalBytesDropped); + // Calculate the index of the allocation containing the position, and the offset within it. + int allocationIndex = relativePosition / allocationLength; + int allocationOffset = relativePosition % allocationLength; + // We want to discard any allocations after the one at allocationIdnex. + int allocationDiscardCount = dataQueue.size() - allocationIndex - 1; + if (allocationOffset == 0) { + // If the allocation at allocationIndex is empty, we should discard that one too. + allocationDiscardCount++; + } + // Discard the allocations. + for (int i = 0; i < allocationDiscardCount; i++) { + allocator.release(dataQueue.removeLast()); + } + // Update lastAllocation and lastAllocationOffset to reflect the new position. + lastAllocation = dataQueue.peekLast(); + lastAllocationOffset = allocationOffset == 0 ? allocationLength : allocationOffset; + } + + // Called by the consuming thread. + + /** + * Returns the current absolute read index. + */ + public int getReadIndex() { + return infoQueue.getReadIndex(); + } + + /** + * Fills {@code holder} with information about the current sample, but does not write its data. + *

+ * The fields set are {@link SampleHolder#size}, {@link SampleHolder#timeUs} and + * {@link SampleHolder#flags}. + * + * @param holder The holder into which the current sample information should be written. + * @return True if the holder was filled. False if there is no current sample. + */ + public boolean peekSample(SampleHolder holder) { + return infoQueue.peekSample(holder, extrasHolder); + } + + /** + * Skips the current sample. + */ + public void skipSample() { + long nextOffset = infoQueue.moveToNextSample(); + dropDownstreamTo(nextOffset); + } + + /** + * Attempts to skip to the keyframe before the specified time, if it's present in the buffer. + * + * @param timeUs The seek time. + * @return True if the skip was successful. False otherwise. + */ + public boolean skipToKeyframeBefore(long timeUs) { + long nextOffset = infoQueue.skipToKeyframeBefore(timeUs); + if (nextOffset == -1) { + return false; + } + dropDownstreamTo(nextOffset); + return true; + } + + /** + * Reads the current sample, advancing the read index to the next sample. + * + * @param sampleHolder The holder into which the current sample should be written. + * @return True if a sample was read. False if there is no current sample. + */ + public boolean readSample(SampleHolder sampleHolder) { + // Write the sample information into the holder and extrasHolder. + boolean haveSample = infoQueue.peekSample(sampleHolder, extrasHolder); + if (!haveSample) { + return false; + } + + // Read encryption data if the sample is encrypted. + if (sampleHolder.isEncrypted()) { + readEncryptionData(sampleHolder, extrasHolder); + } + // Write the sample data into the holder. + sampleHolder.ensureSpaceForWrite(sampleHolder.size); + readData(extrasHolder.offset, sampleHolder.data, sampleHolder.size); + // Advance the read head. + long nextOffset = infoQueue.moveToNextSample(); + dropDownstreamTo(nextOffset); + return true; + } + + /** + * Reads encryption data for the current sample. + *

+ * The encryption data is written into {@code sampleHolder.cryptoInfo}, and + * {@code sampleHolder.size} is adjusted to subtract the number of bytes that were read. The + * same value is added to {@code extrasHolder.offset}. + * + * @param sampleHolder The holder into which the encryption data should be written. + * @param extrasHolder The extras holder whose offset should be read and subsequently adjusted. + */ + private void readEncryptionData(SampleHolder sampleHolder, SampleExtrasHolder extrasHolder) { + long offset = extrasHolder.offset; + + // Read the signal byte. + readData(offset, scratch.data, 1); + offset++; + byte signalByte = scratch.data[0]; + boolean subsampleEncryption = (signalByte & 0x80) != 0; + int ivSize = signalByte & 0x7F; + + // Read the initialization vector. + if (sampleHolder.cryptoInfo.iv == null) { + sampleHolder.cryptoInfo.iv = new byte[16]; + } + readData(offset, sampleHolder.cryptoInfo.iv, ivSize); + offset += ivSize; + + // Read the subsample count, if present. + int subsampleCount; + if (subsampleEncryption) { + readData(offset, scratch.data, 2); + offset += 2; + scratch.setPosition(0); + subsampleCount = scratch.readUnsignedShort(); + } else { + subsampleCount = 1; + } + + // Write the clear and encrypted subsample sizes. + int[] clearDataSizes = sampleHolder.cryptoInfo.numBytesOfClearData; + if (clearDataSizes == null || clearDataSizes.length < subsampleCount) { + clearDataSizes = new int[subsampleCount]; + } + int[] encryptedDataSizes = sampleHolder.cryptoInfo.numBytesOfEncryptedData; + if (encryptedDataSizes == null || encryptedDataSizes.length < subsampleCount) { + encryptedDataSizes = new int[subsampleCount]; + } + if (subsampleEncryption) { + int subsampleDataLength = 6 * subsampleCount; + ensureCapacity(scratch, subsampleDataLength); + readData(offset, scratch.data, subsampleDataLength); + offset += subsampleDataLength; + scratch.setPosition(0); + for (int i = 0; i < subsampleCount; i++) { + clearDataSizes[i] = scratch.readUnsignedShort(); + encryptedDataSizes[i] = scratch.readUnsignedIntToInt(); + } + } else { + clearDataSizes[0] = 0; + encryptedDataSizes[0] = sampleHolder.size - (int) (offset - extrasHolder.offset); + } + + // Populate the cryptoInfo. + sampleHolder.cryptoInfo.set(subsampleCount, clearDataSizes, encryptedDataSizes, + extrasHolder.encryptionKeyId, sampleHolder.cryptoInfo.iv, C.CRYPTO_MODE_AES_CTR); + + // Adjust the offset and size to take into account the bytes read. + int bytesRead = (int) (offset - extrasHolder.offset); + extrasHolder.offset += bytesRead; + sampleHolder.size -= bytesRead; + } + + /** + * Reads data from the front of the rolling buffer. + * + * @param absolutePosition The absolute position from which data should be read. + * @param target The buffer into which data should be written. + * @param length The number of bytes to read. + */ + private void readData(long absolutePosition, ByteBuffer target, int length) { + int remaining = length; + while (remaining > 0) { + dropDownstreamTo(absolutePosition); + int positionInAllocation = (int) (absolutePosition - totalBytesDropped); + int toCopy = Math.min(remaining, allocationLength - positionInAllocation); + Allocation allocation = dataQueue.peek(); + target.put(allocation.data, allocation.translateOffset(positionInAllocation), toCopy); + absolutePosition += toCopy; + remaining -= toCopy; + } + } + + /** + * Reads data from the front of the rolling buffer. + * + * @param absolutePosition The absolute position from which data should be read. + * @param target The array into which data should be written. + * @param length The number of bytes to read. + */ + // TODO: Consider reducing duplication of this method and the one above. + private void readData(long absolutePosition, byte[] target, int length) { + int bytesRead = 0; + while (bytesRead < length) { + dropDownstreamTo(absolutePosition); + int positionInAllocation = (int) (absolutePosition - totalBytesDropped); + int toCopy = Math.min(length - bytesRead, allocationLength - positionInAllocation); + Allocation allocation = dataQueue.peek(); + System.arraycopy(allocation.data, allocation.translateOffset(positionInAllocation), target, + bytesRead, toCopy); + absolutePosition += toCopy; + bytesRead += toCopy; + } + } + + /** + * Discard any allocations that hold data prior to the specified absolute position, returning + * them to the allocator. + * + * @param absolutePosition The absolute position up to which allocations can be discarded. + */ + private void dropDownstreamTo(long absolutePosition) { + int relativePosition = (int) (absolutePosition - totalBytesDropped); + int allocationIndex = relativePosition / allocationLength; + for (int i = 0; i < allocationIndex; i++) { + allocator.release(dataQueue.remove()); + totalBytesDropped += allocationLength; + } + } + + /** + * Ensure that the passed {@link ParsableByteArray} is of at least the specified limit. + */ + private static void ensureCapacity(ParsableByteArray byteArray, int limit) { + if (byteArray.limit() < limit) { + byteArray.reset(new byte[limit], limit); + } + } + + // Called by the loading thread. + + /** + * Returns the current write position in the rolling buffer. + * + * @return The current write position. + */ + public long getWritePosition() { + return totalBytesWritten; + } + + /** + * Appends data to the rolling buffer. + * + * @param dataSource The source from which to read. + * @param length The maximum length of the read. + * @param allowEndOfInput True if encountering the end of the input having appended no data is + * allowed, and should result in {@link C#RESULT_END_OF_INPUT} being returned. False if it + * should be considered an error, causing an {@link EOFException} to be thrown. + * @return The number of bytes appended, or {@link C#RESULT_END_OF_INPUT} if the input has ended. + * @throws IOException If an error occurs reading from the source. + */ + public int appendData(DataSource dataSource, int length, boolean allowEndOfInput) + throws IOException { + length = prepareForAppend(length); + int bytesAppended = dataSource.read(lastAllocation.data, + lastAllocation.translateOffset(lastAllocationOffset), length); + if (bytesAppended == C.RESULT_END_OF_INPUT) { + if (allowEndOfInput) { + return C.RESULT_END_OF_INPUT; + } + throw new EOFException(); + } + lastAllocationOffset += bytesAppended; + totalBytesWritten += bytesAppended; + return bytesAppended; + } + + /** + * Appends data to the rolling buffer. + * + * @param input The source from which to read. + * @param length The maximum length of the read. + * @param allowEndOfInput True if encountering the end of the input having appended no data is + * allowed, and should result in {@link C#RESULT_END_OF_INPUT} being returned. False if it + * should be considered an error, causing an {@link EOFException} to be thrown. + * @return The number of bytes appended, or {@link C#RESULT_END_OF_INPUT} if the input has ended. + * @throws IOException If an error occurs reading from the source. + * @throws InterruptedException If the thread has been interrupted. + */ + public int appendData(ExtractorInput input, int length, boolean allowEndOfInput) + throws IOException, InterruptedException { + length = prepareForAppend(length); + int bytesAppended = input.read(lastAllocation.data, + lastAllocation.translateOffset(lastAllocationOffset), length); + if (bytesAppended == C.RESULT_END_OF_INPUT) { + if (allowEndOfInput) { + return C.RESULT_END_OF_INPUT; + } + throw new EOFException(); + } + lastAllocationOffset += bytesAppended; + totalBytesWritten += bytesAppended; + return bytesAppended; + } + + /** + * Appends data to the rolling buffer. + * + * @param buffer A buffer containing the data to append. + * @param length The length of the data to append. + */ + public void appendData(ParsableByteArray buffer, int length) { + while (length > 0) { + int thisAppendLength = prepareForAppend(length); + buffer.readBytes(lastAllocation.data, lastAllocation.translateOffset(lastAllocationOffset), + thisAppendLength); + lastAllocationOffset += thisAppendLength; + totalBytesWritten += thisAppendLength; + length -= thisAppendLength; + } + } + + /** + * Indicates the end point for the current sample, making it available for consumption. + * + * @param sampleTimeUs The sample timestamp. + * @param flags Flags that accompany the sample. See {@link SampleHolder#flags}. + * @param position The position of the sample data in the rolling buffer. + * @param size The size of the sample, in bytes. + * @param encryptionKey The encryption key associated with the sample, or null. + */ + public void commitSample(long sampleTimeUs, int flags, long position, int size, + byte[] encryptionKey) { + infoQueue.commitSample(sampleTimeUs, flags, position, size, encryptionKey); + } + + /** + * Prepares the rolling sample buffer for an append of up to {@code length} bytes, returning the + * number of bytes that can actually be appended. + */ + private int prepareForAppend(int length) { + if (lastAllocationOffset == allocationLength) { + lastAllocationOffset = 0; + lastAllocation = allocator.allocate(); + dataQueue.add(lastAllocation); + } + return Math.min(length, allocationLength - lastAllocationOffset); + } + + /** + * Holds information about the samples in the rolling buffer. + */ + private static final class InfoQueue { + + private static final int SAMPLE_CAPACITY_INCREMENT = 1000; + + private int capacity; + + private long[] offsets; + private int[] sizes; + private int[] flags; + private long[] timesUs; + private byte[][] encryptionKeys; + + private int queueSize; + private int absoluteReadIndex; + private int relativeReadIndex; + private int relativeWriteIndex; + + public InfoQueue() { + capacity = SAMPLE_CAPACITY_INCREMENT; + offsets = new long[capacity]; + timesUs = new long[capacity]; + flags = new int[capacity]; + sizes = new int[capacity]; + encryptionKeys = new byte[capacity][]; + } + + // Called by the consuming thread, but only when there is no loading thread. + + /** + * Clears the queue. + */ + public void clear() { + absoluteReadIndex = 0; + relativeReadIndex = 0; + relativeWriteIndex = 0; + queueSize = 0; + } + + /** + * Returns the current absolute write index. + */ + public int getWriteIndex() { + return absoluteReadIndex + queueSize; + } + + /** + * Discards samples from the write side of the buffer. + * + * @param discardFromIndex The absolute index of the first sample to be discarded. + * @return The reduced total number of bytes written, after the samples have been discarded. + */ + public long discardUpstreamSamples(int discardFromIndex) { + int discardCount = getWriteIndex() - discardFromIndex; + Assertions.checkArgument(0 <= discardCount && discardCount <= queueSize); + + if (discardCount == 0) { + if (absoluteReadIndex == 0) { + // queueSize == absoluteReadIndex == 0, so nothing has been written to the queue. + return 0; + } + int lastWriteIndex = (relativeWriteIndex == 0 ? capacity : relativeWriteIndex) - 1; + return offsets[lastWriteIndex] + sizes[lastWriteIndex]; + } + + queueSize -= discardCount; + relativeWriteIndex = (relativeWriteIndex + capacity - discardCount) % capacity; + return offsets[relativeWriteIndex]; + } + + // Called by the consuming thread. + + /** + * Returns the current absolute read index. + */ + public int getReadIndex() { + return absoluteReadIndex; + } + + /** + * Fills {@code holder} with information about the current sample, but does not write its data. + * The first entry in {@code offsetHolder} is filled with the absolute position of the sample's + * data in the rolling buffer. + *

+ * Populates {@link SampleHolder#size}, {@link SampleHolder#timeUs}, {@link SampleHolder#flags} + * and the {@code extrasHolder}. + * + * @param holder The holder into which the current sample information should be written. + * @param extrasHolder The holder into which extra sample information should be written. + * @return True if the holders were filled. False if there is no current sample. + */ + public synchronized boolean peekSample(SampleHolder holder, SampleExtrasHolder extrasHolder) { + if (queueSize == 0) { + return false; + } + holder.timeUs = timesUs[relativeReadIndex]; + holder.size = sizes[relativeReadIndex]; + holder.flags = flags[relativeReadIndex]; + extrasHolder.offset = offsets[relativeReadIndex]; + extrasHolder.encryptionKeyId = encryptionKeys[relativeReadIndex]; + return true; + } + + /** + * Advances the read index to the next sample. + * + * @return The absolute position of the first byte in the rolling buffer that may still be + * required after advancing the index. Data prior to this position can be dropped. + */ + public synchronized long moveToNextSample() { + queueSize--; + int lastReadIndex = relativeReadIndex++; + absoluteReadIndex++; + if (relativeReadIndex == capacity) { + // Wrap around. + relativeReadIndex = 0; + } + return queueSize > 0 ? offsets[relativeReadIndex] + : (sizes[lastReadIndex] + offsets[lastReadIndex]); + } + + /** + * Attempts to locate the keyframe before the specified time, if it's present in the buffer. + * + * @param timeUs The seek time. + * @return The offset of the keyframe's data if the keyframe was present. -1 otherwise. + */ + public synchronized long skipToKeyframeBefore(long timeUs) { + if (queueSize == 0 || timeUs < timesUs[relativeReadIndex]) { + return -1; + } + + int lastWriteIndex = (relativeWriteIndex == 0 ? capacity : relativeWriteIndex) - 1; + long lastTimeUs = timesUs[lastWriteIndex]; + if (timeUs > lastTimeUs) { + return -1; + } + + // TODO: This can be optimized further using binary search, although the fact that the array + // is cyclic means we'd need to implement the binary search ourselves. + int sampleCount = 0; + int sampleCountToKeyframe = -1; + int searchIndex = relativeReadIndex; + while (searchIndex != relativeWriteIndex) { + if (timesUs[searchIndex] > timeUs) { + // We've gone too far. + break; + } else if ((flags[searchIndex] & C.SAMPLE_FLAG_SYNC) != 0) { + // We've found a keyframe, and we're still before the seek position. + sampleCountToKeyframe = sampleCount; + } + searchIndex = (searchIndex + 1) % capacity; + sampleCount++; + } + + if (sampleCountToKeyframe == -1) { + return -1; + } + + queueSize -= sampleCountToKeyframe; + relativeReadIndex = (relativeReadIndex + sampleCountToKeyframe) % capacity; + absoluteReadIndex += sampleCountToKeyframe; + return offsets[relativeReadIndex]; + } + + // Called by the loading thread. + + public synchronized void commitSample(long timeUs, int sampleFlags, long offset, int size, + byte[] encryptionKey) { + timesUs[relativeWriteIndex] = timeUs; + offsets[relativeWriteIndex] = offset; + sizes[relativeWriteIndex] = size; + flags[relativeWriteIndex] = sampleFlags; + encryptionKeys[relativeWriteIndex] = encryptionKey; + // Increment the write index. + queueSize++; + if (queueSize == capacity) { + // Increase the capacity. + int newCapacity = capacity + SAMPLE_CAPACITY_INCREMENT; + long[] newOffsets = new long[newCapacity]; + long[] newTimesUs = new long[newCapacity]; + int[] newFlags = new int[newCapacity]; + int[] newSizes = new int[newCapacity]; + byte[][] newEncryptionKeys = new byte[newCapacity][]; + int beforeWrap = capacity - relativeReadIndex; + System.arraycopy(offsets, relativeReadIndex, newOffsets, 0, beforeWrap); + System.arraycopy(timesUs, relativeReadIndex, newTimesUs, 0, beforeWrap); + System.arraycopy(flags, relativeReadIndex, newFlags, 0, beforeWrap); + System.arraycopy(sizes, relativeReadIndex, newSizes, 0, beforeWrap); + System.arraycopy(encryptionKeys, relativeReadIndex, newEncryptionKeys, 0, beforeWrap); + int afterWrap = relativeReadIndex; + System.arraycopy(offsets, 0, newOffsets, beforeWrap, afterWrap); + System.arraycopy(timesUs, 0, newTimesUs, beforeWrap, afterWrap); + System.arraycopy(flags, 0, newFlags, beforeWrap, afterWrap); + System.arraycopy(sizes, 0, newSizes, beforeWrap, afterWrap); + System.arraycopy(encryptionKeys, 0, newEncryptionKeys, beforeWrap, afterWrap); + offsets = newOffsets; + timesUs = newTimesUs; + flags = newFlags; + sizes = newSizes; + encryptionKeys = newEncryptionKeys; + relativeReadIndex = 0; + relativeWriteIndex = capacity; + queueSize = capacity; + capacity = newCapacity; + } else { + relativeWriteIndex++; + if (relativeWriteIndex == capacity) { + // Wrap around. + relativeWriteIndex = 0; + } + } + } + + } + + /** + * Holds additional sample information not held by {@link SampleHolder}. + */ + private static final class SampleExtrasHolder { + + public long offset; + public byte[] encryptionKeyId; + + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/SeekMap.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/SeekMap.java new file mode 100755 index 000000000..3a0df3cc0 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/SeekMap.java @@ -0,0 +1,60 @@ +/* + * 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.messenger.exoplayer.extractor; + +/** + * Maps seek positions (in microseconds) to corresponding positions (byte offsets) in the stream. + */ +public interface SeekMap { + + /** + * A {@link SeekMap} that does not support seeking. + */ + public static final SeekMap UNSEEKABLE = new SeekMap() { + + @Override + public boolean isSeekable() { + return false; + } + + @Override + public long getPosition(long timeUs) { + return 0; + } + + }; + + /** + * Whether or not the seeking is supported. + *

+ * If seeking is not supported then the only valid seek position is the start of the file, and so + * {@link #getPosition(long)} will return 0 for all input values. + * + * @return True if seeking is supported. False otherwise. + */ + boolean isSeekable(); + + /** + * Maps a seek position in microseconds to a corresponding position (byte offset) in the stream + * from which data can be provided to the extractor. + * + * @param timeUs A seek position in microseconds. + * @return The corresponding position (byte offset) in the stream from which data can be provided + * to the extractor, or 0 if {@code #isSeekable()} returns false. + */ + long getPosition(long timeUs); + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/TrackOutput.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/TrackOutput.java new file mode 100755 index 000000000..893dd28c6 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/TrackOutput.java @@ -0,0 +1,79 @@ +/* + * 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.messenger.exoplayer.extractor; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.MediaFormat; +import org.telegram.messenger.exoplayer.SampleHolder; +import org.telegram.messenger.exoplayer.util.ParsableByteArray; + +import java.io.EOFException; +import java.io.IOException; + +/** + * Receives track level data extracted by an {@link Extractor}. + */ +public interface TrackOutput { + + /** + * Invoked when the {@link MediaFormat} of the track has been extracted from the stream. + * + * @param format The extracted {@link MediaFormat}. + */ + void format(MediaFormat format); + + /** + * Invoked to write sample data to the output. + * + * @param input An {@link ExtractorInput} from which to read the sample data. + * @param length The maximum length to read from the input. + * @param allowEndOfInput True if encountering the end of the input having read no data is + * allowed, and should result in {@link C#RESULT_END_OF_INPUT} being returned. False if it + * should be considered an error, causing an {@link EOFException} to be thrown. + * @return The number of bytes appended. + * @throws IOException If an error occurred reading from the input. + * @throws InterruptedException If the thread was interrupted. + */ + int sampleData(ExtractorInput input, int length, boolean allowEndOfInput) + throws IOException, InterruptedException; + + /** + * Invoked to write sample data to the output. + * + * @param data A {@link ParsableByteArray} from which to read the sample data. + * @param length The number of bytes to read. + */ + void sampleData(ParsableByteArray data, int length); + + /** + * Invoked when metadata associated with a sample has been extracted from the stream. + *

+ * The corresponding sample data will have already been passed to the output via calls to + * {@link #sampleData(ExtractorInput, int, boolean)} or + * {@link #sampleData(ParsableByteArray, int)}. + * + * @param timeUs The media timestamp associated with the sample, in microseconds. + * @param flags Flags associated with the sample. See {@link SampleHolder#flags}. + * @param size The size of the sample data, in bytes. + * @param offset The number of bytes that have been passed to + * {@link #sampleData(ExtractorInput, int, boolean)} or + * {@link #sampleData(ParsableByteArray, int)} since the last byte belonging to the sample + * whose metadata is being passed. + * @param encryptionKey The encryption key associated with the sample. May be null. + */ + void sampleMetadata(long timeUs, int flags, int size, int offset, byte[] encryptionKey); + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/flv/AudioTagPayloadReader.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/flv/AudioTagPayloadReader.java new file mode 100755 index 000000000..3c8893d08 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/flv/AudioTagPayloadReader.java @@ -0,0 +1,102 @@ +/* + * 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.messenger.exoplayer.extractor.flv; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.MediaFormat; +import org.telegram.messenger.exoplayer.extractor.TrackOutput; +import org.telegram.messenger.exoplayer.util.CodecSpecificDataUtil; +import org.telegram.messenger.exoplayer.util.MimeTypes; +import org.telegram.messenger.exoplayer.util.ParsableByteArray; + +import android.util.Pair; + +import java.util.Collections; + +/** + * Parses audio tags from an FLV stream and extracts AAC frames. + */ +/* package */ final class AudioTagPayloadReader extends TagPayloadReader { + + // Audio format + private static final int AUDIO_FORMAT_AAC = 10; + + // AAC PACKET TYPE + private static final int AAC_PACKET_TYPE_SEQUENCE_HEADER = 0; + private static final int AAC_PACKET_TYPE_AAC_RAW = 1; + + // SAMPLING RATES + private static final int[] AUDIO_SAMPLING_RATE_TABLE = new int[] { + 5500, 11000, 22000, 44000 + }; + + // State variables + private boolean hasParsedAudioDataHeader; + private boolean hasOutputFormat; + + public AudioTagPayloadReader(TrackOutput output) { + super(output); + } + + @Override + public void seek() { + // Do nothing. + } + + @Override + protected boolean parseHeader(ParsableByteArray data) throws UnsupportedFormatException { + if (!hasParsedAudioDataHeader) { + int header = data.readUnsignedByte(); + int audioFormat = (header >> 4) & 0x0F; + int sampleRateIndex = (header >> 2) & 0x03; + if (sampleRateIndex < 0 || sampleRateIndex >= AUDIO_SAMPLING_RATE_TABLE.length) { + throw new UnsupportedFormatException("Invalid sample rate index: " + sampleRateIndex); + } + // TODO: Add support for MP3 and PCM. + if (audioFormat != AUDIO_FORMAT_AAC) { + throw new UnsupportedFormatException("Audio format not supported: " + audioFormat); + } + hasParsedAudioDataHeader = true; + } else { + // Skip header if it was parsed previously. + data.skipBytes(1); + } + return true; + } + + @Override + protected void parsePayload(ParsableByteArray data, long timeUs) { + int packetType = data.readUnsignedByte(); + // Parse sequence header just in case it was not done before. + if (packetType == AAC_PACKET_TYPE_SEQUENCE_HEADER && !hasOutputFormat) { + byte[] audioSpecifiConfig = new byte[data.bytesLeft()]; + data.readBytes(audioSpecifiConfig, 0, audioSpecifiConfig.length); + Pair audioParams = CodecSpecificDataUtil.parseAacAudioSpecificConfig( + audioSpecifiConfig); + MediaFormat mediaFormat = MediaFormat.createAudioFormat(null, MimeTypes.AUDIO_AAC, + MediaFormat.NO_VALUE, MediaFormat.NO_VALUE, getDurationUs(), audioParams.second, + audioParams.first, Collections.singletonList(audioSpecifiConfig), null); + output.format(mediaFormat); + hasOutputFormat = true; + } else if (packetType == AAC_PACKET_TYPE_AAC_RAW) { + // Sample audio AAC frames + int bytesToWrite = data.bytesLeft(); + output.sampleData(data, bytesToWrite); + output.sampleMetadata(timeUs, C.SAMPLE_FLAG_SYNC, bytesToWrite, 0, null); + } + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/flv/FlvExtractor.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/flv/FlvExtractor.java new file mode 100755 index 000000000..d7f77e9df --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/flv/FlvExtractor.java @@ -0,0 +1,286 @@ +/* + * 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.messenger.exoplayer.extractor.flv; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.extractor.Extractor; +import org.telegram.messenger.exoplayer.extractor.ExtractorInput; +import org.telegram.messenger.exoplayer.extractor.ExtractorOutput; +import org.telegram.messenger.exoplayer.extractor.PositionHolder; +import org.telegram.messenger.exoplayer.extractor.SeekMap; +import org.telegram.messenger.exoplayer.util.ParsableByteArray; +import org.telegram.messenger.exoplayer.util.Util; + +import java.io.IOException; + +/** + * Facilitates the extraction of data from the FLV container format. + */ +public final class FlvExtractor implements Extractor, SeekMap { + + // Header sizes. + private static final int FLV_HEADER_SIZE = 9; + private static final int FLV_TAG_HEADER_SIZE = 11; + + // Parser states. + private static final int STATE_READING_FLV_HEADER = 1; + private static final int STATE_SKIPPING_TO_TAG_HEADER = 2; + private static final int STATE_READING_TAG_HEADER = 3; + private static final int STATE_READING_TAG_DATA = 4; + + // Tag types. + private static final int TAG_TYPE_AUDIO = 8; + private static final int TAG_TYPE_VIDEO = 9; + private static final int TAG_TYPE_SCRIPT_DATA = 18; + + // FLV container identifier. + private static final int FLV_TAG = Util.getIntegerCodeForString("FLV"); + + // Temporary buffers. + private final ParsableByteArray scratch; + private final ParsableByteArray headerBuffer; + private final ParsableByteArray tagHeaderBuffer; + private final ParsableByteArray tagData; + + // Extractor outputs. + private ExtractorOutput extractorOutput; + + // State variables. + private int parserState; + private int bytesToNextTagHeader; + public int tagType; + public int tagDataSize; + public long tagTimestampUs; + + // Tags readers. + private AudioTagPayloadReader audioReader; + private VideoTagPayloadReader videoReader; + private ScriptTagPayloadReader metadataReader; + + public FlvExtractor() { + scratch = new ParsableByteArray(4); + headerBuffer = new ParsableByteArray(FLV_HEADER_SIZE); + tagHeaderBuffer = new ParsableByteArray(FLV_TAG_HEADER_SIZE); + tagData = new ParsableByteArray(); + parserState = STATE_READING_FLV_HEADER; + } + + @Override + public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + // Check if file starts with "FLV" tag + input.peekFully(scratch.data, 0, 3); + scratch.setPosition(0); + if (scratch.readUnsignedInt24() != FLV_TAG) { + return false; + } + + // Checking reserved flags are set to 0 + input.peekFully(scratch.data, 0, 2); + scratch.setPosition(0); + if ((scratch.readUnsignedShort() & 0xFA) != 0) { + return false; + } + + // Read data offset + input.peekFully(scratch.data, 0, 4); + scratch.setPosition(0); + int dataOffset = scratch.readInt(); + + input.resetPeekPosition(); + input.advancePeekPosition(dataOffset); + + // Checking first "previous tag size" is set to 0 + input.peekFully(scratch.data, 0, 4); + scratch.setPosition(0); + + return scratch.readInt() == 0; + } + + @Override + public void init(ExtractorOutput output) { + this.extractorOutput = output; + } + + @Override + public void seek() { + parserState = STATE_READING_FLV_HEADER; + bytesToNextTagHeader = 0; + } + + @Override + public void release() { + // Do nothing + } + + @Override + public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException, + InterruptedException { + while (true) { + switch (parserState) { + case STATE_READING_FLV_HEADER: + if (!readFlvHeader(input)) { + return RESULT_END_OF_INPUT; + } + break; + case STATE_SKIPPING_TO_TAG_HEADER: + skipToTagHeader(input); + break; + case STATE_READING_TAG_HEADER: + if (!readTagHeader(input)) { + return RESULT_END_OF_INPUT; + } + break; + case STATE_READING_TAG_DATA: + if (readTagData(input)) { + return RESULT_CONTINUE; + } + break; + } + } + } + + /** + * Reads an FLV container header from the provided {@link ExtractorInput}. + * + * @param input The {@link ExtractorInput} from which to read. + * @return True if header was read successfully. False if the end of stream was reached. + * @throws IOException If an error occurred reading or parsing data from the source. + * @throws InterruptedException If the thread was interrupted. + */ + private boolean readFlvHeader(ExtractorInput input) throws IOException, InterruptedException { + if (!input.readFully(headerBuffer.data, 0, FLV_HEADER_SIZE, true)) { + // We've reached the end of the stream. + return false; + } + + headerBuffer.setPosition(0); + headerBuffer.skipBytes(4); + int flags = headerBuffer.readUnsignedByte(); + boolean hasAudio = (flags & 0x04) != 0; + boolean hasVideo = (flags & 0x01) != 0; + if (hasAudio && audioReader == null) { + audioReader = new AudioTagPayloadReader(extractorOutput.track(TAG_TYPE_AUDIO)); + } + if (hasVideo && videoReader == null) { + videoReader = new VideoTagPayloadReader(extractorOutput.track(TAG_TYPE_VIDEO)); + } + if (metadataReader == null) { + metadataReader = new ScriptTagPayloadReader(null); + } + extractorOutput.endTracks(); + extractorOutput.seekMap(this); + + // We need to skip any additional content in the FLV header, plus the 4 byte previous tag size. + bytesToNextTagHeader = headerBuffer.readInt() - FLV_HEADER_SIZE + 4; + parserState = STATE_SKIPPING_TO_TAG_HEADER; + return true; + } + + /** + * Skips over data to reach the next tag header. + * + * @param input The {@link ExtractorInput} from which to read. + * @throws IOException If an error occurred skipping data from the source. + * @throws InterruptedException If the thread was interrupted. + */ + private void skipToTagHeader(ExtractorInput input) throws IOException, InterruptedException { + input.skipFully(bytesToNextTagHeader); + bytesToNextTagHeader = 0; + parserState = STATE_READING_TAG_HEADER; + } + + /** + * Reads a tag header from the provided {@link ExtractorInput}. + * + * @param input The {@link ExtractorInput} from which to read. + * @return True if tag header was read successfully. Otherwise, false. + * @throws IOException If an error occurred reading or parsing data from the source. + * @throws InterruptedException If the thread was interrupted. + */ + private boolean readTagHeader(ExtractorInput input) throws IOException, InterruptedException { + if (!input.readFully(tagHeaderBuffer.data, 0, FLV_TAG_HEADER_SIZE, true)) { + // We've reached the end of the stream. + return false; + } + + tagHeaderBuffer.setPosition(0); + tagType = tagHeaderBuffer.readUnsignedByte(); + tagDataSize = tagHeaderBuffer.readUnsignedInt24(); + tagTimestampUs = tagHeaderBuffer.readUnsignedInt24(); + tagTimestampUs = ((tagHeaderBuffer.readUnsignedByte() << 24) | tagTimestampUs) * 1000L; + tagHeaderBuffer.skipBytes(3); // streamId + parserState = STATE_READING_TAG_DATA; + return true; + } + + /** + * Reads the body of a tag from the provided {@link ExtractorInput}. + * + * @param input The {@link ExtractorInput} from which to read. + * @return True if the data was consumed by a reader. False if it was skipped. + * @throws IOException If an error occurred reading or parsing data from the source. + * @throws InterruptedException If the thread was interrupted. + */ + private boolean readTagData(ExtractorInput input) throws IOException, InterruptedException { + boolean wasConsumed = true; + if (tagType == TAG_TYPE_AUDIO && audioReader != null) { + audioReader.consume(prepareTagData(input), tagTimestampUs); + } else if (tagType == TAG_TYPE_VIDEO && videoReader != null) { + videoReader.consume(prepareTagData(input), tagTimestampUs); + } else if (tagType == TAG_TYPE_SCRIPT_DATA && metadataReader != null) { + metadataReader.consume(prepareTagData(input), tagTimestampUs); + if (metadataReader.getDurationUs() != C.UNKNOWN_TIME_US) { + if (audioReader != null) { + audioReader.setDurationUs(metadataReader.getDurationUs()); + } + if (videoReader != null) { + videoReader.setDurationUs(metadataReader.getDurationUs()); + } + } + } else { + input.skipFully(tagDataSize); + wasConsumed = false; + } + bytesToNextTagHeader = 4; // There's a 4 byte previous tag size before the next header. + parserState = STATE_SKIPPING_TO_TAG_HEADER; + return wasConsumed; + } + + private ParsableByteArray prepareTagData(ExtractorInput input) throws IOException, + InterruptedException { + if (tagDataSize > tagData.capacity()) { + tagData.reset(new byte[Math.max(tagData.capacity() * 2, tagDataSize)], 0); + } else { + tagData.setPosition(0); + } + tagData.setLimit(tagDataSize); + input.readFully(tagData.data, 0, tagDataSize); + return tagData; + } + + // SeekMap implementation. + + @Override + public boolean isSeekable() { + return false; + } + + @Override + public long getPosition(long timeUs) { + return 0; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/flv/ScriptTagPayloadReader.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/flv/ScriptTagPayloadReader.java new file mode 100755 index 000000000..748453e9f --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/flv/ScriptTagPayloadReader.java @@ -0,0 +1,212 @@ +/* + * 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.messenger.exoplayer.extractor.flv; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.ParserException; +import org.telegram.messenger.exoplayer.extractor.TrackOutput; +import org.telegram.messenger.exoplayer.util.ParsableByteArray; + +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +/** + * Parses Script Data tags from an FLV stream and extracts metadata information. + */ +/* package */ final class ScriptTagPayloadReader extends TagPayloadReader { + + private static final String NAME_METADATA = "onMetaData"; + private static final String KEY_DURATION = "duration"; + + // AMF object types + private static final int AMF_TYPE_NUMBER = 0; + private static final int AMF_TYPE_BOOLEAN = 1; + private static final int AMF_TYPE_STRING = 2; + private static final int AMF_TYPE_OBJECT = 3; + private static final int AMF_TYPE_ECMA_ARRAY = 8; + private static final int AMF_TYPE_END_MARKER = 9; + private static final int AMF_TYPE_STRICT_ARRAY = 10; + private static final int AMF_TYPE_DATE = 11; + + /** + * @param output A {@link TrackOutput} to which samples should be written. + */ + public ScriptTagPayloadReader(TrackOutput output) { + super(output); + } + + @Override + public void seek() { + // Do nothing. + } + + @Override + protected boolean parseHeader(ParsableByteArray data) { + return true; + } + + @Override + protected void parsePayload(ParsableByteArray data, long timeUs) throws ParserException { + int nameType = readAmfType(data); + if (nameType != AMF_TYPE_STRING) { + // Should never happen. + throw new ParserException(); + } + String name = readAmfString(data); + if (!NAME_METADATA.equals(name)) { + // We're only interested in metadata. + return; + } + int type = readAmfType(data); + if (type != AMF_TYPE_ECMA_ARRAY) { + // Should never happen. + throw new ParserException(); + } + // Set the duration to the value contained in the metadata, if present. + Map metadata = readAmfEcmaArray(data); + if (metadata.containsKey(KEY_DURATION)) { + double durationSeconds = (double) metadata.get(KEY_DURATION); + if (durationSeconds > 0.0) { + setDurationUs((long) (durationSeconds * C.MICROS_PER_SECOND)); + } + } + } + + private static int readAmfType(ParsableByteArray data) { + return data.readUnsignedByte(); + } + + /** + * Read a boolean from an AMF encoded buffer. + * + * @param data The buffer from which to read. + * @return The value read from the buffer. + */ + private static Boolean readAmfBoolean(ParsableByteArray data) { + return data.readUnsignedByte() == 1; + } + + /** + * Read a double number from an AMF encoded buffer. + * + * @param data The buffer from which to read. + * @return The value read from the buffer. + */ + private static Double readAmfDouble(ParsableByteArray data) { + return Double.longBitsToDouble(data.readLong()); + } + + /** + * Read a string from an AMF encoded buffer. + * + * @param data The buffer from which to read. + * @return The value read from the buffer. + */ + private static String readAmfString(ParsableByteArray data) { + int size = data.readUnsignedShort(); + int position = data.getPosition(); + data.skipBytes(size); + return new String(data.data, position, size); + } + + /** + * Read an array from an AMF encoded buffer. + * + * @param data The buffer from which to read. + * @return The value read from the buffer. + */ + private static ArrayList readAmfStrictArray(ParsableByteArray data) { + int count = data.readUnsignedIntToInt(); + ArrayList list = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + int type = readAmfType(data); + list.add(readAmfData(data, type)); + } + return list; + } + + /** + * Read an object from an AMF encoded buffer. + * + * @param data The buffer from which to read. + * @return The value read from the buffer. + */ + private static HashMap readAmfObject(ParsableByteArray data) { + HashMap array = new HashMap<>(); + while (true) { + String key = readAmfString(data); + int type = readAmfType(data); + if (type == AMF_TYPE_END_MARKER) { + break; + } + array.put(key, readAmfData(data, type)); + } + return array; + } + + /** + * Read an ECMA array from an AMF encoded buffer. + * + * @param data The buffer from which to read. + * @return The value read from the buffer. + */ + private static HashMap readAmfEcmaArray(ParsableByteArray data) { + int count = data.readUnsignedIntToInt(); + HashMap array = new HashMap<>(count); + for (int i = 0; i < count; i++) { + String key = readAmfString(data); + int type = readAmfType(data); + array.put(key, readAmfData(data, type)); + } + return array; + } + + /** + * Read a date from an AMF encoded buffer. + * + * @param data The buffer from which to read. + * @return The value read from the buffer. + */ + private static Date readAmfDate(ParsableByteArray data) { + Date date = new Date((long) readAmfDouble(data).doubleValue()); + data.skipBytes(2); // Skip reserved bytes. + return date; + } + + private static Object readAmfData(ParsableByteArray data, int type) { + switch (type) { + case AMF_TYPE_NUMBER: + return readAmfDouble(data); + case AMF_TYPE_BOOLEAN: + return readAmfBoolean(data); + case AMF_TYPE_STRING: + return readAmfString(data); + case AMF_TYPE_OBJECT: + return readAmfObject(data); + case AMF_TYPE_ECMA_ARRAY: + return readAmfEcmaArray(data); + case AMF_TYPE_STRICT_ARRAY: + return readAmfStrictArray(data); + case AMF_TYPE_DATE: + return readAmfDate(data); + default: + return null; + } + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/flv/TagPayloadReader.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/flv/TagPayloadReader.java new file mode 100755 index 000000000..302db7527 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/flv/TagPayloadReader.java @@ -0,0 +1,110 @@ +/* + * 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.messenger.exoplayer.extractor.flv; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.ParserException; +import org.telegram.messenger.exoplayer.extractor.TrackOutput; +import org.telegram.messenger.exoplayer.util.ParsableByteArray; + +/** + * Extracts individual samples from FLV tags, preserving original order. + */ +/* package */ abstract class TagPayloadReader { + + /** + * Thrown when the format is not supported. + */ + public static final class UnsupportedFormatException extends ParserException { + + public UnsupportedFormatException(String msg) { + super(msg); + } + + } + + protected final TrackOutput output; + + private long durationUs; + + /** + * @param output A {@link TrackOutput} to which samples should be written. + */ + protected TagPayloadReader(TrackOutput output) { + this.output = output; + this.durationUs = C.UNKNOWN_TIME_US; + } + + /** + * Sets duration in microseconds. + * + * @param durationUs duration in microseconds. + */ + public final void setDurationUs(long durationUs) { + this.durationUs = durationUs; + } + + /** + * Gets the duration in microseconds. + * + * @return The duration in microseconds. + */ + public final long getDurationUs() { + return durationUs; + } + + /** + * Notifies the reader that a seek has occurred. + *

+ * Following a call to this method, the data passed to the next invocation of + * {@link #consume(ParsableByteArray, long)} will not be a continuation of the data that + * was previously passed. Hence the reader should reset any internal state. + */ + public abstract void seek(); + + /** + * Consumes payload data. + * + * @param data The payload data to consume. + * @param timeUs The timestamp associated with the payload. + * @throws ParserException If an error occurs parsing the data. + */ + public final void consume(ParsableByteArray data, long timeUs) throws ParserException { + if (parseHeader(data)) { + parsePayload(data, timeUs); + } + } + + /** + * Parses tag header. + * + * @param data Buffer where the tag header is stored. + * @return True if the header was parsed successfully and the payload should be read. False + * otherwise. + * @throws ParserException If an error occurs parsing the header. + */ + protected abstract boolean parseHeader(ParsableByteArray data) throws ParserException; + + /** + * Parses tag payload. + * + * @param data Buffer where tag payload is stored + * @param timeUs Time position of the frame + * @throws ParserException If an error occurs parsing the payload. + */ + protected abstract void parsePayload(ParsableByteArray data, long timeUs) throws ParserException; + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/flv/VideoTagPayloadReader.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/flv/VideoTagPayloadReader.java new file mode 100755 index 000000000..b61bc5034 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/flv/VideoTagPayloadReader.java @@ -0,0 +1,199 @@ +/* + * 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.messenger.exoplayer.extractor.flv; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.MediaFormat; +import org.telegram.messenger.exoplayer.ParserException; +import org.telegram.messenger.exoplayer.extractor.TrackOutput; +import org.telegram.messenger.exoplayer.util.Assertions; +import org.telegram.messenger.exoplayer.util.MimeTypes; +import org.telegram.messenger.exoplayer.util.NalUnitUtil; +import org.telegram.messenger.exoplayer.util.ParsableBitArray; +import org.telegram.messenger.exoplayer.util.ParsableByteArray; + +import java.util.ArrayList; +import java.util.List; + +/** + * Parses video tags from an FLV stream and extracts H.264 nal units. + */ +/* package */ final class VideoTagPayloadReader extends TagPayloadReader { + + // Video codec. + private static final int VIDEO_CODEC_AVC = 7; + + // Frame types. + private static final int VIDEO_FRAME_KEYFRAME = 1; + private static final int VIDEO_FRAME_VIDEO_INFO = 5; + + // Packet types. + private static final int AVC_PACKET_TYPE_SEQUENCE_HEADER = 0; + private static final int AVC_PACKET_TYPE_AVC_NALU = 1; + + // Temporary arrays. + private final ParsableByteArray nalStartCode; + private final ParsableByteArray nalLength; + private int nalUnitLengthFieldLength; + + // State variables. + private boolean hasOutputFormat; + private int frameType; + + /** + * @param output A {@link TrackOutput} to which samples should be written. + */ + public VideoTagPayloadReader(TrackOutput output) { + super(output); + nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE); + nalLength = new ParsableByteArray(4); + } + + @Override + public void seek() { + // Do nothing. + } + + @Override + protected boolean parseHeader(ParsableByteArray data) throws UnsupportedFormatException { + int header = data.readUnsignedByte(); + int frameType = (header >> 4) & 0x0F; + int videoCodec = (header & 0x0F); + // Support just H.264 encoded content. + if (videoCodec != VIDEO_CODEC_AVC) { + throw new UnsupportedFormatException("Video format not supported: " + videoCodec); + } + this.frameType = frameType; + return (frameType != VIDEO_FRAME_VIDEO_INFO); + } + + @Override + protected void parsePayload(ParsableByteArray data, long timeUs) throws ParserException { + int packetType = data.readUnsignedByte(); + int compositionTimeMs = data.readUnsignedInt24(); + timeUs += compositionTimeMs * 1000L; + // Parse avc sequence header in case this was not done before. + if (packetType == AVC_PACKET_TYPE_SEQUENCE_HEADER && !hasOutputFormat) { + ParsableByteArray videoSequence = new ParsableByteArray(new byte[data.bytesLeft()]); + data.readBytes(videoSequence.data, 0, data.bytesLeft()); + + AvcSequenceHeaderData avcData = parseAvcCodecPrivate(videoSequence); + nalUnitLengthFieldLength = avcData.nalUnitLengthFieldLength; + + // Construct and output the format. + MediaFormat mediaFormat = MediaFormat.createVideoFormat(null, MimeTypes.VIDEO_H264, + MediaFormat.NO_VALUE, MediaFormat.NO_VALUE, getDurationUs(), avcData.width, + avcData.height, avcData.initializationData, MediaFormat.NO_VALUE, + avcData.pixelWidthAspectRatio); + output.format(mediaFormat); + hasOutputFormat = true; + } else if (packetType == AVC_PACKET_TYPE_AVC_NALU) { + // TODO: Deduplicate with Mp4Extractor. + // Zero the top three bytes of the array that we'll use to parse nal unit lengths, in case + // they're only 1 or 2 bytes long. + byte[] nalLengthData = nalLength.data; + nalLengthData[0] = 0; + nalLengthData[1] = 0; + nalLengthData[2] = 0; + int nalUnitLengthFieldLengthDiff = 4 - nalUnitLengthFieldLength; + // NAL units are length delimited, but the decoder requires start code delimited units. + // Loop until we've written the sample to the track output, replacing length delimiters with + // start codes as we encounter them. + int bytesWritten = 0; + int bytesToWrite; + while (data.bytesLeft() > 0) { + // Read the NAL length so that we know where we find the next one. + data.readBytes(nalLength.data, nalUnitLengthFieldLengthDiff, nalUnitLengthFieldLength); + nalLength.setPosition(0); + bytesToWrite = nalLength.readUnsignedIntToInt(); + + // Write a start code for the current NAL unit. + nalStartCode.setPosition(0); + output.sampleData(nalStartCode, 4); + bytesWritten += 4; + + // Write the payload of the NAL unit. + output.sampleData(data, bytesToWrite); + bytesWritten += bytesToWrite; + } + output.sampleMetadata(timeUs, frameType == VIDEO_FRAME_KEYFRAME ? C.SAMPLE_FLAG_SYNC : 0, + bytesWritten, 0, null); + } + } + + /** + * Builds initialization data for a {@link MediaFormat} from H.264 (AVC) codec private data. + * + * @return The AvcSequenceHeader data needed to initialize the video codec. + * @throws ParserException If the initialization data could not be built. + */ + private AvcSequenceHeaderData parseAvcCodecPrivate(ParsableByteArray buffer) + throws ParserException { + // TODO: Deduplicate with AtomParsers.parseAvcCFromParent. + buffer.setPosition(4); + int nalUnitLengthFieldLength = (buffer.readUnsignedByte() & 0x03) + 1; + Assertions.checkState(nalUnitLengthFieldLength != 3); + List initializationData = new ArrayList<>(); + int numSequenceParameterSets = buffer.readUnsignedByte() & 0x1F; + for (int i = 0; i < numSequenceParameterSets; i++) { + initializationData.add(NalUnitUtil.parseChildNalUnit(buffer)); + } + int numPictureParameterSets = buffer.readUnsignedByte(); + for (int j = 0; j < numPictureParameterSets; j++) { + initializationData.add(NalUnitUtil.parseChildNalUnit(buffer)); + } + + float pixelWidthAspectRatio = 1; + int width = MediaFormat.NO_VALUE; + int height = MediaFormat.NO_VALUE; + if (numSequenceParameterSets > 0) { + // Parse the first sequence parameter set to obtain pixelWidthAspectRatio. + ParsableBitArray spsDataBitArray = new ParsableBitArray(initializationData.get(0)); + // Skip the NAL header consisting of the nalUnitLengthField and the type (1 byte). + spsDataBitArray.setPosition(8 * (nalUnitLengthFieldLength + 1)); + NalUnitUtil.SpsData sps = NalUnitUtil.parseSpsNalUnit(spsDataBitArray); + width = sps.width; + height = sps.height; + pixelWidthAspectRatio = sps.pixelWidthAspectRatio; + } + + return new AvcSequenceHeaderData(initializationData, nalUnitLengthFieldLength, + width, height, pixelWidthAspectRatio); + } + + /** + * Holds data parsed from an Sequence Header video tag atom. + */ + private static final class AvcSequenceHeaderData { + + public final List initializationData; + public final int nalUnitLengthFieldLength; + public final float pixelWidthAspectRatio; + public final int width; + public final int height; + + public AvcSequenceHeaderData(List initializationData, int nalUnitLengthFieldLength, + int width, int height, float pixelWidthAspectRatio) { + this.initializationData = initializationData; + this.nalUnitLengthFieldLength = nalUnitLengthFieldLength; + this.pixelWidthAspectRatio = pixelWidthAspectRatio; + this.width = width; + this.height = height; + } + + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp3/ConstantBitrateSeeker.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp3/ConstantBitrateSeeker.java new file mode 100755 index 000000000..2bcba4c2e --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp3/ConstantBitrateSeeker.java @@ -0,0 +1,59 @@ +/* + * 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.messenger.exoplayer.extractor.mp3; + +import org.telegram.messenger.exoplayer.C; + +/** + * MP3 seeker that doesn't rely on metadata and seeks assuming the source has a constant bitrate. + */ +/* package */ final class ConstantBitrateSeeker implements Mp3Extractor.Seeker { + + private static final int BITS_PER_BYTE = 8; + + private final long firstFramePosition; + private final int bitrate; + private final long durationUs; + + public ConstantBitrateSeeker(long firstFramePosition, int bitrate, long inputLength) { + this.firstFramePosition = firstFramePosition; + this.bitrate = bitrate; + durationUs = inputLength == C.LENGTH_UNBOUNDED ? C.UNKNOWN_TIME_US : getTimeUs(inputLength); + } + + @Override + public boolean isSeekable() { + return durationUs != C.UNKNOWN_TIME_US; + } + + @Override + public long getPosition(long timeUs) { + return durationUs == C.UNKNOWN_TIME_US ? 0 + : firstFramePosition + (timeUs * bitrate) / (C.MICROS_PER_SECOND * BITS_PER_BYTE); + } + + @Override + public long getTimeUs(long position) { + return (Math.max(0, position - firstFramePosition) * C.MICROS_PER_SECOND * BITS_PER_BYTE) + / bitrate; + } + + @Override + public long getDurationUs() { + return durationUs; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp3/Id3Util.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp3/Id3Util.java new file mode 100755 index 000000000..14bc90227 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp3/Id3Util.java @@ -0,0 +1,299 @@ +/* + * 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.messenger.exoplayer.extractor.mp3; + +import org.telegram.messenger.exoplayer.extractor.ExtractorInput; +import org.telegram.messenger.exoplayer.extractor.GaplessInfo; +import org.telegram.messenger.exoplayer.util.ParsableByteArray; +import org.telegram.messenger.exoplayer.util.Util; + +import android.util.Pair; + +import java.io.IOException; +import java.nio.charset.Charset; + +/** + * Utility for parsing ID3 version 2 metadata in MP3 files. + */ +/* package */ final class Id3Util { + + /** + * The maximum valid length for metadata in bytes. + */ + private static final int MAXIMUM_METADATA_SIZE = 3 * 1024 * 1024; + + private static final int ID3_TAG = Util.getIntegerCodeForString("ID3"); + private static final Charset[] CHARSET_BY_ENCODING = new Charset[] {Charset.forName("ISO-8859-1"), + Charset.forName("UTF-16LE"), Charset.forName("UTF-16BE"), Charset.forName("UTF-8")}; + + /** + * Peeks data from the input and parses ID3 metadata. + * + * @param input The {@link ExtractorInput} from which data should be peeked. + * @return The gapless playback information, if present and non-zero. {@code null} otherwise. + * @throws IOException If an error occurred peeking from the input. + * @throws InterruptedException If the thread was interrupted. + */ + public static GaplessInfo parseId3(ExtractorInput input) + throws IOException, InterruptedException { + ParsableByteArray scratch = new ParsableByteArray(10); + int peekedId3Bytes = 0; + GaplessInfo metadata = null; + while (true) { + input.peekFully(scratch.data, 0, 10); + scratch.setPosition(0); + if (scratch.readUnsignedInt24() != ID3_TAG) { + break; + } + + int majorVersion = scratch.readUnsignedByte(); + int minorVersion = scratch.readUnsignedByte(); + int flags = scratch.readUnsignedByte(); + int length = scratch.readSynchSafeInt(); + if (metadata == null && canParseMetadata(majorVersion, minorVersion, flags, length)) { + byte[] frame = new byte[length]; + input.peekFully(frame, 0, length); + metadata = parseGaplessInfo(new ParsableByteArray(frame), majorVersion, flags); + } else { + input.advancePeekPosition(length); + } + + peekedId3Bytes += 10 + length; + } + input.resetPeekPosition(); + input.advancePeekPosition(peekedId3Bytes); + return metadata; + } + + private static boolean canParseMetadata(int majorVersion, int minorVersion, int flags, + int length) { + return minorVersion != 0xFF && majorVersion >= 2 && majorVersion <= 4 + && length <= MAXIMUM_METADATA_SIZE + && !(majorVersion == 2 && ((flags & 0x3F) != 0 || (flags & 0x40) != 0)) + && !(majorVersion == 3 && (flags & 0x1F) != 0) + && !(majorVersion == 4 && (flags & 0x0F) != 0); + } + + private static GaplessInfo parseGaplessInfo(ParsableByteArray frame, int version, int flags) { + unescape(frame, version, flags); + + // Skip any extended header. + frame.setPosition(0); + if (version == 3 && (flags & 0x40) != 0) { + if (frame.bytesLeft() < 4) { + return null; + } + int extendedHeaderSize = frame.readUnsignedIntToInt(); + if (extendedHeaderSize > frame.bytesLeft()) { + return null; + } + int paddingSize = 0; + if (extendedHeaderSize >= 6) { + frame.skipBytes(2); // extended flags + paddingSize = frame.readUnsignedIntToInt(); + frame.setPosition(4); + frame.setLimit(frame.limit() - paddingSize); + if (frame.bytesLeft() < extendedHeaderSize) { + return null; + } + } + frame.skipBytes(extendedHeaderSize); + } else if (version == 4 && (flags & 0x40) != 0) { + if (frame.bytesLeft() < 4) { + return null; + } + int extendedHeaderSize = frame.readSynchSafeInt(); + if (extendedHeaderSize < 6 || extendedHeaderSize > frame.bytesLeft() + 4) { + return null; + } + frame.setPosition(extendedHeaderSize); + } + + // Extract gapless playback metadata stored in comments. + Pair comment; + while ((comment = findNextComment(version, frame)) != null) { + if (comment.first.length() > 3) { + GaplessInfo gaplessInfo = + GaplessInfo.createFromComment(comment.first.substring(3), comment.second); + if (gaplessInfo != null) { + return gaplessInfo; + } + } + } + return null; + } + + private static Pair findNextComment(int majorVersion, ParsableByteArray data) { + int frameSize; + while (true) { + if (majorVersion == 2) { + if (data.bytesLeft() < 6) { + return null; + } + String id = data.readString(3, Charset.forName("US-ASCII")); + if (id.equals("\0\0\0")) { + return null; + } + frameSize = data.readUnsignedInt24(); + if (frameSize == 0 || frameSize > data.bytesLeft()) { + return null; + } + if (id.equals("COM")) { + break; + } + } else /* major == 3 || major == 4 */ { + if (data.bytesLeft() < 10) { + return null; + } + String id = data.readString(4, Charset.forName("US-ASCII")); + if (id.equals("\0\0\0\0")) { + return null; + } + frameSize = majorVersion == 4 ? data.readSynchSafeInt() : data.readUnsignedIntToInt(); + if (frameSize == 0 || frameSize > data.bytesLeft() - 2) { + return null; + } + int flags = data.readUnsignedShort(); + boolean compressedOrEncrypted = (majorVersion == 4 && (flags & 0x0C) != 0) + || (majorVersion == 3 && (flags & 0xC0) != 0); + if (!compressedOrEncrypted && id.equals("COMM")) { + break; + } + } + data.skipBytes(frameSize); + } + + // The comment tag is at the reading position in data. + int encoding = data.readUnsignedByte(); + if (encoding < 0 || encoding >= CHARSET_BY_ENCODING.length) { + return null; + } + Charset charset = CHARSET_BY_ENCODING[encoding]; + String[] commentFields = data.readString(frameSize - 1, charset).split("\0"); + return commentFields.length == 2 ? Pair.create(commentFields[0], commentFields[1]) : null; + } + + private static boolean unescape(ParsableByteArray frame, int version, int flags) { + if (version != 4) { + if ((flags & 0x80) != 0) { + // Remove unsynchronization on ID3 version < 2.4.0. + byte[] bytes = frame.data; + int newLength = bytes.length; + for (int i = 0; i + 1 < newLength; i++) { + if ((bytes[i] & 0xFF) == 0xFF && bytes[i + 1] == 0x00) { + System.arraycopy(bytes, i + 2, bytes, i + 1, newLength - i - 2); + newLength--; + } + } + frame.setLimit(newLength); + } + } else { + // Remove unsynchronization on ID3 version 2.4.0. + if (canUnescapeVersion4(frame, false)) { + unescapeVersion4(frame, false); + } else if (canUnescapeVersion4(frame, true)) { + unescapeVersion4(frame, true); + } else { + return false; + } + } + return true; + } + + private static boolean canUnescapeVersion4(ParsableByteArray frame, + boolean unsignedIntDataSizeHack) { + frame.setPosition(0); + while (frame.bytesLeft() >= 10) { + if (frame.readInt() == 0) { + return true; + } + long dataSize = frame.readUnsignedInt(); + if (!unsignedIntDataSizeHack) { + // Parse the data size as a syncsafe integer. + if ((dataSize & 0x808080L) != 0) { + return false; + } + dataSize = (dataSize & 0x7F) | (((dataSize >> 8) & 0x7F) << 7) + | (((dataSize >> 16) & 0x7F) << 14) | (((dataSize >> 24) & 0x7F) << 21); + } + if (dataSize > frame.bytesLeft() - 2) { + return false; + } + int flags = frame.readUnsignedShort(); + if ((flags & 1) != 0) { + if (frame.bytesLeft() < 4) { + return false; + } + } + frame.skipBytes((int) dataSize); + } + return true; + } + + private static void unescapeVersion4(ParsableByteArray frame, boolean unsignedIntDataSizeHack) { + frame.setPosition(0); + byte[] bytes = frame.data; + while (frame.bytesLeft() >= 10) { + if (frame.readInt() == 0) { + return; + } + int dataSize = + unsignedIntDataSizeHack ? frame.readUnsignedIntToInt() : frame.readSynchSafeInt(); + int flags = frame.readUnsignedShort(); + int previousFlags = flags; + if ((flags & 1) != 0) { + // Strip data length indicator. + int offset = frame.getPosition(); + System.arraycopy(bytes, offset + 4, bytes, offset, frame.bytesLeft() - 4); + dataSize -= 4; + flags &= ~1; + frame.setLimit(frame.limit() - 4); + } + if ((flags & 2) != 0) { + // Unescape 0xFF00 to 0xFF in the next dataSize bytes. + int readOffset = frame.getPosition() + 1; + int writeOffset = readOffset; + for (int i = 0; i + 1 < dataSize; i++) { + if ((bytes[readOffset - 1] & 0xFF) == 0xFF && bytes[readOffset] == 0) { + readOffset++; + dataSize--; + } + bytes[writeOffset++] = bytes[readOffset++]; + } + frame.setLimit(frame.limit() - (readOffset - writeOffset)); + System.arraycopy(bytes, readOffset, bytes, writeOffset, frame.bytesLeft() - readOffset); + flags &= ~2; + } + if (flags != previousFlags || unsignedIntDataSizeHack) { + int dataSizeOffset = frame.getPosition() - 6; + writeSyncSafeInteger(bytes, dataSizeOffset, dataSize); + bytes[dataSizeOffset + 4] = (byte) (flags >> 8); + bytes[dataSizeOffset + 5] = (byte) (flags & 0xFF); + } + frame.skipBytes(dataSize); + } + } + + private static void writeSyncSafeInteger(byte[] bytes, int offset, int value) { + bytes[offset] = (byte) ((value >> 21) & 0x7F); + bytes[offset + 1] = (byte) ((value >> 14) & 0x7F); + bytes[offset + 2] = (byte) ((value >> 7) & 0x7F); + bytes[offset + 3] = (byte) (value & 0x7F); + } + + private Id3Util() {} + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp3/Mp3Extractor.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp3/Mp3Extractor.java new file mode 100755 index 000000000..6bfa8ef74 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp3/Mp3Extractor.java @@ -0,0 +1,343 @@ +/* + * 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.messenger.exoplayer.extractor.mp3; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.MediaFormat; +import org.telegram.messenger.exoplayer.ParserException; +import org.telegram.messenger.exoplayer.extractor.Extractor; +import org.telegram.messenger.exoplayer.extractor.ExtractorInput; +import org.telegram.messenger.exoplayer.extractor.ExtractorOutput; +import org.telegram.messenger.exoplayer.extractor.GaplessInfo; +import org.telegram.messenger.exoplayer.extractor.PositionHolder; +import org.telegram.messenger.exoplayer.extractor.SeekMap; +import org.telegram.messenger.exoplayer.extractor.TrackOutput; +import org.telegram.messenger.exoplayer.util.MpegAudioHeader; +import org.telegram.messenger.exoplayer.util.ParsableByteArray; +import org.telegram.messenger.exoplayer.util.Util; + +import java.io.EOFException; +import java.io.IOException; + +/** + * Extracts data from an MP3 file. + */ +public final class Mp3Extractor implements Extractor { + + /** + * The maximum number of bytes to search when synchronizing, before giving up. + */ + private static final int MAX_SYNC_BYTES = 128 * 1024; + /** + * The maximum number of bytes to peek when sniffing, excluding the ID3 header, before giving up. + */ + private static final int MAX_SNIFF_BYTES = MpegAudioHeader.MAX_FRAME_SIZE_BYTES; + + /** + * Mask that includes the audio header values that must match between frames. + */ + private static final int HEADER_MASK = 0xFFFE0C00; + private static final int XING_HEADER = Util.getIntegerCodeForString("Xing"); + private static final int INFO_HEADER = Util.getIntegerCodeForString("Info"); + private static final int VBRI_HEADER = Util.getIntegerCodeForString("VBRI"); + + private final long forcedFirstSampleTimestampUs; + private final ParsableByteArray scratch; + private final MpegAudioHeader synchronizedHeader; + + // Extractor outputs. + private ExtractorOutput extractorOutput; + private TrackOutput trackOutput; + + private int synchronizedHeaderData; + + private GaplessInfo gaplessInfo; + private Seeker seeker; + private long basisTimeUs; + private long samplesRead; + private int sampleBytesRemaining; + + /** + * Constructs a new {@link Mp3Extractor}. + */ + public Mp3Extractor() { + this(-1); + } + + /** + * Constructs a new {@link Mp3Extractor}. + * + * @param forcedFirstSampleTimestampUs A timestamp to force for the first sample, or -1 if forcing + * is not required. + */ + public Mp3Extractor(long forcedFirstSampleTimestampUs) { + this.forcedFirstSampleTimestampUs = forcedFirstSampleTimestampUs; + scratch = new ParsableByteArray(4); + synchronizedHeader = new MpegAudioHeader(); + basisTimeUs = -1; + } + + @Override + public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + return synchronize(input, true); + } + + @Override + public void init(ExtractorOutput extractorOutput) { + this.extractorOutput = extractorOutput; + trackOutput = extractorOutput.track(0); + extractorOutput.endTracks(); + } + + @Override + public void seek() { + synchronizedHeaderData = 0; + samplesRead = 0; + basisTimeUs = -1; + sampleBytesRemaining = 0; + } + + @Override + public void release() { + // Do nothing + } + + @Override + public int read(ExtractorInput input, PositionHolder seekPosition) + throws IOException, InterruptedException { + if (synchronizedHeaderData == 0 && !synchronizeCatchingEndOfInput(input)) { + return RESULT_END_OF_INPUT; + } + if (seeker == null) { + setupSeeker(input); + extractorOutput.seekMap(seeker); + MediaFormat mediaFormat = MediaFormat.createAudioFormat(null, synchronizedHeader.mimeType, + MediaFormat.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, seeker.getDurationUs(), + synchronizedHeader.channels, synchronizedHeader.sampleRate, null, null); + if (gaplessInfo != null) { + mediaFormat = + mediaFormat.copyWithGaplessInfo(gaplessInfo.encoderDelay, gaplessInfo.encoderPadding); + } + trackOutput.format(mediaFormat); + } + return readSample(input); + } + + private int readSample(ExtractorInput extractorInput) throws IOException, InterruptedException { + if (sampleBytesRemaining == 0) { + if (!maybeResynchronize(extractorInput)) { + return RESULT_END_OF_INPUT; + } + if (basisTimeUs == -1) { + basisTimeUs = seeker.getTimeUs(extractorInput.getPosition()); + if (forcedFirstSampleTimestampUs != -1) { + long embeddedFirstSampleTimestampUs = seeker.getTimeUs(0); + basisTimeUs += forcedFirstSampleTimestampUs - embeddedFirstSampleTimestampUs; + } + } + sampleBytesRemaining = synchronizedHeader.frameSize; + } + int bytesAppended = trackOutput.sampleData(extractorInput, sampleBytesRemaining, true); + if (bytesAppended == C.RESULT_END_OF_INPUT) { + return RESULT_END_OF_INPUT; + } + sampleBytesRemaining -= bytesAppended; + if (sampleBytesRemaining > 0) { + return RESULT_CONTINUE; + } + long timeUs = basisTimeUs + (samplesRead * C.MICROS_PER_SECOND / synchronizedHeader.sampleRate); + trackOutput.sampleMetadata(timeUs, C.SAMPLE_FLAG_SYNC, synchronizedHeader.frameSize, 0, null); + samplesRead += synchronizedHeader.samplesPerFrame; + sampleBytesRemaining = 0; + return RESULT_CONTINUE; + } + + /** + * Attempts to read an MPEG audio header at the current offset, resynchronizing if necessary. + */ + private boolean maybeResynchronize(ExtractorInput extractorInput) + throws IOException, InterruptedException { + extractorInput.resetPeekPosition(); + if (!extractorInput.peekFully(scratch.data, 0, 4, true)) { + return false; + } + + scratch.setPosition(0); + int sampleHeaderData = scratch.readInt(); + if ((sampleHeaderData & HEADER_MASK) == (synchronizedHeaderData & HEADER_MASK)) { + int frameSize = MpegAudioHeader.getFrameSize(sampleHeaderData); + if (frameSize != -1) { + MpegAudioHeader.populateHeader(sampleHeaderData, synchronizedHeader); + return true; + } + } + + synchronizedHeaderData = 0; + extractorInput.skipFully(1); + return synchronizeCatchingEndOfInput(extractorInput); + } + + private boolean synchronizeCatchingEndOfInput(ExtractorInput input) + throws IOException, InterruptedException { + // An EOFException will be raised if any peek operation was partially satisfied. If a seek + // operation resulted in reading from within the last frame, we may try to peek past the end of + // the file in a partially-satisfied read operation, so we need to catch the exception. + try { + return synchronize(input, false); + } catch (EOFException e) { + return false; + } + } + + private boolean synchronize(ExtractorInput input, boolean sniffing) + throws IOException, InterruptedException { + int searched = 0; + int validFrameCount = 0; + int candidateSynchronizedHeaderData = 0; + int peekedId3Bytes = 0; + input.resetPeekPosition(); + if (input.getPosition() == 0) { + gaplessInfo = Id3Util.parseId3(input); + peekedId3Bytes = (int) input.getPeekPosition(); + if (!sniffing) { + input.skipFully(peekedId3Bytes); + } + } + while (true) { + if (sniffing && searched == MAX_SNIFF_BYTES) { + return false; + } + if (!sniffing && searched == MAX_SYNC_BYTES) { + throw new ParserException("Searched too many bytes."); + } + if (!input.peekFully(scratch.data, 0, 4, true)) { + return false; + } + scratch.setPosition(0); + int headerData = scratch.readInt(); + int frameSize; + if ((candidateSynchronizedHeaderData != 0 + && (headerData & HEADER_MASK) != (candidateSynchronizedHeaderData & HEADER_MASK)) + || (frameSize = MpegAudioHeader.getFrameSize(headerData)) == -1) { + // The header is invalid or doesn't match the candidate header. Try the next byte offset. + validFrameCount = 0; + candidateSynchronizedHeaderData = 0; + searched++; + if (sniffing) { + input.resetPeekPosition(); + input.advancePeekPosition(peekedId3Bytes + searched); + } else { + input.skipFully(1); + } + } else { + // The header is valid and matches the candidate header. + validFrameCount++; + if (validFrameCount == 1) { + MpegAudioHeader.populateHeader(headerData, synchronizedHeader); + candidateSynchronizedHeaderData = headerData; + } else if (validFrameCount == 4) { + break; + } + input.advancePeekPosition(frameSize - 4); + } + } + // Prepare to read the synchronized frame. + if (sniffing) { + input.skipFully(peekedId3Bytes + searched); + } else { + input.resetPeekPosition(); + } + synchronizedHeaderData = candidateSynchronizedHeaderData; + return true; + } + + /** + * Sets {@link #seeker} to seek using metadata read from {@code input}, which should provide data + * from the start of the first frame in the stream. On returning, the input's position will be set + * to the start of the first frame of audio. + * + * @param input The {@link ExtractorInput} from which to read. + * @throws IOException Thrown if there was an error reading from the stream. Not expected if the + * next two frames were already peeked during synchronization. + * @throws InterruptedException Thrown if reading from the stream was interrupted. Not expected if + * the next two frames were already peeked during synchronization. + */ + private void setupSeeker(ExtractorInput input) throws IOException, InterruptedException { + // Read the first frame which may contain a Xing or VBRI header with seeking metadata. + ParsableByteArray frame = new ParsableByteArray(synchronizedHeader.frameSize); + input.peekFully(frame.data, 0, synchronizedHeader.frameSize); + + long position = input.getPosition(); + long length = input.getLength(); + + // Check if there is a Xing header. + int xingBase = (synchronizedHeader.version & 1) != 0 + ? (synchronizedHeader.channels != 1 ? 36 : 21) // MPEG 1 + : (synchronizedHeader.channels != 1 ? 21 : 13); // MPEG 2 or 2.5 + frame.setPosition(xingBase); + int headerData = frame.readInt(); + if (headerData == XING_HEADER || headerData == INFO_HEADER) { + seeker = XingSeeker.create(synchronizedHeader, frame, position, length); + if (seeker != null && gaplessInfo == null) { + // If there is a Xing header, read gapless playback metadata at a fixed offset. + input.resetPeekPosition(); + input.advancePeekPosition(xingBase + 141); + input.peekFully(scratch.data, 0, 3); + scratch.setPosition(0); + gaplessInfo = GaplessInfo.createFromXingHeaderValue(scratch.readUnsignedInt24()); + } + input.skipFully(synchronizedHeader.frameSize); + } else { + // Check if there is a VBRI header. + frame.setPosition(36); // MPEG audio header (4 bytes) + 32 bytes. + headerData = frame.readInt(); + if (headerData == VBRI_HEADER) { + seeker = VbriSeeker.create(synchronizedHeader, frame, position, length); + input.skipFully(synchronizedHeader.frameSize); + } + } + + if (seeker == null) { + // Repopulate the synchronized header in case we had to skip an invalid seeking header, which + // would give an invalid CBR bitrate. + input.resetPeekPosition(); + input.peekFully(scratch.data, 0, 4); + scratch.setPosition(0); + MpegAudioHeader.populateHeader(scratch.readInt(), synchronizedHeader); + seeker = new ConstantBitrateSeeker(input.getPosition(), synchronizedHeader.bitrate, length); + } + } + + /** + * {@link SeekMap} that also allows mapping from position (byte offset) back to time, which can be + * used to work out the new sample basis timestamp after seeking and resynchronization. + */ + /* package */ interface Seeker extends SeekMap { + + /** + * Maps a position (byte offset) to a corresponding sample timestamp. + * + * @param position A seek position (byte offset) relative to the start of the stream. + * @return The corresponding timestamp of the next sample to be read, in microseconds. + */ + long getTimeUs(long position); + + /** Returns the duration of the source, in microseconds. */ + long getDurationUs(); + + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp3/VbriSeeker.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp3/VbriSeeker.java new file mode 100755 index 000000000..0dbac52b5 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp3/VbriSeeker.java @@ -0,0 +1,120 @@ +/* + * 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.messenger.exoplayer.extractor.mp3; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.util.MpegAudioHeader; +import org.telegram.messenger.exoplayer.util.ParsableByteArray; +import org.telegram.messenger.exoplayer.util.Util; + +/** + * MP3 seeker that uses metadata from a VBRI header. + */ +/* package */ final class VbriSeeker implements Mp3Extractor.Seeker { + + /** + * Returns a {@link VbriSeeker} for seeking in the stream, if required information is present. + * Returns {@code null} if not. On returning, {@code frame}'s position is not specified so the + * caller should reset it. + * + * @param mpegAudioHeader The MPEG audio header associated with the frame. + * @param frame The data in this audio frame, with its position set to immediately after the + * 'VBRI' tag. + * @param position The position (byte offset) of the start of this frame in the stream. + * @param inputLength The length of the stream in bytes. + * @return A {@link VbriSeeker} for seeking in the stream, or {@code null} if the required + * information is not present. + */ + public static VbriSeeker create(MpegAudioHeader mpegAudioHeader, ParsableByteArray frame, + long position, long inputLength) { + frame.skipBytes(10); + int numFrames = frame.readInt(); + if (numFrames <= 0) { + return null; + } + int sampleRate = mpegAudioHeader.sampleRate; + long durationUs = Util.scaleLargeTimestamp(numFrames, + C.MICROS_PER_SECOND * (sampleRate >= 32000 ? 1152 : 576), sampleRate); + int entryCount = frame.readUnsignedShort(); + int scale = frame.readUnsignedShort(); + int entrySize = frame.readUnsignedShort(); + frame.skipBytes(2); + + // Skip the frame containing the VBRI header. + position += mpegAudioHeader.frameSize; + + // Read table of contents entries. + long[] timesUs = new long[entryCount + 1]; + long[] positions = new long[entryCount + 1]; + timesUs[0] = 0L; + positions[0] = position; + for (int index = 1; index < timesUs.length; index++) { + int segmentSize; + switch (entrySize) { + case 1: + segmentSize = frame.readUnsignedByte(); + break; + case 2: + segmentSize = frame.readUnsignedShort(); + break; + case 3: + segmentSize = frame.readUnsignedInt24(); + break; + case 4: + segmentSize = frame.readUnsignedIntToInt(); + break; + default: + return null; + } + position += segmentSize * scale; + timesUs[index] = index * durationUs / entryCount; + positions[index] = + inputLength == C.LENGTH_UNBOUNDED ? position : Math.min(inputLength, position); + } + return new VbriSeeker(timesUs, positions, durationUs); + } + + private final long[] timesUs; + private final long[] positions; + private final long durationUs; + + private VbriSeeker(long[] timesUs, long[] positions, long durationUs) { + this.timesUs = timesUs; + this.positions = positions; + this.durationUs = durationUs; + } + + @Override + public boolean isSeekable() { + return true; + } + + @Override + public long getPosition(long timeUs) { + return positions[Util.binarySearchFloor(timesUs, timeUs, true, true)]; + } + + @Override + public long getTimeUs(long position) { + return timesUs[Util.binarySearchFloor(positions, position, true, true)]; + } + + @Override + public long getDurationUs() { + return durationUs; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp3/XingSeeker.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp3/XingSeeker.java new file mode 100755 index 000000000..1476ff258 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp3/XingSeeker.java @@ -0,0 +1,168 @@ +/* + * 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.messenger.exoplayer.extractor.mp3; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.util.MpegAudioHeader; +import org.telegram.messenger.exoplayer.util.ParsableByteArray; +import org.telegram.messenger.exoplayer.util.Util; + +/** + * MP3 seeker that uses metadata from a Xing header. + */ +/* package */ final class XingSeeker implements Mp3Extractor.Seeker { + + /** + * Returns a {@link XingSeeker} for seeking in the stream, if required information is present. + * Returns {@code null} if not. On returning, {@code frame}'s position is not specified so the + * caller should reset it. + * + * @param mpegAudioHeader The MPEG audio header associated with the frame. + * @param frame The data in this audio frame, with its position set to immediately after the + * 'Xing' or 'Info' tag. + * @param position The position (byte offset) of the start of this frame in the stream. + * @param inputLength The length of the stream in bytes. + * @return A {@link XingSeeker} for seeking in the stream, or {@code null} if the required + * information is not present. + */ + public static XingSeeker create(MpegAudioHeader mpegAudioHeader, ParsableByteArray frame, + long position, long inputLength) { + int samplesPerFrame = mpegAudioHeader.samplesPerFrame; + int sampleRate = mpegAudioHeader.sampleRate; + long firstFramePosition = position + mpegAudioHeader.frameSize; + + int flags = frame.readInt(); + int frameCount; + if ((flags & 0x01) != 0x01 || (frameCount = frame.readUnsignedIntToInt()) == 0) { + // If the frame count is missing/invalid, the header can't be used to determine the duration. + return null; + } + long durationUs = Util.scaleLargeTimestamp(frameCount, samplesPerFrame * C.MICROS_PER_SECOND, + sampleRate); + if ((flags & 0x06) != 0x06) { + // If the size in bytes or table of contents is missing, the stream is not seekable. + return new XingSeeker(firstFramePosition, durationUs, inputLength); + } + + long sizeBytes = frame.readUnsignedIntToInt(); + frame.skipBytes(1); + long[] tableOfContents = new long[99]; + for (int i = 0; i < 99; i++) { + tableOfContents[i] = frame.readUnsignedByte(); + } + + // TODO: Handle encoder delay and padding in 3 bytes offset by xingBase + 213 bytes: + // delay = (frame.readUnsignedByte() << 4) + (frame.readUnsignedByte() >> 4); + // padding = ((frame.readUnsignedByte() & 0x0F) << 8) + frame.readUnsignedByte(); + return new XingSeeker(firstFramePosition, durationUs, inputLength, tableOfContents, + sizeBytes, mpegAudioHeader.frameSize); + } + + private final long firstFramePosition; + private final long durationUs; + private final long inputLength; + /** + * Entries are in the range [0, 255], but are stored as long integers for convenience. + */ + private final long[] tableOfContents; + private final long sizeBytes; + private final int headerSize; + + private XingSeeker(long firstFramePosition, long durationUs, long inputLength) { + this(firstFramePosition, durationUs, inputLength, null, 0, 0); + } + + private XingSeeker(long firstFramePosition, long durationUs, long inputLength, + long[] tableOfContents, long sizeBytes, int headerSize) { + this.firstFramePosition = firstFramePosition; + this.durationUs = durationUs; + this.inputLength = inputLength; + this.tableOfContents = tableOfContents; + this.sizeBytes = sizeBytes; + this.headerSize = headerSize; + } + + @Override + public boolean isSeekable() { + return tableOfContents != null; + } + + @Override + public long getPosition(long timeUs) { + if (!isSeekable()) { + return firstFramePosition; + } + float percent = timeUs * 100f / durationUs; + float fx; + if (percent <= 0f) { + fx = 0f; + } else if (percent >= 100f) { + fx = 256f; + } else { + int a = (int) percent; + float fa, fb; + if (a == 0) { + fa = 0f; + } else { + fa = tableOfContents[a - 1]; + } + if (a < 99) { + fb = tableOfContents[a]; + } else { + fb = 256f; + } + fx = fa + (fb - fa) * (percent - a); + } + + long position = Math.round((1.0 / 256) * fx * sizeBytes) + firstFramePosition; + long maximumPosition = inputLength != C.LENGTH_UNBOUNDED ? inputLength - 1 + : firstFramePosition - headerSize + sizeBytes - 1; + return Math.min(position, maximumPosition); + } + + @Override + public long getTimeUs(long position) { + if (!isSeekable() || position < firstFramePosition) { + return 0L; + } + double offsetByte = 256.0 * (position - firstFramePosition) / sizeBytes; + int previousTocPosition = + Util.binarySearchFloor(tableOfContents, (long) offsetByte, true, false) + 1; + long previousTime = getTimeUsForTocPosition(previousTocPosition); + + // Linearly interpolate the time taking into account the next entry. + long previousByte = previousTocPosition == 0 ? 0 : tableOfContents[previousTocPosition - 1]; + long nextByte = previousTocPosition == 99 ? 256 : tableOfContents[previousTocPosition]; + long nextTime = getTimeUsForTocPosition(previousTocPosition + 1); + long timeOffset = nextByte == previousByte ? 0 : (long) ((nextTime - previousTime) + * (offsetByte - previousByte) / (nextByte - previousByte)); + return previousTime + timeOffset; + } + + @Override + public long getDurationUs() { + return durationUs; + } + + /** + * Returns the time in microseconds corresponding to a table of contents position, which is + * interpreted as a percentage of the stream's duration between 0 and 100. + */ + private long getTimeUsForTocPosition(int tocPosition) { + return durationUs * tocPosition / 100; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp4/Atom.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp4/Atom.java new file mode 100755 index 000000000..48269a59f --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp4/Atom.java @@ -0,0 +1,299 @@ +/* + * 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.messenger.exoplayer.extractor.mp4; + +import org.telegram.messenger.exoplayer.util.ParsableByteArray; +import org.telegram.messenger.exoplayer.util.Util; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/* package*/ abstract class Atom { + + /** + * Size of an atom header, in bytes. + */ + public static final int HEADER_SIZE = 8; + + /** + * Size of a full atom header, in bytes. + */ + public static final int FULL_HEADER_SIZE = 12; + + /** + * Size of a long atom header, in bytes. + */ + public static final int LONG_HEADER_SIZE = 16; + + /** + * Value for the first 32 bits of atomSize when the atom size is actually a long value. + */ + public static final int LONG_SIZE_PREFIX = 1; + + public static final int TYPE_ftyp = Util.getIntegerCodeForString("ftyp"); + public static final int TYPE_avc1 = Util.getIntegerCodeForString("avc1"); + public static final int TYPE_avc3 = Util.getIntegerCodeForString("avc3"); + public static final int TYPE_hvc1 = Util.getIntegerCodeForString("hvc1"); + public static final int TYPE_hev1 = Util.getIntegerCodeForString("hev1"); + public static final int TYPE_s263 = Util.getIntegerCodeForString("s263"); + public static final int TYPE_d263 = Util.getIntegerCodeForString("d263"); + public static final int TYPE_mdat = Util.getIntegerCodeForString("mdat"); + public static final int TYPE_mp4a = Util.getIntegerCodeForString("mp4a"); + public static final int TYPE_wave = Util.getIntegerCodeForString("wave"); + public static final int TYPE_lpcm = Util.getIntegerCodeForString("lpcm"); + public static final int TYPE_sowt = Util.getIntegerCodeForString("sowt"); + public static final int TYPE_ac_3 = Util.getIntegerCodeForString("ac-3"); + public static final int TYPE_dac3 = Util.getIntegerCodeForString("dac3"); + public static final int TYPE_ec_3 = Util.getIntegerCodeForString("ec-3"); + public static final int TYPE_dec3 = Util.getIntegerCodeForString("dec3"); + public static final int TYPE_dtsc = Util.getIntegerCodeForString("dtsc"); + public static final int TYPE_dtsh = Util.getIntegerCodeForString("dtsh"); + public static final int TYPE_dtsl = Util.getIntegerCodeForString("dtsl"); + public static final int TYPE_dtse = Util.getIntegerCodeForString("dtse"); + public static final int TYPE_ddts = Util.getIntegerCodeForString("ddts"); + public static final int TYPE_tfdt = Util.getIntegerCodeForString("tfdt"); + public static final int TYPE_tfhd = Util.getIntegerCodeForString("tfhd"); + public static final int TYPE_trex = Util.getIntegerCodeForString("trex"); + public static final int TYPE_trun = Util.getIntegerCodeForString("trun"); + public static final int TYPE_sidx = Util.getIntegerCodeForString("sidx"); + public static final int TYPE_moov = Util.getIntegerCodeForString("moov"); + public static final int TYPE_mvhd = Util.getIntegerCodeForString("mvhd"); + public static final int TYPE_trak = Util.getIntegerCodeForString("trak"); + public static final int TYPE_mdia = Util.getIntegerCodeForString("mdia"); + public static final int TYPE_minf = Util.getIntegerCodeForString("minf"); + public static final int TYPE_stbl = Util.getIntegerCodeForString("stbl"); + public static final int TYPE_avcC = Util.getIntegerCodeForString("avcC"); + public static final int TYPE_hvcC = Util.getIntegerCodeForString("hvcC"); + public static final int TYPE_esds = Util.getIntegerCodeForString("esds"); + public static final int TYPE_moof = Util.getIntegerCodeForString("moof"); + public static final int TYPE_traf = Util.getIntegerCodeForString("traf"); + public static final int TYPE_mvex = Util.getIntegerCodeForString("mvex"); + public static final int TYPE_mehd = Util.getIntegerCodeForString("mehd"); + public static final int TYPE_tkhd = Util.getIntegerCodeForString("tkhd"); + public static final int TYPE_edts = Util.getIntegerCodeForString("edts"); + public static final int TYPE_elst = Util.getIntegerCodeForString("elst"); + public static final int TYPE_mdhd = Util.getIntegerCodeForString("mdhd"); + public static final int TYPE_hdlr = Util.getIntegerCodeForString("hdlr"); + public static final int TYPE_stsd = Util.getIntegerCodeForString("stsd"); + public static final int TYPE_pssh = Util.getIntegerCodeForString("pssh"); + public static final int TYPE_sinf = Util.getIntegerCodeForString("sinf"); + public static final int TYPE_schm = Util.getIntegerCodeForString("schm"); + public static final int TYPE_schi = Util.getIntegerCodeForString("schi"); + public static final int TYPE_tenc = Util.getIntegerCodeForString("tenc"); + public static final int TYPE_encv = Util.getIntegerCodeForString("encv"); + public static final int TYPE_enca = Util.getIntegerCodeForString("enca"); + public static final int TYPE_frma = Util.getIntegerCodeForString("frma"); + public static final int TYPE_saiz = Util.getIntegerCodeForString("saiz"); + public static final int TYPE_saio = Util.getIntegerCodeForString("saio"); + public static final int TYPE_uuid = Util.getIntegerCodeForString("uuid"); + public static final int TYPE_senc = Util.getIntegerCodeForString("senc"); + public static final int TYPE_pasp = Util.getIntegerCodeForString("pasp"); + public static final int TYPE_TTML = Util.getIntegerCodeForString("TTML"); + public static final int TYPE_vmhd = Util.getIntegerCodeForString("vmhd"); + public static final int TYPE_mp4v = Util.getIntegerCodeForString("mp4v"); + public static final int TYPE_stts = Util.getIntegerCodeForString("stts"); + public static final int TYPE_stss = Util.getIntegerCodeForString("stss"); + public static final int TYPE_ctts = Util.getIntegerCodeForString("ctts"); + public static final int TYPE_stsc = Util.getIntegerCodeForString("stsc"); + public static final int TYPE_stsz = Util.getIntegerCodeForString("stsz"); + public static final int TYPE_stco = Util.getIntegerCodeForString("stco"); + public static final int TYPE_co64 = Util.getIntegerCodeForString("co64"); + public static final int TYPE_tx3g = Util.getIntegerCodeForString("tx3g"); + public static final int TYPE_wvtt = Util.getIntegerCodeForString("wvtt"); + public static final int TYPE_stpp = Util.getIntegerCodeForString("stpp"); + public static final int TYPE_samr = Util.getIntegerCodeForString("samr"); + public static final int TYPE_sawb = Util.getIntegerCodeForString("sawb"); + public static final int TYPE_udta = Util.getIntegerCodeForString("udta"); + public static final int TYPE_meta = Util.getIntegerCodeForString("meta"); + public static final int TYPE_ilst = Util.getIntegerCodeForString("ilst"); + public static final int TYPE_mean = Util.getIntegerCodeForString("mean"); + public static final int TYPE_name = Util.getIntegerCodeForString("name"); + public static final int TYPE_data = Util.getIntegerCodeForString("data"); + public static final int TYPE_DASHES = Util.getIntegerCodeForString("----"); + + public final int type; + + public Atom(int type) { + this.type = type; + } + + @Override + public String toString() { + return getAtomTypeString(type); + } + + /** + * An MP4 atom that is a leaf. + */ + /* package */ static final class LeafAtom extends Atom { + + /** + * The atom data. + */ + public final ParsableByteArray data; + + /** + * @param type The type of the atom. + * @param data The atom data. + */ + public LeafAtom(int type, ParsableByteArray data) { + super(type); + this.data = data; + } + + } + + /** + * An MP4 atom that has child atoms. + */ + /* package */ static final class ContainerAtom extends Atom { + + public final long endPosition; + public final List leafChildren; + public final List containerChildren; + + /** + * @param type The type of the atom. + * @param endPosition The position of the first byte after the end of the atom. + */ + public ContainerAtom(int type, long endPosition) { + super(type); + this.endPosition = endPosition; + leafChildren = new ArrayList<>(); + containerChildren = new ArrayList<>(); + } + + /** + * Adds a child leaf to this container. + * + * @param atom The child to add. + */ + public void add(LeafAtom atom) { + leafChildren.add(atom); + } + + /** + * Adds a child container to this container. + * + * @param atom The child to add. + */ + public void add(ContainerAtom atom) { + containerChildren.add(atom); + } + + /** + * Gets the child leaf of the given type. + *

+ * If no child exists with the given type then null is returned. If multiple children exist with + * the given type then the first one to have been added is returned. + * + * @param type The leaf type. + * @return The child leaf of the given type, or null if no such child exists. + */ + public LeafAtom getLeafAtomOfType(int type) { + int childrenSize = leafChildren.size(); + for (int i = 0; i < childrenSize; i++) { + LeafAtom atom = leafChildren.get(i); + if (atom.type == type) { + return atom; + } + } + return null; + } + + /** + * Gets the child container of the given type. + *

+ * If no child exists with the given type then null is returned. If multiple children exist with + * the given type then the first one to have been added is returned. + * + * @param type The container type. + * @return The child container of the given type, or null if no such child exists. + */ + public ContainerAtom getContainerAtomOfType(int type) { + int childrenSize = containerChildren.size(); + for (int i = 0; i < childrenSize; i++) { + ContainerAtom atom = containerChildren.get(i); + if (atom.type == type) { + return atom; + } + } + return null; + } + + /** + * Returns the total number of leaf/container children of this atom with the given type. + * + * @param type The type of child atoms to count. + * @return The total number of leaf/container children of this atom with the given type. + */ + public int getChildAtomOfTypeCount(int type) { + int count = 0; + int size = leafChildren.size(); + for (int i = 0; i < size; i++) { + LeafAtom atom = leafChildren.get(i); + if (atom.type == type) { + count++; + } + } + size = containerChildren.size(); + for (int i = 0; i < size; i++) { + ContainerAtom atom = containerChildren.get(i); + if (atom.type == type) { + count++; + } + } + return count; + } + + @Override + public String toString() { + return getAtomTypeString(type) + + " leaves: " + Arrays.toString(leafChildren.toArray(new LeafAtom[0])) + + " containers: " + Arrays.toString(containerChildren.toArray(new ContainerAtom[0])); + } + + } + + /** + * Parses the version number out of the additional integer component of a full atom. + */ + public static int parseFullAtomVersion(int fullAtomInt) { + return 0x000000FF & (fullAtomInt >> 24); + } + + /** + * Parses the atom flags out of the additional integer component of a full atom. + */ + public static int parseFullAtomFlags(int fullAtomInt) { + return 0x00FFFFFF & fullAtomInt; + } + + /** + * Converts a numeric atom type to the corresponding four character string. + * + * @param type The numeric atom type. + * @return The corresponding four character string. + */ + public static String getAtomTypeString(int type) { + return "" + (char) (type >> 24) + + (char) ((type >> 16) & 0xFF) + + (char) ((type >> 8) & 0xFF) + + (char) (type & 0xFF); + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp4/AtomParsers.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp4/AtomParsers.java new file mode 100755 index 000000000..b2306842a --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp4/AtomParsers.java @@ -0,0 +1,1135 @@ +/* + * 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.messenger.exoplayer.extractor.mp4; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.MediaFormat; +import org.telegram.messenger.exoplayer.ParserException; +import org.telegram.messenger.exoplayer.extractor.GaplessInfo; +import org.telegram.messenger.exoplayer.util.Ac3Util; +import org.telegram.messenger.exoplayer.util.Assertions; +import org.telegram.messenger.exoplayer.util.CodecSpecificDataUtil; +import org.telegram.messenger.exoplayer.util.MimeTypes; +import org.telegram.messenger.exoplayer.util.NalUnitUtil; +import org.telegram.messenger.exoplayer.util.ParsableBitArray; +import org.telegram.messenger.exoplayer.util.ParsableByteArray; +import org.telegram.messenger.exoplayer.util.Util; + +import android.util.Pair; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Utility methods for parsing MP4 format atom payloads according to ISO 14496-12. + */ +/* package */ final class AtomParsers { + + /** + * Parses a trak atom (defined in 14496-12). + * + * @param trak Atom to parse. + * @param mvhd Movie header atom, used to get the timescale. + * @param duration The duration in units of the timescale declared in the mvhd atom, or -1 if the + * duration should be parsed from the tkhd atom. + * @param isQuickTime True for QuickTime media. False otherwise. + * @return A {@link Track} instance, or {@code null} if the track's type isn't supported. + */ + public static Track parseTrak(Atom.ContainerAtom trak, Atom.LeafAtom mvhd, long duration, + boolean isQuickTime) { + Atom.ContainerAtom mdia = trak.getContainerAtomOfType(Atom.TYPE_mdia); + int trackType = parseHdlr(mdia.getLeafAtomOfType(Atom.TYPE_hdlr).data); + if (trackType != Track.TYPE_soun && trackType != Track.TYPE_vide && trackType != Track.TYPE_text + && trackType != Track.TYPE_sbtl && trackType != Track.TYPE_subt) { + return null; + } + + TkhdData tkhdData = parseTkhd(trak.getLeafAtomOfType(Atom.TYPE_tkhd).data); + if (duration == -1) { + duration = tkhdData.duration; + } + long movieTimescale = parseMvhd(mvhd.data); + long durationUs; + if (duration == -1) { + durationUs = C.UNKNOWN_TIME_US; + } else { + durationUs = Util.scaleLargeTimestamp(duration, C.MICROS_PER_SECOND, movieTimescale); + } + Atom.ContainerAtom stbl = mdia.getContainerAtomOfType(Atom.TYPE_minf) + .getContainerAtomOfType(Atom.TYPE_stbl); + + Pair mdhdData = parseMdhd(mdia.getLeafAtomOfType(Atom.TYPE_mdhd).data); + StsdData stsdData = parseStsd(stbl.getLeafAtomOfType(Atom.TYPE_stsd).data, tkhdData.id, + durationUs, tkhdData.rotationDegrees, mdhdData.second, isQuickTime); + Pair edtsData = parseEdts(trak.getContainerAtomOfType(Atom.TYPE_edts)); + return stsdData.mediaFormat == null ? null + : new Track(tkhdData.id, trackType, mdhdData.first, movieTimescale, durationUs, + stsdData.mediaFormat, stsdData.trackEncryptionBoxes, stsdData.nalUnitLengthFieldLength, + edtsData.first, edtsData.second); + } + + /** + * Parses an stbl atom (defined in 14496-12). + * + * @param track Track to which this sample table corresponds. + * @param stblAtom stbl (sample table) atom to parse. + * @return Sample table described by the stbl atom. + * @throws ParserException If the resulting sample sequence does not contain a sync sample. + */ + public static TrackSampleTable parseStbl(Track track, Atom.ContainerAtom stblAtom) + throws ParserException { + // Array of sample sizes. + ParsableByteArray stsz = stblAtom.getLeafAtomOfType(Atom.TYPE_stsz).data; + + // Entries are byte offsets of chunks. + boolean chunkOffsetsAreLongs = false; + Atom.LeafAtom chunkOffsetsAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_stco); + if (chunkOffsetsAtom == null) { + chunkOffsetsAreLongs = true; + chunkOffsetsAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_co64); + } + ParsableByteArray chunkOffsets = chunkOffsetsAtom.data; + // Entries are (chunk number, number of samples per chunk, sample description index). + ParsableByteArray stsc = stblAtom.getLeafAtomOfType(Atom.TYPE_stsc).data; + // Entries are (number of samples, timestamp delta between those samples). + ParsableByteArray stts = stblAtom.getLeafAtomOfType(Atom.TYPE_stts).data; + // Entries are the indices of samples that are synchronization samples. + Atom.LeafAtom stssAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_stss); + ParsableByteArray stss = stssAtom != null ? stssAtom.data : null; + // Entries are (number of samples, timestamp offset). + Atom.LeafAtom cttsAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_ctts); + ParsableByteArray ctts = cttsAtom != null ? cttsAtom.data : null; + + // Skip full atom. + stsz.setPosition(Atom.FULL_HEADER_SIZE); + int fixedSampleSize = stsz.readUnsignedIntToInt(); + int sampleCount = stsz.readUnsignedIntToInt(); + if (sampleCount == 0) { + return new TrackSampleTable(new long[0], new int[0], 0, new long[0], new int[0]); + } + + // Prepare to read chunk information. + ChunkIterator chunkIterator = new ChunkIterator(stsc, chunkOffsets, chunkOffsetsAreLongs); + + // Prepare to read sample timestamps. + stts.setPosition(Atom.FULL_HEADER_SIZE); + int remainingTimestampDeltaChanges = stts.readUnsignedIntToInt() - 1; + int remainingSamplesAtTimestampDelta = stts.readUnsignedIntToInt(); + int timestampDeltaInTimeUnits = stts.readUnsignedIntToInt(); + + // Prepare to read sample timestamp offsets, if ctts is present. + int remainingSamplesAtTimestampOffset = 0; + int remainingTimestampOffsetChanges = 0; + int timestampOffset = 0; + if (ctts != null) { + ctts.setPosition(Atom.FULL_HEADER_SIZE); + remainingTimestampOffsetChanges = ctts.readUnsignedIntToInt(); + } + + int nextSynchronizationSampleIndex = -1; + int remainingSynchronizationSamples = 0; + if (stss != null) { + stss.setPosition(Atom.FULL_HEADER_SIZE); + remainingSynchronizationSamples = stss.readUnsignedIntToInt(); + nextSynchronizationSampleIndex = stss.readUnsignedIntToInt() - 1; + } + + // True if we can rechunk fixed-sample-size data. Note that we only rechunk raw audio. + boolean isRechunkable = + fixedSampleSize != 0 + && MimeTypes.AUDIO_RAW.equals(track.mediaFormat.mimeType) + && remainingTimestampDeltaChanges == 0 + && remainingTimestampOffsetChanges == 0 + && remainingSynchronizationSamples == 0; + + long[] offsets; + int[] sizes; + int maximumSize = 0; + long[] timestamps; + int[] flags; + + if (!isRechunkable) { + offsets = new long[sampleCount]; + sizes = new int[sampleCount]; + timestamps = new long[sampleCount]; + flags = new int[sampleCount]; + long timestampTimeUnits = 0; + long offset = 0; + int remainingSamplesInChunk = 0; + + for (int i = 0; i < sampleCount; i++) { + // Advance to the next chunk if necessary. + while (remainingSamplesInChunk == 0) { + Assertions.checkState(chunkIterator.moveNext()); + offset = chunkIterator.offset; + remainingSamplesInChunk = chunkIterator.numSamples; + } + + // Add on the timestamp offset if ctts is present. + if (ctts != null) { + while (remainingSamplesAtTimestampOffset == 0 && remainingTimestampOffsetChanges > 0) { + remainingSamplesAtTimestampOffset = ctts.readUnsignedIntToInt(); + // The BMFF spec (ISO 14496-12) states that sample offsets should be unsigned integers + // in version 0 ctts boxes, however some streams violate the spec and use signed + // integers instead. It's safe to always parse sample offsets as signed integers here, + // because unsigned integers will still be parsed correctly (unless their top bit is + // set, which is never true in practice because sample offsets are always small). + timestampOffset = ctts.readInt(); + remainingTimestampOffsetChanges--; + } + remainingSamplesAtTimestampOffset--; + } + + offsets[i] = offset; + sizes[i] = fixedSampleSize == 0 ? stsz.readUnsignedIntToInt() : fixedSampleSize; + if (sizes[i] > maximumSize) { + maximumSize = sizes[i]; + } + timestamps[i] = timestampTimeUnits + timestampOffset; + + // All samples are synchronization samples if the stss is not present. + flags[i] = stss == null ? C.SAMPLE_FLAG_SYNC : 0; + if (i == nextSynchronizationSampleIndex) { + flags[i] = C.SAMPLE_FLAG_SYNC; + remainingSynchronizationSamples--; + if (remainingSynchronizationSamples > 0) { + nextSynchronizationSampleIndex = stss.readUnsignedIntToInt() - 1; + } + } + + // Add on the duration of this sample. + timestampTimeUnits += timestampDeltaInTimeUnits; + remainingSamplesAtTimestampDelta--; + if (remainingSamplesAtTimestampDelta == 0 && remainingTimestampDeltaChanges > 0) { + remainingSamplesAtTimestampDelta = stts.readUnsignedIntToInt(); + timestampDeltaInTimeUnits = stts.readUnsignedIntToInt(); + remainingTimestampDeltaChanges--; + } + + offset += sizes[i]; + remainingSamplesInChunk--; + } + + // Check all the expected samples have been seen. + Assertions.checkArgument(remainingSynchronizationSamples == 0); + Assertions.checkArgument(remainingSamplesAtTimestampDelta == 0); + Assertions.checkArgument(remainingSamplesInChunk == 0); + Assertions.checkArgument(remainingTimestampDeltaChanges == 0); + Assertions.checkArgument(remainingTimestampOffsetChanges == 0); + } else { + long[] chunkOffsetsBytes = new long[chunkIterator.length]; + int[] chunkSampleCounts = new int[chunkIterator.length]; + while (chunkIterator.moveNext()) { + chunkOffsetsBytes[chunkIterator.index] = chunkIterator.offset; + chunkSampleCounts[chunkIterator.index] = chunkIterator.numSamples; + } + FixedSampleSizeRechunker.Results rechunkedResults = FixedSampleSizeRechunker.rechunk( + fixedSampleSize, chunkOffsetsBytes, chunkSampleCounts, timestampDeltaInTimeUnits); + offsets = rechunkedResults.offsets; + sizes = rechunkedResults.sizes; + maximumSize = rechunkedResults.maximumSize; + timestamps = rechunkedResults.timestamps; + flags = rechunkedResults.flags; + } + + if (track.editListDurations == null) { + Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale); + return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags); + } + + // See the BMFF spec (ISO 14496-12) subsection 8.6.6. Edit lists that truncate audio and + // require prerolling from a sync sample after reordering are not supported. This + // implementation handles simple discarding/delaying of samples. The extractor may place + // further restrictions on what edited streams are playable. + + if (track.editListDurations.length == 1 && track.editListDurations[0] == 0) { + // The current version of the spec leaves handling of an edit with zero segment_duration in + // unfragmented files open to interpretation. We handle this as a special case and include all + // samples in the edit. + for (int i = 0; i < timestamps.length; i++) { + timestamps[i] = Util.scaleLargeTimestamp(timestamps[i] - track.editListMediaTimes[0], + C.MICROS_PER_SECOND, track.timescale); + } + return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags); + } + + // Count the number of samples after applying edits. + int editedSampleCount = 0; + int nextSampleIndex = 0; + boolean copyMetadata = false; + for (int i = 0; i < track.editListDurations.length; i++) { + long mediaTime = track.editListMediaTimes[i]; + if (mediaTime != -1) { + long duration = Util.scaleLargeTimestamp(track.editListDurations[i], track.timescale, + track.movieTimescale); + int startIndex = Util.binarySearchCeil(timestamps, mediaTime, true, true); + int endIndex = Util.binarySearchCeil(timestamps, mediaTime + duration, true, false); + editedSampleCount += endIndex - startIndex; + copyMetadata |= nextSampleIndex != startIndex; + nextSampleIndex = endIndex; + } + } + copyMetadata |= editedSampleCount != sampleCount; + + // Calculate edited sample timestamps and update the corresponding metadata arrays. + long[] editedOffsets = copyMetadata ? new long[editedSampleCount] : offsets; + int[] editedSizes = copyMetadata ? new int[editedSampleCount] : sizes; + int editedMaximumSize = copyMetadata ? 0 : maximumSize; + int[] editedFlags = copyMetadata ? new int[editedSampleCount] : flags; + long[] editedTimestamps = new long[editedSampleCount]; + long pts = 0; + int sampleIndex = 0; + for (int i = 0; i < track.editListDurations.length; i++) { + long mediaTime = track.editListMediaTimes[i]; + long duration = track.editListDurations[i]; + if (mediaTime != -1) { + long endMediaTime = mediaTime + Util.scaleLargeTimestamp(duration, track.timescale, + track.movieTimescale); + int startIndex = Util.binarySearchCeil(timestamps, mediaTime, true, true); + int endIndex = Util.binarySearchCeil(timestamps, endMediaTime, true, false); + if (copyMetadata) { + int count = endIndex - startIndex; + System.arraycopy(offsets, startIndex, editedOffsets, sampleIndex, count); + System.arraycopy(sizes, startIndex, editedSizes, sampleIndex, count); + System.arraycopy(flags, startIndex, editedFlags, sampleIndex, count); + } + for (int j = startIndex; j < endIndex; j++) { + long ptsUs = Util.scaleLargeTimestamp(pts, C.MICROS_PER_SECOND, track.movieTimescale); + long timeInSegmentUs = Util.scaleLargeTimestamp(timestamps[j] - mediaTime, + C.MICROS_PER_SECOND, track.timescale); + editedTimestamps[sampleIndex] = ptsUs + timeInSegmentUs; + if (copyMetadata && editedSizes[sampleIndex] > editedMaximumSize) { + editedMaximumSize = sizes[j]; + } + sampleIndex++; + } + } + pts += duration; + } + + boolean hasSyncSample = false; + for (int i = 0; i < editedFlags.length && !hasSyncSample; i++) { + hasSyncSample |= (editedFlags[i] & C.SAMPLE_FLAG_SYNC) != 0; + } + if (!hasSyncSample) { + throw new ParserException("The edited sample sequence does not contain a sync sample."); + } + + return new TrackSampleTable(editedOffsets, editedSizes, editedMaximumSize, editedTimestamps, + editedFlags); + } + + /** + * Parses a udta atom. + * + * @param udtaAtom The udta (user data) atom to parse. + * @param isQuickTime True for QuickTime media. False otherwise. + * @return Gapless playback information stored in the user data, or {@code null} if not present. + */ + public static GaplessInfo parseUdta(Atom.LeafAtom udtaAtom, boolean isQuickTime) { + if (isQuickTime) { + // Meta boxes are regular boxes rather than full boxes in QuickTime. For now, don't try and + // parse one. + return null; + } + ParsableByteArray udtaData = udtaAtom.data; + udtaData.setPosition(Atom.HEADER_SIZE); + while (udtaData.bytesLeft() >= Atom.HEADER_SIZE) { + int atomSize = udtaData.readInt(); + int atomType = udtaData.readInt(); + if (atomType == Atom.TYPE_meta) { + udtaData.setPosition(udtaData.getPosition() - Atom.HEADER_SIZE); + udtaData.setLimit(udtaData.getPosition() + atomSize); + return parseMetaAtom(udtaData); + } else { + udtaData.skipBytes(atomSize - Atom.HEADER_SIZE); + } + } + return null; + } + + private static GaplessInfo parseMetaAtom(ParsableByteArray data) { + data.skipBytes(Atom.FULL_HEADER_SIZE); + ParsableByteArray ilst = new ParsableByteArray(); + while (data.bytesLeft() >= Atom.HEADER_SIZE) { + int payloadSize = data.readInt() - Atom.HEADER_SIZE; + int atomType = data.readInt(); + if (atomType == Atom.TYPE_ilst) { + ilst.reset(data.data, data.getPosition() + payloadSize); + ilst.setPosition(data.getPosition()); + GaplessInfo gaplessInfo = parseIlst(ilst); + if (gaplessInfo != null) { + return gaplessInfo; + } + } + data.skipBytes(payloadSize); + } + return null; + } + + private static GaplessInfo parseIlst(ParsableByteArray ilst) { + while (ilst.bytesLeft() > 0) { + int position = ilst.getPosition(); + int endPosition = position + ilst.readInt(); + int type = ilst.readInt(); + if (type == Atom.TYPE_DASHES) { + String lastCommentMean = null; + String lastCommentName = null; + String lastCommentData = null; + while (ilst.getPosition() < endPosition) { + int length = ilst.readInt() - Atom.FULL_HEADER_SIZE; + int key = ilst.readInt(); + ilst.skipBytes(4); + if (key == Atom.TYPE_mean) { + lastCommentMean = ilst.readString(length); + } else if (key == Atom.TYPE_name) { + lastCommentName = ilst.readString(length); + } else if (key == Atom.TYPE_data) { + ilst.skipBytes(4); + lastCommentData = ilst.readString(length - 4); + } else { + ilst.skipBytes(length); + } + } + if (lastCommentName != null && lastCommentData != null + && "com.apple.iTunes".equals(lastCommentMean)) { + return GaplessInfo.createFromComment(lastCommentName, lastCommentData); + } + } else { + ilst.setPosition(endPosition); + } + } + return null; + } + + /** + * Parses a mvhd atom (defined in 14496-12), returning the timescale for the movie. + * + * @param mvhd Contents of the mvhd atom to be parsed. + * @return Timescale for the movie. + */ + private static long parseMvhd(ParsableByteArray mvhd) { + mvhd.setPosition(Atom.HEADER_SIZE); + + int fullAtom = mvhd.readInt(); + int version = Atom.parseFullAtomVersion(fullAtom); + + mvhd.skipBytes(version == 0 ? 8 : 16); + + return mvhd.readUnsignedInt(); + } + + /** + * Parses a tkhd atom (defined in 14496-12). + * + * @return An object containing the parsed data. + */ + private static TkhdData parseTkhd(ParsableByteArray tkhd) { + tkhd.setPosition(Atom.HEADER_SIZE); + int fullAtom = tkhd.readInt(); + int version = Atom.parseFullAtomVersion(fullAtom); + + tkhd.skipBytes(version == 0 ? 8 : 16); + int trackId = tkhd.readInt(); + + tkhd.skipBytes(4); + boolean durationUnknown = true; + int durationPosition = tkhd.getPosition(); + int durationByteCount = version == 0 ? 4 : 8; + for (int i = 0; i < durationByteCount; i++) { + if (tkhd.data[durationPosition + i] != -1) { + durationUnknown = false; + break; + } + } + long duration; + if (durationUnknown) { + tkhd.skipBytes(durationByteCount); + duration = -1; + } else { + duration = version == 0 ? tkhd.readUnsignedInt() : tkhd.readUnsignedLongToLong(); + if (duration == 0) { + // 0 duration normally indicates that the file is fully fragmented (i.e. all of the media + // samples are in fragments). Treat as unknown. + duration = -1; + } + } + + tkhd.skipBytes(16); + int a00 = tkhd.readInt(); + int a01 = tkhd.readInt(); + tkhd.skipBytes(4); + int a10 = tkhd.readInt(); + int a11 = tkhd.readInt(); + + int rotationDegrees; + int fixedOne = 65536; + if (a00 == 0 && a01 == fixedOne && a10 == -fixedOne && a11 == 0) { + rotationDegrees = 90; + } else if (a00 == 0 && a01 == -fixedOne && a10 == fixedOne && a11 == 0) { + rotationDegrees = 270; + } else if (a00 == -fixedOne && a01 == 0 && a10 == 0 && a11 == -fixedOne) { + rotationDegrees = 180; + } else { + // Only 0, 90, 180 and 270 are supported. Treat anything else as 0. + rotationDegrees = 0; + } + + return new TkhdData(trackId, duration, rotationDegrees); + } + + /** + * Parses an hdlr atom. + * + * @param hdlr The hdlr atom to parse. + * @return The track type. + */ + private static int parseHdlr(ParsableByteArray hdlr) { + hdlr.setPosition(Atom.FULL_HEADER_SIZE + 4); + return hdlr.readInt(); + } + + /** + * Parses an mdhd atom (defined in 14496-12). + * + * @param mdhd The mdhd atom to parse. + * @return A pair consisting of the media timescale defined as the number of time units that pass + * in one second, and the language code. + */ + private static Pair parseMdhd(ParsableByteArray mdhd) { + mdhd.setPosition(Atom.HEADER_SIZE); + int fullAtom = mdhd.readInt(); + int version = Atom.parseFullAtomVersion(fullAtom); + mdhd.skipBytes(version == 0 ? 8 : 16); + long timescale = mdhd.readUnsignedInt(); + mdhd.skipBytes(version == 0 ? 4 : 8); + int languageCode = mdhd.readUnsignedShort(); + String language = "" + (char) (((languageCode >> 10) & 0x1F) + 0x60) + + (char) (((languageCode >> 5) & 0x1F) + 0x60) + + (char) (((languageCode) & 0x1F) + 0x60); + return Pair.create(timescale, language); + } + + /** + * Parses a stsd atom (defined in 14496-12). + * + * @param stsd The stsd atom to parse. + * @param trackId The track's identifier in its container. + * @param durationUs The duration of the track in microseconds. + * @param rotationDegrees The rotation of the track in degrees. + * @param language The language of the track. + * @param isQuickTime True for QuickTime media. False otherwise. + * @return An object containing the parsed data. + */ + private static StsdData parseStsd(ParsableByteArray stsd, int trackId, long durationUs, + int rotationDegrees, String language, boolean isQuickTime) { + stsd.setPosition(Atom.FULL_HEADER_SIZE); + int numberOfEntries = stsd.readInt(); + StsdData out = new StsdData(numberOfEntries); + for (int i = 0; i < numberOfEntries; i++) { + int childStartPosition = stsd.getPosition(); + int childAtomSize = stsd.readInt(); + Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive"); + int childAtomType = stsd.readInt(); + if (childAtomType == Atom.TYPE_avc1 || childAtomType == Atom.TYPE_avc3 + || childAtomType == Atom.TYPE_encv || childAtomType == Atom.TYPE_mp4v + || childAtomType == Atom.TYPE_hvc1 || childAtomType == Atom.TYPE_hev1 + || childAtomType == Atom.TYPE_s263) { + parseVideoSampleEntry(stsd, childStartPosition, childAtomSize, trackId, durationUs, + rotationDegrees, out, i); + } else if (childAtomType == Atom.TYPE_mp4a || childAtomType == Atom.TYPE_enca + || childAtomType == Atom.TYPE_ac_3 || childAtomType == Atom.TYPE_ec_3 + || childAtomType == Atom.TYPE_dtsc || childAtomType == Atom.TYPE_dtse + || childAtomType == Atom.TYPE_dtsh || childAtomType == Atom.TYPE_dtsl + || childAtomType == Atom.TYPE_samr || childAtomType == Atom.TYPE_sawb + || childAtomType == Atom.TYPE_lpcm || childAtomType == Atom.TYPE_sowt) { + parseAudioSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize, trackId, + durationUs, language, isQuickTime, out, i); + } else if (childAtomType == Atom.TYPE_TTML) { + out.mediaFormat = MediaFormat.createTextFormat(Integer.toString(trackId), + MimeTypes.APPLICATION_TTML, MediaFormat.NO_VALUE, durationUs, language); + } else if (childAtomType == Atom.TYPE_tx3g) { + out.mediaFormat = MediaFormat.createTextFormat(Integer.toString(trackId), + MimeTypes.APPLICATION_TX3G, MediaFormat.NO_VALUE, durationUs, language); + } else if (childAtomType == Atom.TYPE_wvtt) { + out.mediaFormat = MediaFormat.createTextFormat(Integer.toString(trackId), + MimeTypes.APPLICATION_MP4VTT, MediaFormat.NO_VALUE, durationUs, language); + } else if (childAtomType == Atom.TYPE_stpp) { + out.mediaFormat = MediaFormat.createTextFormat(Integer.toString(trackId), + MimeTypes.APPLICATION_TTML, MediaFormat.NO_VALUE, durationUs, language, + 0 /* subsample timing is absolute */); + } + stsd.setPosition(childStartPosition + childAtomSize); + } + return out; + } + + private static void parseVideoSampleEntry(ParsableByteArray parent, int position, int size, + int trackId, long durationUs, int rotationDegrees, StsdData out, int entryIndex) { + parent.setPosition(position + Atom.HEADER_SIZE); + + parent.skipBytes(24); + int width = parent.readUnsignedShort(); + int height = parent.readUnsignedShort(); + boolean pixelWidthHeightRatioFromPasp = false; + float pixelWidthHeightRatio = 1; + parent.skipBytes(50); + + List initializationData = null; + int childPosition = parent.getPosition(); + String mimeType = null; + while (childPosition - position < size) { + parent.setPosition(childPosition); + int childStartPosition = parent.getPosition(); + int childAtomSize = parent.readInt(); + if (childAtomSize == 0 && parent.getPosition() - position == size) { + // Handle optional terminating four zero bytes in MOV files. + break; + } + Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive"); + int childAtomType = parent.readInt(); + if (childAtomType == Atom.TYPE_avcC) { + Assertions.checkState(mimeType == null); + mimeType = MimeTypes.VIDEO_H264; + AvcCData avcCData = parseAvcCFromParent(parent, childStartPosition); + initializationData = avcCData.initializationData; + out.nalUnitLengthFieldLength = avcCData.nalUnitLengthFieldLength; + if (!pixelWidthHeightRatioFromPasp) { + pixelWidthHeightRatio = avcCData.pixelWidthAspectRatio; + } + } else if (childAtomType == Atom.TYPE_hvcC) { + Assertions.checkState(mimeType == null); + mimeType = MimeTypes.VIDEO_H265; + Pair, Integer> hvcCData = parseHvcCFromParent(parent, childStartPosition); + initializationData = hvcCData.first; + out.nalUnitLengthFieldLength = hvcCData.second; + } else if (childAtomType == Atom.TYPE_d263) { + Assertions.checkState(mimeType == null); + mimeType = MimeTypes.VIDEO_H263; + } else if (childAtomType == Atom.TYPE_esds) { + Assertions.checkState(mimeType == null); + Pair mimeTypeAndInitializationData = + parseEsdsFromParent(parent, childStartPosition); + mimeType = mimeTypeAndInitializationData.first; + initializationData = Collections.singletonList(mimeTypeAndInitializationData.second); + } else if (childAtomType == Atom.TYPE_sinf) { + out.trackEncryptionBoxes[entryIndex] = + parseSinfFromParent(parent, childStartPosition, childAtomSize); + } else if (childAtomType == Atom.TYPE_pasp) { + pixelWidthHeightRatio = parsePaspFromParent(parent, childStartPosition); + pixelWidthHeightRatioFromPasp = true; + } + childPosition += childAtomSize; + } + + // If the media type was not recognized, ignore the track. + if (mimeType == null) { + return; + } + + out.mediaFormat = MediaFormat.createVideoFormat(Integer.toString(trackId), mimeType, + MediaFormat.NO_VALUE, MediaFormat.NO_VALUE, durationUs, width, height, initializationData, + rotationDegrees, pixelWidthHeightRatio); + } + + private static AvcCData parseAvcCFromParent(ParsableByteArray parent, int position) { + parent.setPosition(position + Atom.HEADER_SIZE + 4); + // Start of the AVCDecoderConfigurationRecord (defined in 14496-15) + int nalUnitLengthFieldLength = (parent.readUnsignedByte() & 0x3) + 1; + if (nalUnitLengthFieldLength == 3) { + throw new IllegalStateException(); + } + List initializationData = new ArrayList<>(); + float pixelWidthAspectRatio = 1; + int numSequenceParameterSets = parent.readUnsignedByte() & 0x1F; + for (int j = 0; j < numSequenceParameterSets; j++) { + initializationData.add(NalUnitUtil.parseChildNalUnit(parent)); + } + int numPictureParameterSets = parent.readUnsignedByte(); + for (int j = 0; j < numPictureParameterSets; j++) { + initializationData.add(NalUnitUtil.parseChildNalUnit(parent)); + } + + if (numSequenceParameterSets > 0) { + // Parse the first sequence parameter set to obtain pixelWidthAspectRatio. + ParsableBitArray spsDataBitArray = new ParsableBitArray(initializationData.get(0)); + // Skip the NAL header consisting of the nalUnitLengthField and the type (1 byte). + spsDataBitArray.setPosition(8 * (nalUnitLengthFieldLength + 1)); + pixelWidthAspectRatio = NalUnitUtil.parseSpsNalUnit(spsDataBitArray).pixelWidthAspectRatio; + } + + return new AvcCData(initializationData, nalUnitLengthFieldLength, pixelWidthAspectRatio); + } + + private static Pair, Integer> parseHvcCFromParent(ParsableByteArray parent, + int position) { + // Skip to the NAL unit length size field. + parent.setPosition(position + Atom.HEADER_SIZE + 21); + int lengthSizeMinusOne = parent.readUnsignedByte() & 0x03; + + // Calculate the combined size of all VPS/SPS/PPS bitstreams. + int numberOfArrays = parent.readUnsignedByte(); + int csdLength = 0; + int csdStartPosition = parent.getPosition(); + for (int i = 0; i < numberOfArrays; i++) { + parent.skipBytes(1); // completeness (1), nal_unit_type (7) + int numberOfNalUnits = parent.readUnsignedShort(); + for (int j = 0; j < numberOfNalUnits; j++) { + int nalUnitLength = parent.readUnsignedShort(); + csdLength += 4 + nalUnitLength; // Start code and NAL unit. + parent.skipBytes(nalUnitLength); + } + } + + // Concatenate the codec-specific data into a single buffer. + parent.setPosition(csdStartPosition); + byte[] buffer = new byte[csdLength]; + int bufferPosition = 0; + for (int i = 0; i < numberOfArrays; i++) { + parent.skipBytes(1); // completeness (1), nal_unit_type (7) + int numberOfNalUnits = parent.readUnsignedShort(); + for (int j = 0; j < numberOfNalUnits; j++) { + int nalUnitLength = parent.readUnsignedShort(); + System.arraycopy(NalUnitUtil.NAL_START_CODE, 0, buffer, bufferPosition, + NalUnitUtil.NAL_START_CODE.length); + bufferPosition += NalUnitUtil.NAL_START_CODE.length; + System.arraycopy(parent.data, parent.getPosition(), buffer, bufferPosition, nalUnitLength); + bufferPosition += nalUnitLength; + parent.skipBytes(nalUnitLength); + } + } + + List initializationData = csdLength == 0 ? null : Collections.singletonList(buffer); + return Pair.create(initializationData, lengthSizeMinusOne + 1); + } + + /** + * Parses the edts atom (defined in 14496-12 subsection 8.6.5). + * + * @param edtsAtom edts (edit box) atom to parse. + * @return Pair of edit list durations and edit list media times, or a pair of nulls if they are + * not present. + */ + private static Pair parseEdts(Atom.ContainerAtom edtsAtom) { + Atom.LeafAtom elst; + if (edtsAtom == null || (elst = edtsAtom.getLeafAtomOfType(Atom.TYPE_elst)) == null) { + return Pair.create(null, null); + } + ParsableByteArray elstData = elst.data; + elstData.setPosition(Atom.HEADER_SIZE); + int fullAtom = elstData.readInt(); + int version = Atom.parseFullAtomVersion(fullAtom); + int entryCount = elstData.readUnsignedIntToInt(); + long[] editListDurations = new long[entryCount]; + long[] editListMediaTimes = new long[entryCount]; + for (int i = 0; i < entryCount; i++) { + editListDurations[i] = + version == 1 ? elstData.readUnsignedLongToLong() : elstData.readUnsignedInt(); + editListMediaTimes[i] = version == 1 ? elstData.readLong() : elstData.readInt(); + int mediaRateInteger = elstData.readShort(); + if (mediaRateInteger != 1) { + // The extractor does not handle dwell edits (mediaRateInteger == 0). + throw new IllegalArgumentException("Unsupported media rate."); + } + elstData.skipBytes(2); + } + return Pair.create(editListDurations, editListMediaTimes); + } + + private static TrackEncryptionBox parseSinfFromParent(ParsableByteArray parent, int position, + int size) { + int childPosition = position + Atom.HEADER_SIZE; + + TrackEncryptionBox trackEncryptionBox = null; + while (childPosition - position < size) { + parent.setPosition(childPosition); + int childAtomSize = parent.readInt(); + int childAtomType = parent.readInt(); + if (childAtomType == Atom.TYPE_frma) { + parent.readInt(); // dataFormat. + } else if (childAtomType == Atom.TYPE_schm) { + parent.skipBytes(4); + parent.readInt(); // schemeType. Expect cenc + parent.readInt(); // schemeVersion. Expect 0x00010000 + } else if (childAtomType == Atom.TYPE_schi) { + trackEncryptionBox = parseSchiFromParent(parent, childPosition, childAtomSize); + } + childPosition += childAtomSize; + } + + return trackEncryptionBox; + } + + private static float parsePaspFromParent(ParsableByteArray parent, int position) { + parent.setPosition(position + Atom.HEADER_SIZE); + int hSpacing = parent.readUnsignedIntToInt(); + int vSpacing = parent.readUnsignedIntToInt(); + return (float) hSpacing / vSpacing; + } + + private static TrackEncryptionBox parseSchiFromParent(ParsableByteArray parent, int position, + int size) { + int childPosition = position + Atom.HEADER_SIZE; + while (childPosition - position < size) { + parent.setPosition(childPosition); + int childAtomSize = parent.readInt(); + int childAtomType = parent.readInt(); + if (childAtomType == Atom.TYPE_tenc) { + parent.skipBytes(4); + int firstInt = parent.readInt(); + boolean defaultIsEncrypted = (firstInt >> 8) == 1; + int defaultInitVectorSize = firstInt & 0xFF; + byte[] defaultKeyId = new byte[16]; + parent.readBytes(defaultKeyId, 0, defaultKeyId.length); + return new TrackEncryptionBox(defaultIsEncrypted, defaultInitVectorSize, defaultKeyId); + } + childPosition += childAtomSize; + } + return null; + } + + private static void parseAudioSampleEntry(ParsableByteArray parent, int atomType, int position, + int size, int trackId, long durationUs, String language, boolean isQuickTime, StsdData out, + int entryIndex) { + parent.setPosition(position + Atom.HEADER_SIZE); + + int quickTimeSoundDescriptionVersion = 0; + if (isQuickTime) { + parent.skipBytes(8); + quickTimeSoundDescriptionVersion = parent.readUnsignedShort(); + parent.skipBytes(6); + } else { + parent.skipBytes(16); + } + + int channelCount; + int sampleRate; + + if (quickTimeSoundDescriptionVersion == 0 || quickTimeSoundDescriptionVersion == 1) { + channelCount = parent.readUnsignedShort(); + parent.skipBytes(6); // sampleSize, compressionId, packetSize. + sampleRate = parent.readUnsignedFixedPoint1616(); + + if (quickTimeSoundDescriptionVersion == 1) { + parent.skipBytes(16); + } + } else if (quickTimeSoundDescriptionVersion == 2) { + parent.skipBytes(16); // always[3,16,Minus2,0,65536], sizeOfStructOnly + + sampleRate = (int) Math.round(parent.readDouble()); + channelCount = parent.readUnsignedIntToInt(); + + // Skip always7F000000, sampleSize, formatSpecificFlags, constBytesPerAudioPacket, + // constLPCMFramesPerAudioPacket. + parent.skipBytes(20); + } else { + // Unsupported version. + return; + } + + // If the atom type determines a MIME type, set it immediately. + String mimeType = null; + if (atomType == Atom.TYPE_ac_3) { + mimeType = MimeTypes.AUDIO_AC3; + } else if (atomType == Atom.TYPE_ec_3) { + mimeType = MimeTypes.AUDIO_E_AC3; + } else if (atomType == Atom.TYPE_dtsc) { + mimeType = MimeTypes.AUDIO_DTS; + } else if (atomType == Atom.TYPE_dtsh || atomType == Atom.TYPE_dtsl) { + mimeType = MimeTypes.AUDIO_DTS_HD; + } else if (atomType == Atom.TYPE_dtse) { + mimeType = MimeTypes.AUDIO_DTS_EXPRESS; + } else if (atomType == Atom.TYPE_samr) { + mimeType = MimeTypes.AUDIO_AMR_NB; + } else if (atomType == Atom.TYPE_sawb) { + mimeType = MimeTypes.AUDIO_AMR_WB; + } else if (atomType == Atom.TYPE_lpcm || atomType == Atom.TYPE_sowt) { + mimeType = MimeTypes.AUDIO_RAW; + } + + byte[] initializationData = null; + int childAtomPosition = parent.getPosition(); + while (childAtomPosition - position < size) { + parent.setPosition(childAtomPosition); + int childAtomSize = parent.readInt(); + Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive"); + int childAtomType = parent.readInt(); + if (atomType == Atom.TYPE_mp4a || atomType == Atom.TYPE_enca) { + int esdsAtomPosition = -1; + if (childAtomType == Atom.TYPE_esds) { + esdsAtomPosition = childAtomPosition; + } else if (isQuickTime && childAtomType == Atom.TYPE_wave) { + esdsAtomPosition = findEsdsPosition(parent, childAtomPosition, childAtomSize); + } + if (esdsAtomPosition != -1) { + Pair mimeTypeAndInitializationData = + parseEsdsFromParent(parent, esdsAtomPosition); + mimeType = mimeTypeAndInitializationData.first; + initializationData = mimeTypeAndInitializationData.second; + if (MimeTypes.AUDIO_AAC.equals(mimeType)) { + // TODO: Do we really need to do this? See [Internal: b/10903778] + // Update sampleRate and channelCount from the AudioSpecificConfig initialization data. + Pair audioSpecificConfig = + CodecSpecificDataUtil.parseAacAudioSpecificConfig(initializationData); + sampleRate = audioSpecificConfig.first; + channelCount = audioSpecificConfig.second; + } + } else if (childAtomType == Atom.TYPE_sinf) { + out.trackEncryptionBoxes[entryIndex] = parseSinfFromParent(parent, childAtomPosition, + childAtomSize); + } + } else if (atomType == Atom.TYPE_ac_3 && childAtomType == Atom.TYPE_dac3) { + // TODO: Choose the right AC-3 track based on the contents of dac3/dec3. + // TODO: Add support for encryption (by setting out.trackEncryptionBoxes). + parent.setPosition(Atom.HEADER_SIZE + childAtomPosition); + out.mediaFormat = Ac3Util.parseAc3AnnexFFormat(parent, Integer.toString(trackId), + durationUs, language); + return; + } else if (atomType == Atom.TYPE_ec_3 && childAtomType == Atom.TYPE_dec3) { + parent.setPosition(Atom.HEADER_SIZE + childAtomPosition); + out.mediaFormat = Ac3Util.parseEAc3AnnexFFormat(parent, Integer.toString(trackId), + durationUs, language); + return; + } else if ((atomType == Atom.TYPE_dtsc || atomType == Atom.TYPE_dtse + || atomType == Atom.TYPE_dtsh || atomType == Atom.TYPE_dtsl) + && childAtomType == Atom.TYPE_ddts) { + out.mediaFormat = MediaFormat.createAudioFormat(Integer.toString(trackId), mimeType, + MediaFormat.NO_VALUE, MediaFormat.NO_VALUE, durationUs, channelCount, sampleRate, null, + language); + return; + } + childAtomPosition += childAtomSize; + } + + // If the media type was not recognized, ignore the track. + if (mimeType == null) { + return; + } + + // TODO: Determine the correct PCM encoding. + int pcmEncoding = MimeTypes.AUDIO_RAW.equals(mimeType) ? C.ENCODING_PCM_16BIT + : MediaFormat.NO_VALUE; + + out.mediaFormat = MediaFormat.createAudioFormat(Integer.toString(trackId), mimeType, + MediaFormat.NO_VALUE, MediaFormat.NO_VALUE, durationUs, channelCount, sampleRate, + initializationData == null ? null : Collections.singletonList(initializationData), + language, pcmEncoding); + } + + /** Returns the position of the esds box within a parent, or -1 if no esds box is found */ + private static int findEsdsPosition(ParsableByteArray parent, int position, int size) { + int childAtomPosition = parent.getPosition(); + while (childAtomPosition - position < size) { + parent.setPosition(childAtomPosition); + int childAtomSize = parent.readInt(); + Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive"); + int childType = parent.readInt(); + if (childType == Atom.TYPE_esds) { + return childAtomPosition; + } + childAtomPosition += childAtomSize; + } + return -1; + } + + /** Returns codec-specific initialization data contained in an esds box. */ + private static Pair parseEsdsFromParent(ParsableByteArray parent, int position) { + parent.setPosition(position + Atom.HEADER_SIZE + 4); + // Start of the ES_Descriptor (defined in 14496-1) + parent.skipBytes(1); // ES_Descriptor tag + parseExpandableClassSize(parent); + parent.skipBytes(2); // ES_ID + + int flags = parent.readUnsignedByte(); + if ((flags & 0x80 /* streamDependenceFlag */) != 0) { + parent.skipBytes(2); + } + if ((flags & 0x40 /* URL_Flag */) != 0) { + parent.skipBytes(parent.readUnsignedShort()); + } + if ((flags & 0x20 /* OCRstreamFlag */) != 0) { + parent.skipBytes(2); + } + + // Start of the DecoderConfigDescriptor (defined in 14496-1) + parent.skipBytes(1); // DecoderConfigDescriptor tag + parseExpandableClassSize(parent); + + // Set the MIME type based on the object type indication (14496-1 table 5). + int objectTypeIndication = parent.readUnsignedByte(); + String mimeType; + switch (objectTypeIndication) { + case 0x6B: + mimeType = MimeTypes.AUDIO_MPEG; + return Pair.create(mimeType, null); + case 0x20: + mimeType = MimeTypes.VIDEO_MP4V; + break; + case 0x21: + mimeType = MimeTypes.VIDEO_H264; + break; + case 0x23: + mimeType = MimeTypes.VIDEO_H265; + break; + case 0x40: + case 0x66: + case 0x67: + case 0x68: + mimeType = MimeTypes.AUDIO_AAC; + break; + case 0xA5: + mimeType = MimeTypes.AUDIO_AC3; + break; + case 0xA6: + mimeType = MimeTypes.AUDIO_E_AC3; + break; + case 0xA9: + case 0xAC: + mimeType = MimeTypes.AUDIO_DTS; + return Pair.create(mimeType, null); + case 0xAA: + case 0xAB: + mimeType = MimeTypes.AUDIO_DTS_HD; + return Pair.create(mimeType, null); + default: + mimeType = null; + break; + } + + parent.skipBytes(12); + + // Start of the AudioSpecificConfig. + parent.skipBytes(1); // AudioSpecificConfig tag + int initializationDataSize = parseExpandableClassSize(parent); + byte[] initializationData = new byte[initializationDataSize]; + parent.readBytes(initializationData, 0, initializationDataSize); + return Pair.create(mimeType, initializationData); + } + + /** Parses the size of an expandable class, as specified by ISO 14496-1 subsection 8.3.3. */ + private static int parseExpandableClassSize(ParsableByteArray data) { + int currentByte = data.readUnsignedByte(); + int size = currentByte & 0x7F; + while ((currentByte & 0x80) == 0x80) { + currentByte = data.readUnsignedByte(); + size = (size << 7) | (currentByte & 0x7F); + } + return size; + } + + private AtomParsers() { + // Prevent instantiation. + } + + private static final class ChunkIterator { + + public final int length; + + public int index; + public int numSamples; + public long offset; + + private final boolean chunkOffsetsAreLongs; + private final ParsableByteArray chunkOffsets; + private final ParsableByteArray stsc; + + private int nextSamplesPerChunkChangeIndex; + private int remainingSamplesPerChunkChanges; + + public ChunkIterator(ParsableByteArray stsc, ParsableByteArray chunkOffsets, + boolean chunkOffsetsAreLongs) { + this.stsc = stsc; + this.chunkOffsets = chunkOffsets; + this.chunkOffsetsAreLongs = chunkOffsetsAreLongs; + chunkOffsets.setPosition(Atom.FULL_HEADER_SIZE); + length = chunkOffsets.readUnsignedIntToInt(); + stsc.setPosition(Atom.FULL_HEADER_SIZE); + remainingSamplesPerChunkChanges = stsc.readUnsignedIntToInt(); + Assertions.checkState(stsc.readInt() == 1, "first_chunk must be 1"); + index = -1; + } + + public boolean moveNext() { + if (++index == length) { + return false; + } + offset = chunkOffsetsAreLongs ? chunkOffsets.readUnsignedLongToLong() + : chunkOffsets.readUnsignedInt(); + if (index == nextSamplesPerChunkChangeIndex) { + numSamples = stsc.readUnsignedIntToInt(); + stsc.skipBytes(4); // Skip sample_description_index + nextSamplesPerChunkChangeIndex = --remainingSamplesPerChunkChanges > 0 + ? (stsc.readUnsignedIntToInt() - 1) : -1; + } + return true; + } + + } + + /** + * Holds data parsed from a tkhd atom. + */ + private static final class TkhdData { + + private final int id; + private final long duration; + private final int rotationDegrees; + + public TkhdData(int id, long duration, int rotationDegrees) { + this.id = id; + this.duration = duration; + this.rotationDegrees = rotationDegrees; + } + + } + + /** + * Holds data parsed from an stsd atom and its children. + */ + private static final class StsdData { + + public final TrackEncryptionBox[] trackEncryptionBoxes; + + public MediaFormat mediaFormat; + public int nalUnitLengthFieldLength; + + public StsdData(int numberOfEntries) { + trackEncryptionBoxes = new TrackEncryptionBox[numberOfEntries]; + nalUnitLengthFieldLength = -1; + } + + } + + /** + * Holds data parsed from an AvcC atom. + */ + private static final class AvcCData { + + public final List initializationData; + public final int nalUnitLengthFieldLength; + public final float pixelWidthAspectRatio; + + public AvcCData(List initializationData, int nalUnitLengthFieldLength, + float pixelWidthAspectRatio) { + this.initializationData = initializationData; + this.nalUnitLengthFieldLength = nalUnitLengthFieldLength; + this.pixelWidthAspectRatio = pixelWidthAspectRatio; + } + + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp4/DefaultSampleValues.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp4/DefaultSampleValues.java new file mode 100755 index 000000000..3240c47a9 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp4/DefaultSampleValues.java @@ -0,0 +1,32 @@ +/* + * 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.messenger.exoplayer.extractor.mp4; + +/* package */ final class DefaultSampleValues { + + public final int sampleDescriptionIndex; + public final int duration; + public final int size; + public final int flags; + + public DefaultSampleValues(int sampleDescriptionIndex, int duration, int size, int flags) { + this.sampleDescriptionIndex = sampleDescriptionIndex; + this.duration = duration; + this.size = size; + this.flags = flags; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp4/FixedSampleSizeRechunker.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp4/FixedSampleSizeRechunker.java new file mode 100755 index 000000000..20ec3ed07 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp4/FixedSampleSizeRechunker.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2016 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.messenger.exoplayer.extractor.mp4; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.util.Util; + +/** Utilities for rechunking fixed sample size data (e.g., uncompressed audio). */ +/* package */ final class FixedSampleSizeRechunker { + + /** Maximum number of bytes for each buffer in rechunked output. */ + private static final int MAX_SAMPLE_SIZE = 8 * 1024; + + /** Stores the results (new chunk information) of a rechunking operation. */ + public static final class Results { + public final long[] offsets; + public final int[] sizes; + public final int maximumSize; + public final long[] timestamps; + public final int[] flags; + + public Results(long[] offsets, int[] sizes, int maximumSize, long[] timestamps, int[] flags) { + this.offsets = offsets; + this.sizes = sizes; + this.maximumSize = maximumSize; + this.timestamps = timestamps; + this.flags = flags; + } + } + + /** + * Rechunk the given fixed sample size input to produce a new sequence of samples. + * + * @param fixedSampleSize Size in bytes of each sample. + * @param chunkOffsets Chunk offsets in the MP4 stream to rechunk. + * @param chunkSampleCounts Sample counts for each of the MP4 stream's chunks. + * @param timestampDeltaInTimeUnits Timestamp delta between each sample in time units. + */ + public static Results rechunk( + int fixedSampleSize, + long[] chunkOffsets, + int[] chunkSampleCounts, + long timestampDeltaInTimeUnits) { + int maxSampleCount = MAX_SAMPLE_SIZE / fixedSampleSize; + + // Count the number of new, rechunked buffers. + int rechunkedSampleCount = 0; + for (int chunkSampleCount : chunkSampleCounts) { + rechunkedSampleCount += Util.ceilDivide(chunkSampleCount, maxSampleCount); + } + + long[] offsets = new long[rechunkedSampleCount]; + int[] sizes = new int[rechunkedSampleCount]; + int maximumSize = 0; + long[] timestamps = new long[rechunkedSampleCount]; + int[] flags = new int[rechunkedSampleCount]; + + int originalSampleIndex = 0; + int newSampleIndex = 0; + for (int chunkIndex = 0; chunkIndex < chunkSampleCounts.length; chunkIndex++) { + int chunkSamplesRemaining = chunkSampleCounts[chunkIndex]; + long sampleOffset = chunkOffsets[chunkIndex]; + + while (chunkSamplesRemaining > 0) { + int bufferSampleCount = Math.min(maxSampleCount, chunkSamplesRemaining); + + offsets[newSampleIndex] = sampleOffset; + sizes[newSampleIndex] = fixedSampleSize * bufferSampleCount; + maximumSize = Math.max(maximumSize, sizes[newSampleIndex]); + timestamps[newSampleIndex] = (timestampDeltaInTimeUnits * originalSampleIndex); + flags[newSampleIndex] = C.SAMPLE_FLAG_SYNC; + + sampleOffset += sizes[newSampleIndex]; + originalSampleIndex += bufferSampleCount; + + chunkSamplesRemaining -= bufferSampleCount; + newSampleIndex++; + } + } + + return new Results(offsets, sizes, maximumSize, timestamps, flags); + } +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp4/FragmentedMp4Extractor.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp4/FragmentedMp4Extractor.java new file mode 100755 index 000000000..03bbaae3e --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp4/FragmentedMp4Extractor.java @@ -0,0 +1,1019 @@ +/* + * 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.messenger.exoplayer.extractor.mp4; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.ParserException; +import org.telegram.messenger.exoplayer.drm.DrmInitData; +import org.telegram.messenger.exoplayer.drm.DrmInitData.SchemeInitData; +import org.telegram.messenger.exoplayer.extractor.ChunkIndex; +import org.telegram.messenger.exoplayer.extractor.Extractor; +import org.telegram.messenger.exoplayer.extractor.ExtractorInput; +import org.telegram.messenger.exoplayer.extractor.ExtractorOutput; +import org.telegram.messenger.exoplayer.extractor.PositionHolder; +import org.telegram.messenger.exoplayer.extractor.SeekMap; +import org.telegram.messenger.exoplayer.extractor.TrackOutput; +import org.telegram.messenger.exoplayer.extractor.mp4.Atom.ContainerAtom; +import org.telegram.messenger.exoplayer.extractor.mp4.Atom.LeafAtom; +import org.telegram.messenger.exoplayer.util.Assertions; +import org.telegram.messenger.exoplayer.util.MimeTypes; +import org.telegram.messenger.exoplayer.util.NalUnitUtil; +import org.telegram.messenger.exoplayer.util.ParsableByteArray; +import org.telegram.messenger.exoplayer.util.Util; + +import android.util.Log; +import android.util.Pair; +import android.util.SparseArray; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.Stack; +import java.util.UUID; + +/** + * Facilitates the extraction of data from the fragmented mp4 container format. + */ +public final class FragmentedMp4Extractor implements Extractor { + + private static final String TAG = "FragmentedMp4Extractor"; + + /** + * Flag to work around an issue in some video streams where every frame is marked as a sync frame. + * The workaround overrides the sync frame flags in the stream, forcing them to false except for + * the first sample in each segment. + *

+ * This flag does nothing if the stream is not a video stream. + */ + public static final int FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME = 1; + + /** + * Flag to ignore any tfdt boxes in the stream. + */ + public static final int FLAG_WORKAROUND_IGNORE_TFDT_BOX = 2; + + /** + * Flag to indicate that the {@link Track} was sideloaded, instead of being declared by the MP4 + * container. + */ + private static final int FLAG_SIDELOADED = 4; + + private static final byte[] PIFF_SAMPLE_ENCRYPTION_BOX_EXTENDED_TYPE = + new byte[] {-94, 57, 79, 82, 90, -101, 79, 20, -94, 68, 108, 66, 124, 100, -115, -12}; + + // Parser states. + private static final int STATE_READING_ATOM_HEADER = 0; + private static final int STATE_READING_ATOM_PAYLOAD = 1; + private static final int STATE_READING_ENCRYPTION_DATA = 2; + private static final int STATE_READING_SAMPLE_START = 3; + private static final int STATE_READING_SAMPLE_CONTINUE = 4; + + // Workarounds. + private final int flags; + private final Track sideloadedTrack; + + // Track-linked data bundle, accessible as a whole through trackID. + private final SparseArray trackBundles; + + // Temporary arrays. + private final ParsableByteArray nalStartCode; + private final ParsableByteArray nalLength; + private final ParsableByteArray encryptionSignalByte; + + // Parser state. + private final ParsableByteArray atomHeader; + private final byte[] extendedTypeScratch; + private final Stack containerAtoms; + + private int parserState; + private int atomType; + private long atomSize; + private int atomHeaderBytesRead; + private ParsableByteArray atomData; + private long endOfMdatPosition; + + private TrackBundle currentTrackBundle; + private int sampleSize; + private int sampleBytesWritten; + private int sampleCurrentNalBytesRemaining; + + // Extractor output. + private ExtractorOutput extractorOutput; + + // Whether extractorOutput.seekMap has been invoked. + private boolean haveOutputSeekMap; + + public FragmentedMp4Extractor() { + this(0); + } + + /** + * @param flags Flags to allow parsing of faulty streams. + */ + public FragmentedMp4Extractor(int flags) { + this(flags, null); + } + + /** + * @param flags Flags to allow parsing of faulty streams. + * @param sideloadedTrack Sideloaded track information, in the case that the extractor + * will not receive a moov box in the input data. + */ + public FragmentedMp4Extractor(int flags, Track sideloadedTrack) { + this.sideloadedTrack = sideloadedTrack; + this.flags = flags | (sideloadedTrack != null ? FLAG_SIDELOADED : 0); + atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE); + nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE); + nalLength = new ParsableByteArray(4); + encryptionSignalByte = new ParsableByteArray(1); + extendedTypeScratch = new byte[16]; + containerAtoms = new Stack<>(); + trackBundles = new SparseArray<>(); + enterReadingAtomHeaderState(); + } + + @Override + public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + return Sniffer.sniffFragmented(input); + } + + @Override + public void init(ExtractorOutput output) { + extractorOutput = output; + if (sideloadedTrack != null) { + TrackBundle bundle = new TrackBundle(output.track(0)); + bundle.init(sideloadedTrack, new DefaultSampleValues(0, 0, 0, 0)); + trackBundles.put(0, bundle); + extractorOutput.endTracks(); + } + } + + @Override + public void seek() { + int trackCount = trackBundles.size(); + for (int i = 0; i < trackCount; i++) { + trackBundles.valueAt(i).reset(); + } + containerAtoms.clear(); + enterReadingAtomHeaderState(); + } + + @Override + public void release() { + // Do nothing + } + + @Override + public int read(ExtractorInput input, PositionHolder seekPosition) + throws IOException, InterruptedException { + while (true) { + switch (parserState) { + case STATE_READING_ATOM_HEADER: + if (!readAtomHeader(input)) { + return Extractor.RESULT_END_OF_INPUT; + } + break; + case STATE_READING_ATOM_PAYLOAD: + readAtomPayload(input); + break; + case STATE_READING_ENCRYPTION_DATA: + readEncryptionData(input); + break; + default: + if (readSample(input)) { + return RESULT_CONTINUE; + } + } + } + } + + private void enterReadingAtomHeaderState() { + parserState = STATE_READING_ATOM_HEADER; + atomHeaderBytesRead = 0; + } + + private boolean readAtomHeader(ExtractorInput input) throws IOException, InterruptedException { + if (atomHeaderBytesRead == 0) { + // Read the standard length atom header. + if (!input.readFully(atomHeader.data, 0, Atom.HEADER_SIZE, true)) { + return false; + } + atomHeaderBytesRead = Atom.HEADER_SIZE; + atomHeader.setPosition(0); + atomSize = atomHeader.readUnsignedInt(); + atomType = atomHeader.readInt(); + } + + if (atomSize == Atom.LONG_SIZE_PREFIX) { + // Read the extended atom size. + int headerBytesRemaining = Atom.LONG_HEADER_SIZE - Atom.HEADER_SIZE; + input.readFully(atomHeader.data, Atom.HEADER_SIZE, headerBytesRemaining); + atomHeaderBytesRead += headerBytesRemaining; + atomSize = atomHeader.readUnsignedLongToLong(); + } + + long atomPosition = input.getPosition() - atomHeaderBytesRead; + if (atomType == Atom.TYPE_moof) { + // The data positions may be updated when parsing the tfhd/trun. + int trackCount = trackBundles.size(); + for (int i = 0; i < trackCount; i++) { + TrackFragment fragment = trackBundles.valueAt(i).fragment; + fragment.auxiliaryDataPosition = atomPosition; + fragment.dataPosition = atomPosition; + } + } + + if (atomType == Atom.TYPE_mdat) { + currentTrackBundle = null; + endOfMdatPosition = atomPosition + atomSize; + if (!haveOutputSeekMap) { + extractorOutput.seekMap(SeekMap.UNSEEKABLE); + haveOutputSeekMap = true; + } + parserState = STATE_READING_ENCRYPTION_DATA; + return true; + } + + if (shouldParseContainerAtom(atomType)) { + long endPosition = input.getPosition() + atomSize - Atom.HEADER_SIZE; + containerAtoms.add(new ContainerAtom(atomType, endPosition)); + if (atomSize == atomHeaderBytesRead) { + processAtomEnded(endPosition); + } else { + // Start reading the first child atom. + enterReadingAtomHeaderState(); + } + } else if (shouldParseLeafAtom(atomType)) { + if (atomHeaderBytesRead != Atom.HEADER_SIZE) { + throw new ParserException("Leaf atom defines extended atom size (unsupported)."); + } + if (atomSize > Integer.MAX_VALUE) { + throw new ParserException("Leaf atom with length > 2147483647 (unsupported)."); + } + atomData = new ParsableByteArray((int) atomSize); + System.arraycopy(atomHeader.data, 0, atomData.data, 0, Atom.HEADER_SIZE); + parserState = STATE_READING_ATOM_PAYLOAD; + } else { + if (atomSize > Integer.MAX_VALUE) { + throw new ParserException("Skipping atom with length > 2147483647 (unsupported)."); + } + atomData = null; + parserState = STATE_READING_ATOM_PAYLOAD; + } + + return true; + } + + private void readAtomPayload(ExtractorInput input) throws IOException, InterruptedException { + int atomPayloadSize = (int) atomSize - atomHeaderBytesRead; + if (atomData != null) { + input.readFully(atomData.data, Atom.HEADER_SIZE, atomPayloadSize); + onLeafAtomRead(new LeafAtom(atomType, atomData), input.getPosition()); + } else { + input.skipFully(atomPayloadSize); + } + processAtomEnded(input.getPosition()); + } + + private void processAtomEnded(long atomEndPosition) throws ParserException { + while (!containerAtoms.isEmpty() && containerAtoms.peek().endPosition == atomEndPosition) { + onContainerAtomRead(containerAtoms.pop()); + } + enterReadingAtomHeaderState(); + } + + private void onLeafAtomRead(LeafAtom leaf, long inputPosition) throws ParserException { + if (!containerAtoms.isEmpty()) { + containerAtoms.peek().add(leaf); + } else if (leaf.type == Atom.TYPE_sidx) { + ChunkIndex segmentIndex = parseSidx(leaf.data, inputPosition); + extractorOutput.seekMap(segmentIndex); + haveOutputSeekMap = true; + } + } + + private void onContainerAtomRead(ContainerAtom container) throws ParserException { + if (container.type == Atom.TYPE_moov) { + onMoovContainerAtomRead(container); + } else if (container.type == Atom.TYPE_moof) { + onMoofContainerAtomRead(container); + } else if (!containerAtoms.isEmpty()) { + containerAtoms.peek().add(container); + } + } + + private void onMoovContainerAtomRead(ContainerAtom moov) { + Assertions.checkState(sideloadedTrack == null, "Unexpected moov box."); + List moovLeafChildren = moov.leafChildren; + int moovLeafChildrenSize = moovLeafChildren.size(); + + DrmInitData.Mapped drmInitData = null; + for (int i = 0; i < moovLeafChildrenSize; i++) { + LeafAtom child = moovLeafChildren.get(i); + if (child.type == Atom.TYPE_pssh) { + if (drmInitData == null) { + drmInitData = new DrmInitData.Mapped(); + } + byte[] psshData = child.data.data; + UUID uuid = PsshAtomUtil.parseUuid(psshData); + if (uuid == null) { + Log.w(TAG, "Skipped pssh atom (failed to extract uuid)"); + } else { + drmInitData.put(PsshAtomUtil.parseUuid(psshData), + new SchemeInitData(MimeTypes.VIDEO_MP4, psshData)); + } + } + } + if (drmInitData != null) { + extractorOutput.drmInitData(drmInitData); + } + + // Read declaration of track fragments in the Moov box. + ContainerAtom mvex = moov.getContainerAtomOfType(Atom.TYPE_mvex); + SparseArray defaultSampleValuesArray = new SparseArray<>(); + long duration = -1; + int mvexChildrenSize = mvex.leafChildren.size(); + for (int i = 0; i < mvexChildrenSize; i++) { + Atom.LeafAtom atom = mvex.leafChildren.get(i); + if (atom.type == Atom.TYPE_trex) { + Pair trexData = parseTrex(atom.data); + defaultSampleValuesArray.put(trexData.first, trexData.second); + } else if (atom.type == Atom.TYPE_mehd) { + duration = parseMehd(atom.data); + } + } + + // Construction of tracks. + SparseArray tracks = new SparseArray<>(); + int moovContainerChildrenSize = moov.containerChildren.size(); + for (int i = 0; i < moovContainerChildrenSize; i++) { + Atom.ContainerAtom atom = moov.containerChildren.get(i); + if (atom.type == Atom.TYPE_trak) { + Track track = AtomParsers.parseTrak(atom, moov.getLeafAtomOfType(Atom.TYPE_mvhd), duration, + false); + if (track != null) { + tracks.put(track.id, track); + } + } + } + int trackCount = tracks.size(); + + if (trackBundles.size() == 0) { + // We need to create the track bundles. + for (int i = 0; i < trackCount; i++) { + trackBundles.put(tracks.valueAt(i).id, new TrackBundle(extractorOutput.track(i))); + } + extractorOutput.endTracks(); + } else { + Assertions.checkState(trackBundles.size() == trackCount); + } + + // Initialization of tracks and default sample values. + for (int i = 0; i < trackCount; i++) { + Track track = tracks.valueAt(i); + trackBundles.get(track.id).init(track, defaultSampleValuesArray.get(track.id)); + } + } + + private void onMoofContainerAtomRead(ContainerAtom moof) throws ParserException { + parseMoof(moof, trackBundles, flags, extendedTypeScratch); + } + + /** + * Parses a trex atom (defined in 14496-12). + */ + private static Pair parseTrex(ParsableByteArray trex) { + trex.setPosition(Atom.FULL_HEADER_SIZE); + int trackId = trex.readInt(); + int defaultSampleDescriptionIndex = trex.readUnsignedIntToInt() - 1; + int defaultSampleDuration = trex.readUnsignedIntToInt(); + int defaultSampleSize = trex.readUnsignedIntToInt(); + int defaultSampleFlags = trex.readInt(); + + return Pair.create(trackId, new DefaultSampleValues(defaultSampleDescriptionIndex, + defaultSampleDuration, defaultSampleSize, defaultSampleFlags)); + } + + /** + * Parses an mehd atom (defined in 14496-12). + */ + private static long parseMehd(ParsableByteArray mehd) { + mehd.setPosition(Atom.HEADER_SIZE); + int fullAtom = mehd.readInt(); + int version = Atom.parseFullAtomVersion(fullAtom); + return version == 0 ? mehd.readUnsignedInt() : mehd.readUnsignedLongToLong(); + } + + private static void parseMoof(ContainerAtom moof, SparseArray trackBundleArray, + int flags, byte[] extendedTypeScratch) throws ParserException { + int moofContainerChildrenSize = moof.containerChildren.size(); + for (int i = 0; i < moofContainerChildrenSize; i++) { + Atom.ContainerAtom child = moof.containerChildren.get(i); + if (child.type == Atom.TYPE_traf) { + parseTraf(child, trackBundleArray, flags, extendedTypeScratch); + } + } + } + + /** + * Parses a traf atom (defined in 14496-12). + */ + private static void parseTraf(ContainerAtom traf, SparseArray trackBundleArray, + int flags, byte[] extendedTypeScratch) throws ParserException { + if (traf.getChildAtomOfTypeCount(Atom.TYPE_trun) != 1) { + throw new ParserException("Trun count in traf != 1 (unsupported)."); + } + + LeafAtom tfhd = traf.getLeafAtomOfType(Atom.TYPE_tfhd); + TrackBundle trackBundle = parseTfhd(tfhd.data, trackBundleArray, flags); + if (trackBundle == null) { + return; + } + + TrackFragment fragment = trackBundle.fragment; + long decodeTime = fragment.nextFragmentDecodeTime; + trackBundle.reset(); + + LeafAtom tfdtAtom = traf.getLeafAtomOfType(Atom.TYPE_tfdt); + if (tfdtAtom != null && (flags & FLAG_WORKAROUND_IGNORE_TFDT_BOX) == 0) { + decodeTime = parseTfdt(traf.getLeafAtomOfType(Atom.TYPE_tfdt).data); + } + + LeafAtom trun = traf.getLeafAtomOfType(Atom.TYPE_trun); + parseTrun(trackBundle, decodeTime, flags, trun.data); + + LeafAtom saiz = traf.getLeafAtomOfType(Atom.TYPE_saiz); + if (saiz != null) { + TrackEncryptionBox trackEncryptionBox = trackBundle.track + .sampleDescriptionEncryptionBoxes[fragment.header.sampleDescriptionIndex]; + parseSaiz(trackEncryptionBox, saiz.data, fragment); + } + + LeafAtom saio = traf.getLeafAtomOfType(Atom.TYPE_saio); + if (saio != null) { + parseSaio(saio.data, fragment); + } + + LeafAtom senc = traf.getLeafAtomOfType(Atom.TYPE_senc); + if (senc != null) { + parseSenc(senc.data, fragment); + } + + int childrenSize = traf.leafChildren.size(); + for (int i = 0; i < childrenSize; i++) { + LeafAtom atom = traf.leafChildren.get(i); + if (atom.type == Atom.TYPE_uuid) { + parseUuid(atom.data, fragment, extendedTypeScratch); + } + } + } + + private static void parseSaiz(TrackEncryptionBox encryptionBox, ParsableByteArray saiz, + TrackFragment out) throws ParserException { + int vectorSize = encryptionBox.initializationVectorSize; + saiz.setPosition(Atom.HEADER_SIZE); + int fullAtom = saiz.readInt(); + int flags = Atom.parseFullAtomFlags(fullAtom); + if ((flags & 0x01) == 1) { + saiz.skipBytes(8); + } + int defaultSampleInfoSize = saiz.readUnsignedByte(); + + int sampleCount = saiz.readUnsignedIntToInt(); + if (sampleCount != out.length) { + throw new ParserException("Length mismatch: " + sampleCount + ", " + out.length); + } + + int totalSize = 0; + if (defaultSampleInfoSize == 0) { + boolean[] sampleHasSubsampleEncryptionTable = out.sampleHasSubsampleEncryptionTable; + for (int i = 0; i < sampleCount; i++) { + int sampleInfoSize = saiz.readUnsignedByte(); + totalSize += sampleInfoSize; + sampleHasSubsampleEncryptionTable[i] = sampleInfoSize > vectorSize; + } + } else { + boolean subsampleEncryption = defaultSampleInfoSize > vectorSize; + totalSize += defaultSampleInfoSize * sampleCount; + Arrays.fill(out.sampleHasSubsampleEncryptionTable, 0, sampleCount, subsampleEncryption); + } + out.initEncryptionData(totalSize); + } + + /** + * Parses a saio atom (defined in 14496-12). + * + * @param saio The saio atom to parse. + * @param out The {@link TrackFragment} to populate with data from the saio atom. + */ + private static void parseSaio(ParsableByteArray saio, TrackFragment out) throws ParserException { + saio.setPosition(Atom.HEADER_SIZE); + int fullAtom = saio.readInt(); + int flags = Atom.parseFullAtomFlags(fullAtom); + if ((flags & 0x01) == 1) { + saio.skipBytes(8); + } + + int entryCount = saio.readUnsignedIntToInt(); + if (entryCount != 1) { + // We only support one trun element currently, so always expect one entry. + throw new ParserException("Unexpected saio entry count: " + entryCount); + } + + int version = Atom.parseFullAtomVersion(fullAtom); + out.auxiliaryDataPosition += + version == 0 ? saio.readUnsignedInt() : saio.readUnsignedLongToLong(); + } + + /** + * Parses a tfhd atom (defined in 14496-12), updates the corresponding {@link TrackFragment} and + * returns the {@link TrackBundle} of the corresponding {@link Track}. If the tfhd does not refer + * to any {@link TrackBundle}, {@code null} is returned and no changes are made. + * + * @param tfhd The tfhd atom to parse. + * @param trackBundles The track bundles, one of which corresponds to the tfhd atom being parsed. + * @return The {@link TrackBundle} to which the {@link TrackFragment} belongs, or null if the tfhd + * does not refer to any {@link TrackBundle}. + */ + private static TrackBundle parseTfhd(ParsableByteArray tfhd, + SparseArray trackBundles, int flags) { + tfhd.setPosition(Atom.HEADER_SIZE); + int fullAtom = tfhd.readInt(); + int atomFlags = Atom.parseFullAtomFlags(fullAtom); + int trackId = tfhd.readInt(); + TrackBundle trackBundle = trackBundles.get((flags & FLAG_SIDELOADED) == 0 ? trackId : 0); + if (trackBundle == null) { + return null; + } + if ((atomFlags & 0x01 /* base_data_offset_present */) != 0) { + long baseDataPosition = tfhd.readUnsignedLongToLong(); + trackBundle.fragment.dataPosition = baseDataPosition; + trackBundle.fragment.auxiliaryDataPosition = baseDataPosition; + } + + DefaultSampleValues defaultSampleValues = trackBundle.defaultSampleValues; + int defaultSampleDescriptionIndex = + ((atomFlags & 0x02 /* default_sample_description_index_present */) != 0) + ? tfhd.readUnsignedIntToInt() - 1 : defaultSampleValues.sampleDescriptionIndex; + int defaultSampleDuration = ((atomFlags & 0x08 /* default_sample_duration_present */) != 0) + ? tfhd.readUnsignedIntToInt() : defaultSampleValues.duration; + int defaultSampleSize = ((atomFlags & 0x10 /* default_sample_size_present */) != 0) + ? tfhd.readUnsignedIntToInt() : defaultSampleValues.size; + int defaultSampleFlags = ((atomFlags & 0x20 /* default_sample_flags_present */) != 0) + ? tfhd.readUnsignedIntToInt() : defaultSampleValues.flags; + trackBundle.fragment.header = new DefaultSampleValues(defaultSampleDescriptionIndex, + defaultSampleDuration, defaultSampleSize, defaultSampleFlags); + return trackBundle; + } + + /** + * Parses a tfdt atom (defined in 14496-12). + * + * @return baseMediaDecodeTime The sum of the decode durations of all earlier samples in the + * media, expressed in the media's timescale. + */ + private static long parseTfdt(ParsableByteArray tfdt) { + tfdt.setPosition(Atom.HEADER_SIZE); + int fullAtom = tfdt.readInt(); + int version = Atom.parseFullAtomVersion(fullAtom); + return version == 1 ? tfdt.readUnsignedLongToLong() : tfdt.readUnsignedInt(); + } + + /** + * Parses a trun atom (defined in 14496-12). + * + * @param trackBundle The {@link TrackBundle} that contains the {@link TrackFragment} into + * which parsed data should be placed. + * @param decodeTime The decode time of the first sample in the fragment run. + * @param flags Flags to allow any required workaround to be executed. + * @param trun The trun atom to parse. + */ + private static void parseTrun(TrackBundle trackBundle, long decodeTime, int flags, + ParsableByteArray trun) { + trun.setPosition(Atom.HEADER_SIZE); + int fullAtom = trun.readInt(); + int atomFlags = Atom.parseFullAtomFlags(fullAtom); + + Track track = trackBundle.track; + TrackFragment fragment = trackBundle.fragment; + DefaultSampleValues defaultSampleValues = fragment.header; + + int sampleCount = trun.readUnsignedIntToInt(); + if ((atomFlags & 0x01 /* data_offset_present */) != 0) { + fragment.dataPosition += trun.readInt(); + } + + boolean firstSampleFlagsPresent = (atomFlags & 0x04 /* first_sample_flags_present */) != 0; + int firstSampleFlags = defaultSampleValues.flags; + if (firstSampleFlagsPresent) { + firstSampleFlags = trun.readUnsignedIntToInt(); + } + + boolean sampleDurationsPresent = (atomFlags & 0x100 /* sample_duration_present */) != 0; + boolean sampleSizesPresent = (atomFlags & 0x200 /* sample_size_present */) != 0; + boolean sampleFlagsPresent = (atomFlags & 0x400 /* sample_flags_present */) != 0; + boolean sampleCompositionTimeOffsetsPresent = + (atomFlags & 0x800 /* sample_composition_time_offsets_present */) != 0; + + // Offset to the entire video timeline. In the presence of B-frames this is usually used to + // ensure that the first frame's presentation timestamp is zero. + long edtsOffset = 0; + + // Currently we only support a single edit that moves the entire media timeline (indicated by + // duration == 0). Other uses of edit lists are uncommon and unsupported. + if (track.editListDurations != null && track.editListDurations.length == 1 + && track.editListDurations[0] == 0) { + edtsOffset = Util.scaleLargeTimestamp(track.editListMediaTimes[0], 1000, track.timescale); + } + + fragment.initTables(sampleCount); + int[] sampleSizeTable = fragment.sampleSizeTable; + int[] sampleCompositionTimeOffsetTable = fragment.sampleCompositionTimeOffsetTable; + long[] sampleDecodingTimeTable = fragment.sampleDecodingTimeTable; + boolean[] sampleIsSyncFrameTable = fragment.sampleIsSyncFrameTable; + + long timescale = track.timescale; + long cumulativeTime = decodeTime; + boolean workaroundEveryVideoFrameIsSyncFrame = track.type == Track.TYPE_vide + && (flags & FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME) != 0; + for (int i = 0; i < sampleCount; i++) { + // Use trun values if present, otherwise tfhd, otherwise trex. + int sampleDuration = sampleDurationsPresent ? trun.readUnsignedIntToInt() + : defaultSampleValues.duration; + int sampleSize = sampleSizesPresent ? trun.readUnsignedIntToInt() : defaultSampleValues.size; + int sampleFlags = (i == 0 && firstSampleFlagsPresent) ? firstSampleFlags + : sampleFlagsPresent ? trun.readInt() : defaultSampleValues.flags; + if (sampleCompositionTimeOffsetsPresent) { + // The BMFF spec (ISO 14496-12) states that sample offsets should be unsigned integers in + // version 0 trun boxes, however a significant number of streams violate the spec and use + // signed integers instead. It's safe to always parse sample offsets as signed integers + // here, because unsigned integers will still be parsed correctly (unless their top bit is + // set, which is never true in practice because sample offsets are always small). + int sampleOffset = trun.readInt(); + sampleCompositionTimeOffsetTable[i] = (int) ((sampleOffset * 1000) / timescale); + } else { + sampleCompositionTimeOffsetTable[i] = 0; + } + sampleDecodingTimeTable[i] = + Util.scaleLargeTimestamp(cumulativeTime, 1000, timescale) - edtsOffset; + sampleSizeTable[i] = sampleSize; + sampleIsSyncFrameTable[i] = ((sampleFlags >> 16) & 0x1) == 0 + && (!workaroundEveryVideoFrameIsSyncFrame || i == 0); + cumulativeTime += sampleDuration; + } + fragment.nextFragmentDecodeTime = cumulativeTime; + } + + private static void parseUuid(ParsableByteArray uuid, TrackFragment out, + byte[] extendedTypeScratch) throws ParserException { + uuid.setPosition(Atom.HEADER_SIZE); + uuid.readBytes(extendedTypeScratch, 0, 16); + + // Currently this parser only supports Microsoft's PIFF SampleEncryptionBox. + if (!Arrays.equals(extendedTypeScratch, PIFF_SAMPLE_ENCRYPTION_BOX_EXTENDED_TYPE)) { + return; + } + + // Except for the extended type, this box is identical to a SENC box. See "Portable encoding of + // audio-video objects: The Protected Interoperable File Format (PIFF), John A. Bocharov et al, + // Section 5.3.2.1." + parseSenc(uuid, 16, out); + } + + private static void parseSenc(ParsableByteArray senc, TrackFragment out) throws ParserException { + parseSenc(senc, 0, out); + } + + private static void parseSenc(ParsableByteArray senc, int offset, TrackFragment out) + throws ParserException { + senc.setPosition(Atom.HEADER_SIZE + offset); + int fullAtom = senc.readInt(); + int flags = Atom.parseFullAtomFlags(fullAtom); + + if ((flags & 0x01 /* override_track_encryption_box_parameters */) != 0) { + // TODO: Implement this. + throw new ParserException("Overriding TrackEncryptionBox parameters is unsupported."); + } + + boolean subsampleEncryption = (flags & 0x02 /* use_subsample_encryption */) != 0; + int sampleCount = senc.readUnsignedIntToInt(); + if (sampleCount != out.length) { + throw new ParserException("Length mismatch: " + sampleCount + ", " + out.length); + } + + Arrays.fill(out.sampleHasSubsampleEncryptionTable, 0, sampleCount, subsampleEncryption); + out.initEncryptionData(senc.bytesLeft()); + out.fillEncryptionData(senc); + } + + /** + * Parses a sidx atom (defined in 14496-12). + */ + private static ChunkIndex parseSidx(ParsableByteArray atom, long inputPosition) + throws ParserException { + atom.setPosition(Atom.HEADER_SIZE); + int fullAtom = atom.readInt(); + int version = Atom.parseFullAtomVersion(fullAtom); + + atom.skipBytes(4); + long timescale = atom.readUnsignedInt(); + long earliestPresentationTime; + long offset = inputPosition; + if (version == 0) { + earliestPresentationTime = atom.readUnsignedInt(); + offset += atom.readUnsignedInt(); + } else { + earliestPresentationTime = atom.readUnsignedLongToLong(); + offset += atom.readUnsignedLongToLong(); + } + + atom.skipBytes(2); + + int referenceCount = atom.readUnsignedShort(); + int[] sizes = new int[referenceCount]; + long[] offsets = new long[referenceCount]; + long[] durationsUs = new long[referenceCount]; + long[] timesUs = new long[referenceCount]; + + long time = earliestPresentationTime; + long timeUs = Util.scaleLargeTimestamp(time, C.MICROS_PER_SECOND, timescale); + for (int i = 0; i < referenceCount; i++) { + int firstInt = atom.readInt(); + + int type = 0x80000000 & firstInt; + if (type != 0) { + throw new ParserException("Unhandled indirect reference"); + } + long referenceDuration = atom.readUnsignedInt(); + + sizes[i] = 0x7FFFFFFF & firstInt; + offsets[i] = offset; + + // Calculate time and duration values such that any rounding errors are consistent. i.e. That + // timesUs[i] + durationsUs[i] == timesUs[i + 1]. + timesUs[i] = timeUs; + time += referenceDuration; + timeUs = Util.scaleLargeTimestamp(time, C.MICROS_PER_SECOND, timescale); + durationsUs[i] = timeUs - timesUs[i]; + + atom.skipBytes(4); + offset += sizes[i]; + } + + return new ChunkIndex(sizes, offsets, durationsUs, timesUs); + } + + private void readEncryptionData(ExtractorInput input) throws IOException, InterruptedException { + TrackBundle nextTrackBundle = null; + long nextDataOffset = Long.MAX_VALUE; + int trackBundlesSize = trackBundles.size(); + for (int i = 0; i < trackBundlesSize; i++) { + TrackFragment trackFragment = trackBundles.valueAt(i).fragment; + if (trackFragment.sampleEncryptionDataNeedsFill + && trackFragment.auxiliaryDataPosition < nextDataOffset) { + nextDataOffset = trackFragment.auxiliaryDataPosition; + nextTrackBundle = trackBundles.valueAt(i); + } + } + if (nextTrackBundle == null) { + parserState = STATE_READING_SAMPLE_START; + return; + } + int bytesToSkip = (int) (nextDataOffset - input.getPosition()); + if (bytesToSkip < 0) { + throw new ParserException("Offset to encryption data was negative."); + } + input.skipFully(bytesToSkip); + nextTrackBundle.fragment.fillEncryptionData(input); + } + + /** + * Attempts to extract the next sample in the current mdat atom. + *

+ * If there are no more samples in the current mdat atom then the parser state is transitioned + * to {@link #STATE_READING_ATOM_HEADER} and {@code false} is returned. + *

+ * It is possible for a sample to be extracted in part in the case that an exception is thrown. In + * this case the method can be called again to extract the remainder of the sample. + * + * @param input The {@link ExtractorInput} from which to read data. + * @return True if a sample was extracted. False otherwise. + * @throws IOException If an error occurs reading from the input. + * @throws InterruptedException If the thread is interrupted. + */ + private boolean readSample(ExtractorInput input) throws IOException, InterruptedException { + if (parserState == STATE_READING_SAMPLE_START) { + if (currentTrackBundle == null) { + currentTrackBundle = getNextFragmentRun(trackBundles); + if (currentTrackBundle == null) { + // We've run out of samples in the current mdat. Discard any trailing data and prepare to + // read the header of the next atom. + int bytesToSkip = (int) (endOfMdatPosition - input.getPosition()); + if (bytesToSkip < 0) { + throw new ParserException("Offset to end of mdat was negative."); + } + input.skipFully(bytesToSkip); + enterReadingAtomHeaderState(); + return false; + } + + long nextDataPosition = currentTrackBundle.fragment.dataPosition; + // We skip bytes preceding the next sample to read. + int bytesToSkip = (int) (nextDataPosition - input.getPosition()); + if (bytesToSkip < 0) { + throw new ParserException("Offset to sample data was negative."); + } + input.skipFully(bytesToSkip); + } + sampleSize = currentTrackBundle.fragment + .sampleSizeTable[currentTrackBundle.currentSampleIndex]; + if (currentTrackBundle.fragment.definesEncryptionData) { + sampleBytesWritten = appendSampleEncryptionData(currentTrackBundle); + sampleSize += sampleBytesWritten; + } else { + sampleBytesWritten = 0; + } + parserState = STATE_READING_SAMPLE_CONTINUE; + sampleCurrentNalBytesRemaining = 0; + } + + TrackFragment fragment = currentTrackBundle.fragment; + Track track = currentTrackBundle.track; + TrackOutput output = currentTrackBundle.output; + int sampleIndex = currentTrackBundle.currentSampleIndex; + if (track.nalUnitLengthFieldLength != -1) { + // Zero the top three bytes of the array that we'll use to parse nal unit lengths, in case + // they're only 1 or 2 bytes long. + byte[] nalLengthData = nalLength.data; + nalLengthData[0] = 0; + nalLengthData[1] = 0; + nalLengthData[2] = 0; + int nalUnitLengthFieldLength = track.nalUnitLengthFieldLength; + int nalUnitLengthFieldLengthDiff = 4 - track.nalUnitLengthFieldLength; + // NAL units are length delimited, but the decoder requires start code delimited units. + // Loop until we've written the sample to the track output, replacing length delimiters with + // start codes as we encounter them. + while (sampleBytesWritten < sampleSize) { + if (sampleCurrentNalBytesRemaining == 0) { + // Read the NAL length so that we know where we find the next one. + input.readFully(nalLength.data, nalUnitLengthFieldLengthDiff, nalUnitLengthFieldLength); + nalLength.setPosition(0); + sampleCurrentNalBytesRemaining = nalLength.readUnsignedIntToInt(); + // Write a start code for the current NAL unit. + nalStartCode.setPosition(0); + output.sampleData(nalStartCode, 4); + sampleBytesWritten += 4; + sampleSize += nalUnitLengthFieldLengthDiff; + } else { + // Write the payload of the NAL unit. + int writtenBytes = output.sampleData(input, sampleCurrentNalBytesRemaining, false); + sampleBytesWritten += writtenBytes; + sampleCurrentNalBytesRemaining -= writtenBytes; + } + } + } else { + while (sampleBytesWritten < sampleSize) { + int writtenBytes = output.sampleData(input, sampleSize - sampleBytesWritten, false); + sampleBytesWritten += writtenBytes; + } + } + + long sampleTimeUs = fragment.getSamplePresentationTime(sampleIndex) * 1000L; + int sampleFlags = (fragment.definesEncryptionData ? C.SAMPLE_FLAG_ENCRYPTED : 0) + | (fragment.sampleIsSyncFrameTable[sampleIndex] ? C.SAMPLE_FLAG_SYNC : 0); + int sampleDescriptionIndex = fragment.header.sampleDescriptionIndex; + byte[] encryptionKey = fragment.definesEncryptionData + ? track.sampleDescriptionEncryptionBoxes[sampleDescriptionIndex].keyId : null; + output.sampleMetadata(sampleTimeUs, sampleFlags, sampleSize, 0, encryptionKey); + + currentTrackBundle.currentSampleIndex++; + if (currentTrackBundle.currentSampleIndex == fragment.length) { + currentTrackBundle = null; + } + parserState = STATE_READING_SAMPLE_START; + return true; + } + + /** + * Returns the {@link TrackBundle} whose fragment run has the earliest file position out of those + * yet to be consumed, or null if all have been consumed. + */ + private static TrackBundle getNextFragmentRun(SparseArray trackBundles) { + TrackBundle nextTrackBundle = null; + long nextTrackRunOffset = Long.MAX_VALUE; + + int trackBundlesSize = trackBundles.size(); + for (int i = 0; i < trackBundlesSize; i++) { + TrackBundle trackBundle = trackBundles.valueAt(i); + if (trackBundle.currentSampleIndex == trackBundle.fragment.length) { + // This track fragment contains no more runs in the next mdat box. + } else { + long trunOffset = trackBundle.fragment.dataPosition; + if (trunOffset < nextTrackRunOffset) { + nextTrackBundle = trackBundle; + nextTrackRunOffset = trunOffset; + } + } + } + return nextTrackBundle; + } + + /** + * Appends the corresponding encryption data to the {@link TrackOutput} contained in the given + * {@link TrackBundle}. + * + * @param trackBundle The {@link TrackBundle} that contains the {@link Track} for which the + * Sample encryption data must be output. + * @return The number of written bytes. + */ + private int appendSampleEncryptionData(TrackBundle trackBundle) { + TrackFragment trackFragment = trackBundle.fragment; + ParsableByteArray sampleEncryptionData = trackFragment.sampleEncryptionData; + int sampleDescriptionIndex = trackFragment.header.sampleDescriptionIndex; + TrackEncryptionBox encryptionBox = trackBundle.track + .sampleDescriptionEncryptionBoxes[sampleDescriptionIndex]; + int vectorSize = encryptionBox.initializationVectorSize; + boolean subsampleEncryption = trackFragment + .sampleHasSubsampleEncryptionTable[trackBundle.currentSampleIndex]; + + // Write the signal byte, containing the vector size and the subsample encryption flag. + encryptionSignalByte.data[0] = (byte) (vectorSize | (subsampleEncryption ? 0x80 : 0)); + encryptionSignalByte.setPosition(0); + TrackOutput output = trackBundle.output; + output.sampleData(encryptionSignalByte, 1); + // Write the vector. + output.sampleData(sampleEncryptionData, vectorSize); + // If we don't have subsample encryption data, we're done. + if (!subsampleEncryption) { + return 1 + vectorSize; + } + // Write the subsample encryption data. + int subsampleCount = sampleEncryptionData.readUnsignedShort(); + sampleEncryptionData.skipBytes(-2); + int subsampleDataLength = 2 + 6 * subsampleCount; + output.sampleData(sampleEncryptionData, subsampleDataLength); + return 1 + vectorSize + subsampleDataLength; + } + + /** Returns whether the extractor should parse a leaf atom with type {@code atom}. */ + private static boolean shouldParseLeafAtom(int atom) { + return atom == Atom.TYPE_hdlr || atom == Atom.TYPE_mdhd || atom == Atom.TYPE_mvhd + || atom == Atom.TYPE_sidx || atom == Atom.TYPE_stsd || atom == Atom.TYPE_tfdt + || atom == Atom.TYPE_tfhd || atom == Atom.TYPE_tkhd || atom == Atom.TYPE_trex + || atom == Atom.TYPE_trun || atom == Atom.TYPE_pssh || atom == Atom.TYPE_saiz + || atom == Atom.TYPE_saio || atom == Atom.TYPE_senc || atom == Atom.TYPE_uuid + || atom == Atom.TYPE_elst || atom == Atom.TYPE_mehd; + } + + /** Returns whether the extractor should parse a container atom with type {@code atom}. */ + private static boolean shouldParseContainerAtom(int atom) { + return atom == Atom.TYPE_moov || atom == Atom.TYPE_trak || atom == Atom.TYPE_mdia + || atom == Atom.TYPE_minf || atom == Atom.TYPE_stbl || atom == Atom.TYPE_moof + || atom == Atom.TYPE_traf || atom == Atom.TYPE_mvex || atom == Atom.TYPE_edts; + } + + /** + * Holds data corresponding to a single track. + */ + private static final class TrackBundle { + + public final TrackFragment fragment; + public final TrackOutput output; + + public Track track; + public DefaultSampleValues defaultSampleValues; + public int currentSampleIndex; + + public TrackBundle(TrackOutput output) { + fragment = new TrackFragment(); + this.output = output; + } + + public void init(Track track, DefaultSampleValues defaultSampleValues) { + this.track = Assertions.checkNotNull(track); + this.defaultSampleValues = Assertions.checkNotNull(defaultSampleValues); + output.format(track.mediaFormat); + reset(); + } + + public void reset() { + fragment.reset(); + currentSampleIndex = 0; + } + + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp4/Mp4Extractor.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp4/Mp4Extractor.java new file mode 100755 index 000000000..8ca5ea6cd --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp4/Mp4Extractor.java @@ -0,0 +1,476 @@ +/* + * 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.messenger.exoplayer.extractor.mp4; + +import org.telegram.messenger.exoplayer.MediaFormat; +import org.telegram.messenger.exoplayer.ParserException; +import org.telegram.messenger.exoplayer.extractor.Extractor; +import org.telegram.messenger.exoplayer.extractor.ExtractorInput; +import org.telegram.messenger.exoplayer.extractor.ExtractorOutput; +import org.telegram.messenger.exoplayer.extractor.GaplessInfo; +import org.telegram.messenger.exoplayer.extractor.PositionHolder; +import org.telegram.messenger.exoplayer.extractor.SeekMap; +import org.telegram.messenger.exoplayer.extractor.TrackOutput; +import org.telegram.messenger.exoplayer.extractor.mp4.Atom.ContainerAtom; +import org.telegram.messenger.exoplayer.util.Assertions; +import org.telegram.messenger.exoplayer.util.NalUnitUtil; +import org.telegram.messenger.exoplayer.util.ParsableByteArray; +import org.telegram.messenger.exoplayer.util.Util; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Stack; + +/** + * Extracts data from an unfragmented MP4 file. + */ +public final class Mp4Extractor implements Extractor, SeekMap { + + // Parser states. + private static final int STATE_AFTER_SEEK = 0; + private static final int STATE_READING_ATOM_HEADER = 1; + private static final int STATE_READING_ATOM_PAYLOAD = 2; + private static final int STATE_READING_SAMPLE = 3; + + // Brand stored in the ftyp atom for QuickTime media. + private static final int BRAND_QUICKTIME = Util.getIntegerCodeForString("qt "); + + /** + * When seeking within the source, if the offset is greater than or equal to this value (or the + * offset is negative), the source will be reloaded. + */ + private static final long RELOAD_MINIMUM_SEEK_DISTANCE = 256 * 1024; + + // Temporary arrays. + private final ParsableByteArray nalStartCode; + private final ParsableByteArray nalLength; + + private final ParsableByteArray atomHeader; + private final Stack containerAtoms; + + private int parserState; + private int atomType; + private long atomSize; + private int atomHeaderBytesRead; + private ParsableByteArray atomData; + + private int sampleSize; + private int sampleBytesWritten; + private int sampleCurrentNalBytesRemaining; + + // Extractor outputs. + private ExtractorOutput extractorOutput; + private Mp4Track[] tracks; + private boolean isQuickTime; + + public Mp4Extractor() { + atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE); + containerAtoms = new Stack<>(); + nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE); + nalLength = new ParsableByteArray(4); + enterReadingAtomHeaderState(); + } + + @Override + public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + return Sniffer.sniffUnfragmented(input); + } + + @Override + public void init(ExtractorOutput output) { + extractorOutput = output; + } + + @Override + public void seek() { + containerAtoms.clear(); + atomHeaderBytesRead = 0; + sampleBytesWritten = 0; + sampleCurrentNalBytesRemaining = 0; + parserState = STATE_AFTER_SEEK; + } + + @Override + public void release() { + // Do nothing + } + + @Override + public int read(ExtractorInput input, PositionHolder seekPosition) + throws IOException, InterruptedException { + while (true) { + switch (parserState) { + case STATE_AFTER_SEEK: + if (input.getPosition() == 0) { + enterReadingAtomHeaderState(); + } else { + parserState = STATE_READING_SAMPLE; + } + break; + case STATE_READING_ATOM_HEADER: + if (!readAtomHeader(input)) { + return RESULT_END_OF_INPUT; + } + break; + case STATE_READING_ATOM_PAYLOAD: + if (readAtomPayload(input, seekPosition)) { + return RESULT_SEEK; + } + break; + default: + return readSample(input, seekPosition); + } + } + } + + // SeekMap implementation. + + @Override + public boolean isSeekable() { + return true; + } + + @Override + public long getPosition(long timeUs) { + long earliestSamplePosition = Long.MAX_VALUE; + for (int trackIndex = 0; trackIndex < tracks.length; trackIndex++) { + TrackSampleTable sampleTable = tracks[trackIndex].sampleTable; + int sampleIndex = sampleTable.getIndexOfEarlierOrEqualSynchronizationSample(timeUs); + if (sampleIndex == TrackSampleTable.NO_SAMPLE) { + // Handle the case where the requested time is before the first synchronization sample. + sampleIndex = sampleTable.getIndexOfLaterOrEqualSynchronizationSample(timeUs); + } + tracks[trackIndex].sampleIndex = sampleIndex; + + long offset = sampleTable.offsets[sampleIndex]; + if (offset < earliestSamplePosition) { + earliestSamplePosition = offset; + } + } + return earliestSamplePosition; + } + + // Private methods. + + private void enterReadingAtomHeaderState() { + parserState = STATE_READING_ATOM_HEADER; + atomHeaderBytesRead = 0; + } + + private boolean readAtomHeader(ExtractorInput input) throws IOException, InterruptedException { + if (atomHeaderBytesRead == 0) { + // Read the standard length atom header. + if (!input.readFully(atomHeader.data, 0, Atom.HEADER_SIZE, true)) { + return false; + } + atomHeaderBytesRead = Atom.HEADER_SIZE; + atomHeader.setPosition(0); + atomSize = atomHeader.readUnsignedInt(); + atomType = atomHeader.readInt(); + } + + if (atomSize == Atom.LONG_SIZE_PREFIX) { + // Read the extended atom size. + int headerBytesRemaining = Atom.LONG_HEADER_SIZE - Atom.HEADER_SIZE; + input.readFully(atomHeader.data, Atom.HEADER_SIZE, headerBytesRemaining); + atomHeaderBytesRead += headerBytesRemaining; + atomSize = atomHeader.readUnsignedLongToLong(); + } + + if (shouldParseContainerAtom(atomType)) { + long endPosition = input.getPosition() + atomSize - atomHeaderBytesRead; + containerAtoms.add(new ContainerAtom(atomType, endPosition)); + if (atomSize == atomHeaderBytesRead) { + processAtomEnded(endPosition); + } else { + // Start reading the first child atom. + enterReadingAtomHeaderState(); + } + } else if (shouldParseLeafAtom(atomType)) { + // We don't support parsing of leaf atoms that define extended atom sizes, or that have + // lengths greater than Integer.MAX_VALUE. + Assertions.checkState(atomHeaderBytesRead == Atom.HEADER_SIZE); + Assertions.checkState(atomSize <= Integer.MAX_VALUE); + atomData = new ParsableByteArray((int) atomSize); + System.arraycopy(atomHeader.data, 0, atomData.data, 0, Atom.HEADER_SIZE); + parserState = STATE_READING_ATOM_PAYLOAD; + } else { + atomData = null; + parserState = STATE_READING_ATOM_PAYLOAD; + } + + return true; + } + + /** + * Processes the atom payload. If {@link #atomData} is null and the size is at or above the + * threshold {@link #RELOAD_MINIMUM_SEEK_DISTANCE}, {@code true} is returned and the caller should + * restart loading at the position in {@code positionHolder}. Otherwise, the atom is read/skipped. + */ + private boolean readAtomPayload(ExtractorInput input, PositionHolder positionHolder) + throws IOException, InterruptedException { + long atomPayloadSize = atomSize - atomHeaderBytesRead; + long atomEndPosition = input.getPosition() + atomPayloadSize; + boolean seekRequired = false; + if (atomData != null) { + input.readFully(atomData.data, atomHeaderBytesRead, (int) atomPayloadSize); + if (atomType == Atom.TYPE_ftyp) { + isQuickTime = processFtypAtom(atomData); + } else if (!containerAtoms.isEmpty()) { + containerAtoms.peek().add(new Atom.LeafAtom(atomType, atomData)); + } + } else { + // We don't need the data. Skip or seek, depending on how large the atom is. + if (atomPayloadSize < RELOAD_MINIMUM_SEEK_DISTANCE) { + input.skipFully((int) atomPayloadSize); + } else { + positionHolder.position = input.getPosition() + atomPayloadSize; + seekRequired = true; + } + } + processAtomEnded(atomEndPosition); + return seekRequired && parserState != STATE_READING_SAMPLE; + } + + private void processAtomEnded(long atomEndPosition) throws ParserException { + while (!containerAtoms.isEmpty() && containerAtoms.peek().endPosition == atomEndPosition) { + Atom.ContainerAtom containerAtom = containerAtoms.pop(); + if (containerAtom.type == Atom.TYPE_moov) { + // We've reached the end of the moov atom. Process it and prepare to read samples. + processMoovAtom(containerAtom); + containerAtoms.clear(); + parserState = STATE_READING_SAMPLE; + } else if (!containerAtoms.isEmpty()) { + containerAtoms.peek().add(containerAtom); + } + } + if (parserState != STATE_READING_SAMPLE) { + enterReadingAtomHeaderState(); + } + } + + /** + * Process an ftyp atom to determine whether the media is QuickTime. + * + * @param atomData The ftyp atom data. + * @return True if the media is QuickTime. False otherwise. + */ + private static boolean processFtypAtom(ParsableByteArray atomData) { + atomData.setPosition(Atom.HEADER_SIZE); + int majorBrand = atomData.readInt(); + if (majorBrand == BRAND_QUICKTIME) { + return true; + } + atomData.skipBytes(4); // minor_version + while (atomData.bytesLeft() > 0) { + if (atomData.readInt() == BRAND_QUICKTIME) { + return true; + } + } + return false; + } + + /** + * Updates the stored track metadata to reflect the contents of the specified moov atom. + */ + private void processMoovAtom(ContainerAtom moov) throws ParserException { + List tracks = new ArrayList<>(); + long earliestSampleOffset = Long.MAX_VALUE; + GaplessInfo gaplessInfo = null; + Atom.LeafAtom udta = moov.getLeafAtomOfType(Atom.TYPE_udta); + if (udta != null) { + gaplessInfo = AtomParsers.parseUdta(udta, isQuickTime); + } + for (int i = 0; i < moov.containerChildren.size(); i++) { + Atom.ContainerAtom atom = moov.containerChildren.get(i); + if (atom.type != Atom.TYPE_trak) { + continue; + } + + Track track = AtomParsers.parseTrak(atom, moov.getLeafAtomOfType(Atom.TYPE_mvhd), -1, + isQuickTime); + if (track == null) { + continue; + } + + Atom.ContainerAtom stblAtom = atom.getContainerAtomOfType(Atom.TYPE_mdia) + .getContainerAtomOfType(Atom.TYPE_minf).getContainerAtomOfType(Atom.TYPE_stbl); + TrackSampleTable trackSampleTable = AtomParsers.parseStbl(track, stblAtom); + if (trackSampleTable.sampleCount == 0) { + continue; + } + + Mp4Track mp4Track = new Mp4Track(track, trackSampleTable, extractorOutput.track(i)); + // Each sample has up to three bytes of overhead for the start code that replaces its length. + // Allow ten source samples per output sample, like the platform extractor. + int maxInputSize = trackSampleTable.maximumSize + 3 * 10; + MediaFormat mediaFormat = track.mediaFormat.copyWithMaxInputSize(maxInputSize); + if (gaplessInfo != null) { + mediaFormat = + mediaFormat.copyWithGaplessInfo(gaplessInfo.encoderDelay, gaplessInfo.encoderPadding); + } + mp4Track.trackOutput.format(mediaFormat); + tracks.add(mp4Track); + + long firstSampleOffset = trackSampleTable.offsets[0]; + if (firstSampleOffset < earliestSampleOffset) { + earliestSampleOffset = firstSampleOffset; + } + } + this.tracks = tracks.toArray(new Mp4Track[0]); + extractorOutput.endTracks(); + extractorOutput.seekMap(this); + } + + /** + * Attempts to extract the next sample in the current mdat atom for the specified track. + *

+ * Returns {@link #RESULT_SEEK} if the source should be reloaded from the position in + * {@code positionHolder}. + *

+ * Returns {@link #RESULT_END_OF_INPUT} if no samples are left. Otherwise, returns + * {@link #RESULT_CONTINUE}. + * + * @param input The {@link ExtractorInput} from which to read data. + * @param positionHolder If {@link #RESULT_SEEK} is returned, this holder is updated to hold the + * position of the required data. + * @return One of the {@code RESULT_*} flags in {@link Extractor}. + * @throws IOException If an error occurs reading from the input. + * @throws InterruptedException If the thread is interrupted. + */ + private int readSample(ExtractorInput input, PositionHolder positionHolder) + throws IOException, InterruptedException { + int trackIndex = getTrackIndexOfEarliestCurrentSample(); + if (trackIndex == TrackSampleTable.NO_SAMPLE) { + return RESULT_END_OF_INPUT; + } + Mp4Track track = tracks[trackIndex]; + TrackOutput trackOutput = track.trackOutput; + int sampleIndex = track.sampleIndex; + long position = track.sampleTable.offsets[sampleIndex]; + long skipAmount = position - input.getPosition() + sampleBytesWritten; + if (skipAmount < 0 || skipAmount >= RELOAD_MINIMUM_SEEK_DISTANCE) { + positionHolder.position = position; + return RESULT_SEEK; + } + input.skipFully((int) skipAmount); + sampleSize = track.sampleTable.sizes[sampleIndex]; + if (track.track.nalUnitLengthFieldLength != -1) { + // Zero the top three bytes of the array that we'll use to parse nal unit lengths, in case + // they're only 1 or 2 bytes long. + byte[] nalLengthData = nalLength.data; + nalLengthData[0] = 0; + nalLengthData[1] = 0; + nalLengthData[2] = 0; + int nalUnitLengthFieldLength = track.track.nalUnitLengthFieldLength; + int nalUnitLengthFieldLengthDiff = 4 - track.track.nalUnitLengthFieldLength; + // NAL units are length delimited, but the decoder requires start code delimited units. + // Loop until we've written the sample to the track output, replacing length delimiters with + // start codes as we encounter them. + while (sampleBytesWritten < sampleSize) { + if (sampleCurrentNalBytesRemaining == 0) { + // Read the NAL length so that we know where we find the next one. + input.readFully(nalLength.data, nalUnitLengthFieldLengthDiff, nalUnitLengthFieldLength); + nalLength.setPosition(0); + sampleCurrentNalBytesRemaining = nalLength.readUnsignedIntToInt(); + // Write a start code for the current NAL unit. + nalStartCode.setPosition(0); + trackOutput.sampleData(nalStartCode, 4); + sampleBytesWritten += 4; + sampleSize += nalUnitLengthFieldLengthDiff; + } else { + // Write the payload of the NAL unit. + int writtenBytes = trackOutput.sampleData(input, sampleCurrentNalBytesRemaining, false); + sampleBytesWritten += writtenBytes; + sampleCurrentNalBytesRemaining -= writtenBytes; + } + } + } else { + while (sampleBytesWritten < sampleSize) { + int writtenBytes = trackOutput.sampleData(input, sampleSize - sampleBytesWritten, false); + sampleBytesWritten += writtenBytes; + sampleCurrentNalBytesRemaining -= writtenBytes; + } + } + trackOutput.sampleMetadata(track.sampleTable.timestampsUs[sampleIndex], + track.sampleTable.flags[sampleIndex], sampleSize, 0, null); + track.sampleIndex++; + sampleBytesWritten = 0; + sampleCurrentNalBytesRemaining = 0; + return RESULT_CONTINUE; + } + + /** + * Returns the index of the track that contains the earliest current sample, or + * {@link TrackSampleTable#NO_SAMPLE} if no samples remain. + */ + private int getTrackIndexOfEarliestCurrentSample() { + int earliestSampleTrackIndex = TrackSampleTable.NO_SAMPLE; + long earliestSampleOffset = Long.MAX_VALUE; + for (int trackIndex = 0; trackIndex < tracks.length; trackIndex++) { + Mp4Track track = tracks[trackIndex]; + int sampleIndex = track.sampleIndex; + if (sampleIndex == track.sampleTable.sampleCount) { + continue; + } + + long trackSampleOffset = track.sampleTable.offsets[sampleIndex]; + if (trackSampleOffset < earliestSampleOffset) { + earliestSampleOffset = trackSampleOffset; + earliestSampleTrackIndex = trackIndex; + } + } + + return earliestSampleTrackIndex; + } + + /** + * Returns whether the extractor should parse a leaf atom with type {@code atom}. + */ + private static boolean shouldParseLeafAtom(int atom) { + return atom == Atom.TYPE_mdhd || atom == Atom.TYPE_mvhd || atom == Atom.TYPE_hdlr + || atom == Atom.TYPE_stsd || atom == Atom.TYPE_stts || atom == Atom.TYPE_stss + || atom == Atom.TYPE_ctts || atom == Atom.TYPE_elst || atom == Atom.TYPE_stsc + || atom == Atom.TYPE_stsz || atom == Atom.TYPE_stco || atom == Atom.TYPE_co64 + || atom == Atom.TYPE_tkhd || atom == Atom.TYPE_ftyp || atom == Atom.TYPE_udta; + } + + /** + * Returns whether the extractor should parse a container atom with type {@code atom}. + */ + private static boolean shouldParseContainerAtom(int atom) { + return atom == Atom.TYPE_moov || atom == Atom.TYPE_trak || atom == Atom.TYPE_mdia + || atom == Atom.TYPE_minf || atom == Atom.TYPE_stbl || atom == Atom.TYPE_edts; + } + + private static final class Mp4Track { + + public final Track track; + public final TrackSampleTable sampleTable; + public final TrackOutput trackOutput; + + public int sampleIndex; + + public Mp4Track(Track track, TrackSampleTable sampleTable, TrackOutput trackOutput) { + this.track = track; + this.sampleTable = sampleTable; + this.trackOutput = trackOutput; + } + + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp4/PsshAtomUtil.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp4/PsshAtomUtil.java new file mode 100755 index 000000000..e48076228 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp4/PsshAtomUtil.java @@ -0,0 +1,140 @@ +/* + * 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.messenger.exoplayer.extractor.mp4; + +import org.telegram.messenger.exoplayer.util.ParsableByteArray; + +import android.util.Log; +import android.util.Pair; + +import java.nio.ByteBuffer; +import java.util.UUID; + +/** + * Utility methods for handling PSSH atoms. + */ +public final class PsshAtomUtil { + + private static final String TAG = "PsshAtomUtil"; + + private PsshAtomUtil() {} + + /** + * Builds a PSSH atom for a given {@link UUID} containing the given scheme specific data. + * + * @param uuid The UUID of the scheme. + * @param data The scheme specific data. + * @return The PSSH atom. + */ + public static byte[] buildPsshAtom(UUID uuid, byte[] data) { + int psshBoxLength = Atom.FULL_HEADER_SIZE + 16 /* UUID */ + 4 /* DataSize */ + data.length; + ByteBuffer psshBox = ByteBuffer.allocate(psshBoxLength); + psshBox.putInt(psshBoxLength); + psshBox.putInt(Atom.TYPE_pssh); + psshBox.putInt(0 /* version=0, flags=0 */); + psshBox.putLong(uuid.getMostSignificantBits()); + psshBox.putLong(uuid.getLeastSignificantBits()); + psshBox.putInt(data.length); + psshBox.put(data); + return psshBox.array(); + } + + /** + * Parses the UUID from a PSSH atom. Version 0 and 1 PSSH atoms are supported. + *

+ * The UUID is only parsed if the data is a valid PSSH atom. + * + * @param atom The atom to parse. + * @return The parsed UUID. Null if the input is not a valid PSSH atom, or if the PSSH atom has + * an unsupported version. + */ + public static UUID parseUuid(byte[] atom) { + Pair parsedAtom = parsePsshAtom(atom); + if (parsedAtom == null) { + return null; + } + return parsedAtom.first; + } + + /** + * Parses the scheme specific data from a PSSH atom. Version 0 and 1 PSSH atoms are supported. + *

+ * The scheme specific data is only parsed if the data is a valid PSSH atom matching the given + * UUID, or if the data is a valid PSSH atom of any type in the case that the passed UUID is null. + * + * @param atom The atom to parse. + * @param uuid The required UUID of the PSSH atom, or null to accept any UUID. + * @return The parsed scheme specific data. Null if the input is not a valid PSSH atom, or if the + * PSSH atom has an unsupported version, or if the PSSH atom does not match the passed UUID. + */ + public static byte[] parseSchemeSpecificData(byte[] atom, UUID uuid) { + Pair parsedAtom = parsePsshAtom(atom); + if (parsedAtom == null) { + return null; + } + if (uuid != null && !uuid.equals(parsedAtom.first)) { + Log.w(TAG, "UUID mismatch. Expected: " + uuid + ", got: " + parsedAtom.first + "."); + return null; + } + return parsedAtom.second; + } + + /** + * Parses the UUID and scheme specific data from a PSSH atom. Version 0 and 1 PSSH atoms are + * supported. + * + * @param atom The atom to parse. + * @return A pair consisting of the parsed UUID and scheme specific data. Null if the input is + * not a valid PSSH atom, or if the PSSH atom has an unsupported version. + */ + private static Pair parsePsshAtom(byte[] atom) { + ParsableByteArray atomData = new ParsableByteArray(atom); + if (atomData.limit() < Atom.FULL_HEADER_SIZE + 16 /* UUID */ + 4 /* DataSize */) { + // Data too short. + return null; + } + atomData.setPosition(0); + int atomSize = atomData.readInt(); + if (atomSize != atomData.bytesLeft() + 4) { + // Not an atom, or incorrect atom size. + return null; + } + int atomType = atomData.readInt(); + if (atomType != Atom.TYPE_pssh) { + // Not an atom, or incorrect atom type. + return null; + } + int atomVersion = Atom.parseFullAtomVersion(atomData.readInt()); + if (atomVersion > 1) { + Log.w(TAG, "Unsupported pssh version: " + atomVersion); + return null; + } + UUID uuid = new UUID(atomData.readLong(), atomData.readLong()); + if (atomVersion == 1) { + int keyIdCount = atomData.readUnsignedIntToInt(); + atomData.skipBytes(16 * keyIdCount); + } + int dataSize = atomData.readUnsignedIntToInt(); + if (dataSize != atomData.bytesLeft()) { + // Incorrect dataSize. + return null; + } + byte[] data = new byte[dataSize]; + atomData.readBytes(data, 0, dataSize); + return Pair.create(uuid, data); + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp4/Sniffer.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp4/Sniffer.java new file mode 100755 index 000000000..3e9e5e1a4 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp4/Sniffer.java @@ -0,0 +1,189 @@ +/* + * 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.messenger.exoplayer.extractor.mp4; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.extractor.ExtractorInput; +import org.telegram.messenger.exoplayer.util.ParsableByteArray; +import org.telegram.messenger.exoplayer.util.Util; + +import java.io.IOException; + +/** + * Provides methods that peek data from an {@link ExtractorInput} and return whether the input + * appears to be in MP4 format. + */ +/* package */ final class Sniffer { + + /** + * The maximum number of bytes to peek when sniffing. + */ + private static final int SEARCH_LENGTH = 4 * 1024; + + private static final int[] COMPATIBLE_BRANDS = new int[] { + Util.getIntegerCodeForString("isom"), + Util.getIntegerCodeForString("iso2"), + Util.getIntegerCodeForString("iso3"), + Util.getIntegerCodeForString("iso4"), + Util.getIntegerCodeForString("iso5"), + Util.getIntegerCodeForString("iso6"), + Util.getIntegerCodeForString("avc1"), + Util.getIntegerCodeForString("hvc1"), + Util.getIntegerCodeForString("hev1"), + Util.getIntegerCodeForString("mp41"), + Util.getIntegerCodeForString("mp42"), + Util.getIntegerCodeForString("3g2a"), + Util.getIntegerCodeForString("3g2b"), + Util.getIntegerCodeForString("3gr6"), + Util.getIntegerCodeForString("3gs6"), + Util.getIntegerCodeForString("3ge6"), + Util.getIntegerCodeForString("3gg6"), + Util.getIntegerCodeForString("M4V "), + Util.getIntegerCodeForString("M4A "), + Util.getIntegerCodeForString("f4v "), + Util.getIntegerCodeForString("kddi"), + Util.getIntegerCodeForString("M4VP"), + Util.getIntegerCodeForString("qt "), // Apple QuickTime + Util.getIntegerCodeForString("MSNV"), // Sony PSP + }; + + /** + * Returns whether data peeked from the current position in {@code input} is consistent with the + * input being a fragmented MP4 file. + * + * @param input The extractor input from which to peek data. The peek position will be modified. + * @return True if the input appears to be in the fragmented MP4 format. False otherwise. + * @throws IOException If an error occurs reading from the input. + * @throws InterruptedException If the thread has been interrupted. + */ + public static boolean sniffFragmented(ExtractorInput input) + throws IOException, InterruptedException { + return sniffInternal(input, true); + } + + /** + * Returns whether data peeked from the current position in {@code input} is consistent with the + * input being an unfragmented MP4 file. + * + * @param input The extractor input from which to peek data. The peek position will be modified. + * @return True if the input appears to be in the unfragmented MP4 format. False otherwise. + * @throws IOException If an error occurs reading from the input. + * @throws InterruptedException If the thread has been interrupted. + */ + public static boolean sniffUnfragmented(ExtractorInput input) + throws IOException, InterruptedException { + return sniffInternal(input, false); + } + + private static boolean sniffInternal(ExtractorInput input, boolean fragmented) + throws IOException, InterruptedException { + long inputLength = input.getLength(); + int bytesToSearch = (int) (inputLength == C.LENGTH_UNBOUNDED || inputLength > SEARCH_LENGTH + ? SEARCH_LENGTH : inputLength); + + ParsableByteArray buffer = new ParsableByteArray(64); + int bytesSearched = 0; + boolean foundGoodFileType = false; + boolean isFragmented = false; + while (bytesSearched < bytesToSearch) { + // Read an atom header. + int headerSize = Atom.HEADER_SIZE; + input.peekFully(buffer.data, 0, headerSize); + buffer.setPosition(0); + long atomSize = buffer.readUnsignedInt(); + int atomType = buffer.readInt(); + if (atomSize == Atom.LONG_SIZE_PREFIX) { + headerSize = Atom.LONG_HEADER_SIZE; + input.peekFully(buffer.data, Atom.HEADER_SIZE, Atom.LONG_HEADER_SIZE - Atom.HEADER_SIZE); + atomSize = buffer.readUnsignedLongToLong(); + } + + if (atomSize < headerSize) { + // The file is invalid because the atom size is too small for its header. + return false; + } + bytesSearched += headerSize; + + if (atomType == Atom.TYPE_moov) { + // Check for an mvex atom inside the moov atom to identify whether the file is fragmented. + continue; + } + + if (atomType == Atom.TYPE_moof || atomType == Atom.TYPE_mvex) { + // The movie is fragmented. Stop searching as we must have read any ftyp atom already. + isFragmented = true; + break; + } + + if (bytesSearched + atomSize - headerSize >= bytesToSearch) { + // Stop searching as peeking this atom would exceed the search limit. + break; + } + + int atomDataSize = (int) (atomSize - headerSize); + bytesSearched += atomDataSize; + if (atomType == Atom.TYPE_ftyp) { + // Parse the atom and check the file type/brand is compatible with the extractors. + if (atomDataSize < 8) { + return false; + } + if (buffer.capacity() < atomDataSize) { + buffer.reset(new byte[atomDataSize], atomDataSize); + } + input.peekFully(buffer.data, 0, atomDataSize); + int brandsCount = atomDataSize / 4; + for (int i = 0; i < brandsCount; i++) { + if (i == 1) { + // This index refers to the minorVersion, not a brand, so skip it. + buffer.skipBytes(4); + } else if (isCompatibleBrand(buffer.readInt())) { + foundGoodFileType = true; + break; + } + } + if (!foundGoodFileType) { + // The types were not compatible and there is only one ftyp atom, so reject the file. + return false; + } + } else if (atomDataSize != 0) { + // Skip the atom. + input.advancePeekPosition(atomDataSize); + } + } + return foundGoodFileType && fragmented == isFragmented; + } + + /** + * Returns whether {@code brand} is an ftyp atom brand that is compatible with the MP4 extractors. + */ + private static boolean isCompatibleBrand(int brand) { + // Accept all brands starting '3gp'. + if (brand >>> 8 == Util.getIntegerCodeForString("3gp")) { + return true; + } + for (int compatibleBrand : COMPATIBLE_BRANDS) { + if (compatibleBrand == brand) { + return true; + } + } + return false; + } + + private Sniffer() { + // Prevent instantiation. + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp4/Track.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp4/Track.java new file mode 100755 index 000000000..2fce11a7a --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp4/Track.java @@ -0,0 +1,100 @@ +/* + * 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.messenger.exoplayer.extractor.mp4; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.MediaFormat; +import org.telegram.messenger.exoplayer.util.Util; + +/** + * Encapsulates information describing an MP4 track. + */ +public final class Track { + + public static final int TYPE_vide = Util.getIntegerCodeForString("vide"); + public static final int TYPE_soun = Util.getIntegerCodeForString("soun"); + public static final int TYPE_text = Util.getIntegerCodeForString("text"); + public static final int TYPE_sbtl = Util.getIntegerCodeForString("sbtl"); + public static final int TYPE_subt = Util.getIntegerCodeForString("subt"); + + /** + * The track identifier. + */ + public final int id; + + /** + * One of {@link #TYPE_vide}, {@link #TYPE_soun}, {@link #TYPE_text} and {@link #TYPE_sbtl} and + * {@link #TYPE_subt}. + */ + public final int type; + + /** + * The track timescale, defined as the number of time units that pass in one second. + */ + public final long timescale; + + /** + * The movie timescale. + */ + public final long movieTimescale; + + /** + * The duration of the track in microseconds, or {@link C#UNKNOWN_TIME_US} if unknown. + */ + public final long durationUs; + + /** + * The media format. + */ + public final MediaFormat mediaFormat; + + /** + * Track encryption boxes for the different track sample descriptions. Entries may be null. + */ + public final TrackEncryptionBox[] sampleDescriptionEncryptionBoxes; + + /** + * Durations of edit list segments in the movie timescale. Null if there is no edit list. + */ + public final long[] editListDurations; + + /** + * Media times for edit list segments in the track timescale. Null if there is no edit list. + */ + public final long[] editListMediaTimes; + + /** + * For H264 video tracks, the length in bytes of the NALUnitLength field in each sample. -1 for + * other track types. + */ + public final int nalUnitLengthFieldLength; + + public Track(int id, int type, long timescale, long movieTimescale, long durationUs, + MediaFormat mediaFormat, TrackEncryptionBox[] sampleDescriptionEncryptionBoxes, + int nalUnitLengthFieldLength, long[] editListDurations, long[] editListMediaTimes) { + this.id = id; + this.type = type; + this.timescale = timescale; + this.movieTimescale = movieTimescale; + this.durationUs = durationUs; + this.mediaFormat = mediaFormat; + this.sampleDescriptionEncryptionBoxes = sampleDescriptionEncryptionBoxes; + this.nalUnitLengthFieldLength = nalUnitLengthFieldLength; + this.editListDurations = editListDurations; + this.editListMediaTimes = editListMediaTimes; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp4/TrackEncryptionBox.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp4/TrackEncryptionBox.java new file mode 100755 index 000000000..b37c71622 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp4/TrackEncryptionBox.java @@ -0,0 +1,50 @@ +/* + * 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.messenger.exoplayer.extractor.mp4; + +/** + * Encapsulates information parsed from a track encryption (tenc) box in an MP4 stream. + */ +public final class TrackEncryptionBox { + + /** + * Indicates the encryption state of the samples in the sample group. + */ + public final boolean isEncrypted; + + /** + * The initialization vector size in bytes for the samples in the corresponding sample group. + */ + public final int initializationVectorSize; + + /** + * The key identifier for the samples in the corresponding sample group. + */ + public final byte[] keyId; + + /** + * @param isEncrypted Indicates the encryption state of the samples in the sample group. + * @param initializationVectorSize The initialization vector size in bytes for the samples in the + * corresponding sample group. + * @param keyId The key identifier for the samples in the corresponding sample group. + */ + public TrackEncryptionBox(boolean isEncrypted, int initializationVectorSize, byte[] keyId) { + this.isEncrypted = isEncrypted; + this.initializationVectorSize = initializationVectorSize; + this.keyId = keyId; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp4/TrackFragment.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp4/TrackFragment.java new file mode 100755 index 000000000..d0a63281a --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp4/TrackFragment.java @@ -0,0 +1,167 @@ +/* + * 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.messenger.exoplayer.extractor.mp4; + +import org.telegram.messenger.exoplayer.extractor.ExtractorInput; +import org.telegram.messenger.exoplayer.util.ParsableByteArray; + +import java.io.IOException; + +/** + * A holder for information corresponding to a single fragment of an mp4 file. + */ +/* package */ final class TrackFragment { + + /** + * The default values for samples from the track fragment header. + */ + public DefaultSampleValues header; + /** + * The position (byte offset) of the start of sample data. + */ + public long dataPosition; + /** + * The position (byte offset) of the start of auxiliary data. + */ + public long auxiliaryDataPosition; + /** + * The number of samples contained by the fragment. + */ + public int length; + /** + * The size of each sample in the run. + */ + public int[] sampleSizeTable; + /** + * The composition time offset of each sample in the run. + */ + public int[] sampleCompositionTimeOffsetTable; + /** + * The decoding time of each sample in the run. + */ + public long[] sampleDecodingTimeTable; + /** + * Indicates which samples are sync frames. + */ + public boolean[] sampleIsSyncFrameTable; + /** + * True if the fragment defines encryption data. False otherwise. + */ + public boolean definesEncryptionData; + /** + * If {@link #definesEncryptionData} is true, indicates which samples use sub-sample encryption. + * Undefined otherwise. + */ + public boolean[] sampleHasSubsampleEncryptionTable; + /** + * If {@link #definesEncryptionData} is true, indicates the length of the sample encryption data. + * Undefined otherwise. + */ + public int sampleEncryptionDataLength; + /** + * If {@link #definesEncryptionData} is true, contains binary sample encryption data. Undefined + * otherwise. + */ + public ParsableByteArray sampleEncryptionData; + /** + * Whether {@link #sampleEncryptionData} needs populating with the actual encryption data. + */ + public boolean sampleEncryptionDataNeedsFill; + /** + * The absolute decode time of the start of the next fragment. + */ + public long nextFragmentDecodeTime; + + /** + * Resets the fragment. + *

+ * {@link #length} and {@link #nextFragmentDecodeTime} are set to 0, and both + * {@link #definesEncryptionData} and {@link #sampleEncryptionDataNeedsFill} is set to false. + */ + public void reset() { + length = 0; + nextFragmentDecodeTime = 0; + definesEncryptionData = false; + sampleEncryptionDataNeedsFill = false; + } + + /** + * Configures the fragment for the specified number of samples. + *

+ * The {@link #length} of the fragment is set to the specified sample count, and the contained + * tables are resized if necessary such that they are at least this length. + * + * @param sampleCount The number of samples in the new run. + */ + public void initTables(int sampleCount) { + length = sampleCount; + if (sampleSizeTable == null || sampleSizeTable.length < length) { + // Size the tables 25% larger than needed, so as to make future resize operations less + // likely. The choice of 25% is relatively arbitrary. + int tableSize = (sampleCount * 125) / 100; + sampleSizeTable = new int[tableSize]; + sampleCompositionTimeOffsetTable = new int[tableSize]; + sampleDecodingTimeTable = new long[tableSize]; + sampleIsSyncFrameTable = new boolean[tableSize]; + sampleHasSubsampleEncryptionTable = new boolean[tableSize]; + } + } + + /** + * Configures the fragment to be one that defines encryption data of the specified length. + *

+ * {@link #definesEncryptionData} is set to true, {@link #sampleEncryptionDataLength} is set to + * the specified length, and {@link #sampleEncryptionData} is resized if necessary such that it + * is at least this length. + * + * @param length The length in bytes of the encryption data. + */ + public void initEncryptionData(int length) { + if (sampleEncryptionData == null || sampleEncryptionData.limit() < length) { + sampleEncryptionData = new ParsableByteArray(length); + } + sampleEncryptionDataLength = length; + definesEncryptionData = true; + sampleEncryptionDataNeedsFill = true; + } + + /** + * Fills {@link #sampleEncryptionData} from the provided input. + * + * @param input An {@link ExtractorInput} from which to read the encryption data. + */ + public void fillEncryptionData(ExtractorInput input) throws IOException, InterruptedException { + input.readFully(sampleEncryptionData.data, 0, sampleEncryptionDataLength); + sampleEncryptionData.setPosition(0); + sampleEncryptionDataNeedsFill = false; + } + + /** + * Fills {@link #sampleEncryptionData} from the provided source. + * + * @param source A source from which to read the encryption data. + */ + public void fillEncryptionData(ParsableByteArray source) { + source.readBytes(sampleEncryptionData.data, 0, sampleEncryptionDataLength); + sampleEncryptionData.setPosition(0); + sampleEncryptionDataNeedsFill = false; + } + + public long getSamplePresentationTime(int index) { + return sampleDecodingTimeTable[index] + sampleCompositionTimeOffsetTable[index]; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp4/TrackSampleTable.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp4/TrackSampleTable.java new file mode 100755 index 000000000..b80a22b59 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/mp4/TrackSampleTable.java @@ -0,0 +1,106 @@ +/* + * 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.messenger.exoplayer.extractor.mp4; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.util.Assertions; +import org.telegram.messenger.exoplayer.util.Util; + +/** + * Sample table for a track in an MP4 file. + */ +/* package */ final class TrackSampleTable { + + /** + * Sample index when no sample is available. + */ + public static final int NO_SAMPLE = -1; + + /** + * Number of samples. + */ + public final int sampleCount; + /** + * Sample offsets in bytes. + */ + public final long[] offsets; + /** + * Sample sizes in bytes. + */ + public final int[] sizes; + /** + * Maximum sample size in {@link #sizes}. + */ + public final int maximumSize; + /** + * Sample timestamps in microseconds. + */ + public final long[] timestampsUs; + /** + * Sample flags. + */ + public final int[] flags; + + TrackSampleTable(long[] offsets, int[] sizes, int maximumSize, long[] timestampsUs, int[] flags) { + Assertions.checkArgument(sizes.length == timestampsUs.length); + Assertions.checkArgument(offsets.length == timestampsUs.length); + Assertions.checkArgument(flags.length == timestampsUs.length); + + this.offsets = offsets; + this.sizes = sizes; + this.maximumSize = maximumSize; + this.timestampsUs = timestampsUs; + this.flags = flags; + sampleCount = offsets.length; + } + + /** + * Returns the sample index of the closest synchronization sample at or before the given + * timestamp, if one is available. + * + * @param timeUs Timestamp adjacent to which to find a synchronization sample. + * @return Index of the synchronization sample, or {@link #NO_SAMPLE} if none. + */ + public int getIndexOfEarlierOrEqualSynchronizationSample(long timeUs) { + // Video frame timestamps may not be sorted, so the behavior of this call can be undefined. + // Frames are not reordered past synchronization samples so this works in practice. + int startIndex = Util.binarySearchFloor(timestampsUs, timeUs, true, false); + for (int i = startIndex; i >= 0; i--) { + if ((flags[i] & C.SAMPLE_FLAG_SYNC) != 0) { + return i; + } + } + return NO_SAMPLE; + } + + /** + * Returns the sample index of the closest synchronization sample at or after the given timestamp, + * if one is available. + * + * @param timeUs Timestamp adjacent to which to find a synchronization sample. + * @return index Index of the synchronization sample, or {@link #NO_SAMPLE} if none. + */ + public int getIndexOfLaterOrEqualSynchronizationSample(long timeUs) { + int startIndex = Util.binarySearchCeil(timestampsUs, timeUs, true, false); + for (int i = startIndex; i < timestampsUs.length; i++) { + if ((flags[i] & C.SAMPLE_FLAG_SYNC) != 0) { + return i; + } + } + return NO_SAMPLE; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ogg/FlacReader.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ogg/FlacReader.java new file mode 100755 index 000000000..36db8d009 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ogg/FlacReader.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2016 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.messenger.exoplayer.extractor.ogg; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.MediaFormat; +import org.telegram.messenger.exoplayer.extractor.Extractor; +import org.telegram.messenger.exoplayer.extractor.ExtractorInput; +import org.telegram.messenger.exoplayer.extractor.PositionHolder; +import org.telegram.messenger.exoplayer.extractor.SeekMap; +import org.telegram.messenger.exoplayer.util.FlacSeekTable; +import org.telegram.messenger.exoplayer.util.FlacStreamInfo; +import org.telegram.messenger.exoplayer.util.FlacUtil; +import org.telegram.messenger.exoplayer.util.MimeTypes; +import org.telegram.messenger.exoplayer.util.ParsableByteArray; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * {@link StreamReader} to extract Flac data out of Ogg byte stream. + */ +/* package */ final class FlacReader extends StreamReader { + + private static final byte AUDIO_PACKET_TYPE = (byte) 0xFF; + private static final byte SEEKTABLE_PACKET_TYPE = 0x03; + + private FlacStreamInfo streamInfo; + + private FlacSeekTable seekTable; + + private boolean firstAudioPacketProcessed; + + /* package */ static boolean verifyBitstreamType(ParsableByteArray data) { + return data.readUnsignedByte() == 0x7F && // packet type + data.readUnsignedInt() == 0x464C4143; // ASCII signature "FLAC" + } + + @Override + public int read(ExtractorInput input, PositionHolder seekPosition) + throws IOException, InterruptedException { + long position = input.getPosition(); + + if (!oggParser.readPacket(input, scratch)) { + return Extractor.RESULT_END_OF_INPUT; + } + + byte[] data = scratch.data; + if (streamInfo == null) { + streamInfo = new FlacStreamInfo(data, 17); + + byte[] metadata = Arrays.copyOfRange(data, 9, scratch.limit()); + metadata[4] = (byte) 0x80; // Set the last metadata block flag, ignore the other blocks + List initializationData = Collections.singletonList(metadata); + + MediaFormat mediaFormat = MediaFormat.createAudioFormat(null, MimeTypes.AUDIO_FLAC, + streamInfo.bitRate(), MediaFormat.NO_VALUE, streamInfo.durationUs(), + streamInfo.channels, streamInfo.sampleRate, initializationData, null); + trackOutput.format(mediaFormat); + + } else if (data[0] == AUDIO_PACKET_TYPE) { + if (!firstAudioPacketProcessed) { + if (seekTable != null) { + extractorOutput.seekMap(seekTable.createSeekMap(position, streamInfo.sampleRate)); + seekTable = null; + } else { + extractorOutput.seekMap(SeekMap.UNSEEKABLE); + } + firstAudioPacketProcessed = true; + } + + trackOutput.sampleData(scratch, scratch.limit()); + scratch.setPosition(0); + long timeUs = FlacUtil.extractSampleTimestamp(streamInfo, scratch); + trackOutput.sampleMetadata(timeUs, C.SAMPLE_FLAG_SYNC, scratch.limit(), 0, null); + + } else if ((data[0] & 0x7F) == SEEKTABLE_PACKET_TYPE && seekTable == null) { + seekTable = FlacSeekTable.parseSeekTable(scratch); + } + + scratch.reset(); + return Extractor.RESULT_CONTINUE; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ogg/OggExtractor.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ogg/OggExtractor.java new file mode 100755 index 000000000..45342a265 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ogg/OggExtractor.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2015 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.messenger.exoplayer.extractor.ogg; + +import org.telegram.messenger.exoplayer.ParserException; +import org.telegram.messenger.exoplayer.extractor.Extractor; +import org.telegram.messenger.exoplayer.extractor.ExtractorInput; +import org.telegram.messenger.exoplayer.extractor.ExtractorOutput; +import org.telegram.messenger.exoplayer.extractor.PositionHolder; +import org.telegram.messenger.exoplayer.extractor.TrackOutput; +import org.telegram.messenger.exoplayer.util.ParsableByteArray; + +import java.io.IOException; + +/** + * Ogg {@link Extractor}. + */ +public class OggExtractor implements Extractor { + + private StreamReader streamReader; + + @Override + public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + try { + ParsableByteArray scratch = new ParsableByteArray(new byte[OggUtil.PAGE_HEADER_SIZE], 0); + OggUtil.PageHeader header = new OggUtil.PageHeader(); + if (!OggUtil.populatePageHeader(input, header, scratch, true) + || (header.type & 0x02) != 0x02 || header.bodySize < 7) { + return false; + } + scratch.reset(); + input.peekFully(scratch.data, 0, 7); + if (FlacReader.verifyBitstreamType(scratch)) { + streamReader = new FlacReader(); + } else { + scratch.reset(); + if (VorbisReader.verifyBitstreamType(scratch)) { + streamReader = new VorbisReader(); + } else { + return false; + } + } + return true; + } catch (ParserException e) { + // does not happen + } finally { + } + return false; + } + + @Override + public void init(ExtractorOutput output) { + TrackOutput trackOutput = output.track(0); + output.endTracks(); + streamReader.init(output, trackOutput); + } + + @Override + public void seek() { + streamReader.seek(); + } + + @Override + public void release() { + // Do nothing + } + + @Override + public int read(ExtractorInput input, PositionHolder seekPosition) + throws IOException, InterruptedException { + return streamReader.read(input, seekPosition); + } +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ogg/OggParser.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ogg/OggParser.java new file mode 100755 index 000000000..540c5cd63 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ogg/OggParser.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2015 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.messenger.exoplayer.extractor.ogg; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.ParserException; +import org.telegram.messenger.exoplayer.extractor.ExtractorInput; +import org.telegram.messenger.exoplayer.extractor.ogg.OggUtil.PacketInfoHolder; +import org.telegram.messenger.exoplayer.util.Assertions; +import org.telegram.messenger.exoplayer.util.ParsableByteArray; + +import java.io.IOException; + +/** + * Reads OGG packets from an {@link ExtractorInput}. + */ +/* package */ final class OggParser { + + public static final int OGG_MAX_SEGMENT_SIZE = 255; + + private final OggUtil.PageHeader pageHeader = new OggUtil.PageHeader(); + private final ParsableByteArray headerArray = new ParsableByteArray(27 + 255); + private final PacketInfoHolder holder = new PacketInfoHolder(); + + private int currentSegmentIndex = -1; + private long elapsedSamples; + + /** + * Resets this reader. + */ + public void reset() { + pageHeader.reset(); + headerArray.reset(); + currentSegmentIndex = -1; + } + + /** + * Reads the next packet of the ogg stream. In case of an {@code IOException} the caller must make + * sure to pass the same instance of {@code ParsableByteArray} to this method again so this reader + * can resume properly from an error while reading a continued packet spanned across multiple + * pages. + * + * @param input the {@link ExtractorInput} to read data from. + * @param packetArray the {@link ParsableByteArray} to write the packet data into. + * @return {@code true} if the read was successful. {@code false} if the end of the input was + * encountered having read no data. + * @throws IOException thrown if reading from the input fails. + * @throws InterruptedException thrown if interrupted while reading from input. + */ + public boolean readPacket(ExtractorInput input, ParsableByteArray packetArray) + throws IOException, InterruptedException { + Assertions.checkState(input != null && packetArray != null); + + boolean packetComplete = false; + while (!packetComplete) { + if (currentSegmentIndex < 0) { + // We're at the start of a page. + if (!OggUtil.populatePageHeader(input, pageHeader, headerArray, true)) { + return false; + } + int segmentIndex = 0; + int bytesToSkip = pageHeader.headerSize; + if ((pageHeader.type & 0x01) == 0x01 && packetArray.limit() == 0) { + // After seeking, the first packet may be the remainder + // part of a continued packet which has to be discarded. + OggUtil.calculatePacketSize(pageHeader, segmentIndex, holder); + segmentIndex += holder.segmentCount; + bytesToSkip += holder.size; + } + input.skipFully(bytesToSkip); + currentSegmentIndex = segmentIndex; + } + + OggUtil.calculatePacketSize(pageHeader, currentSegmentIndex, holder); + int segmentIndex = currentSegmentIndex + holder.segmentCount; + if (holder.size > 0) { + input.readFully(packetArray.data, packetArray.limit(), holder.size); + packetArray.setLimit(packetArray.limit() + holder.size); + packetComplete = pageHeader.laces[segmentIndex - 1] != 255; + } + // advance now since we are sure reading didn't throw an exception + currentSegmentIndex = segmentIndex == pageHeader.pageSegmentCount ? -1 + : segmentIndex; + } + return true; + } + + /** + * Skips to the last Ogg page in the stream and reads the header's granule field which is the + * total number of samples per channel. + * + * @param input The {@link ExtractorInput} to read from. + * @return the total number of samples of this input. + * @throws IOException thrown if reading from the input fails. + * @throws InterruptedException thrown if interrupted while reading from the input. + */ + public long readGranuleOfLastPage(ExtractorInput input) + throws IOException, InterruptedException { + Assertions.checkArgument(input.getLength() != C.LENGTH_UNBOUNDED); // never read forever! + OggUtil.skipToNextPage(input); + pageHeader.reset(); + while ((pageHeader.type & 0x04) != 0x04 && input.getPosition() < input.getLength()) { + OggUtil.populatePageHeader(input, pageHeader, headerArray, false); + input.skipFully(pageHeader.headerSize + pageHeader.bodySize); + } + return pageHeader.granulePosition; + } + + /** + * Skips to the position of the start of the page containing the {@code targetGranule} and + * returns the elapsed samples which is the granule of the page previous to the target page. + *

+ * Note that the position of the {@code input} must be before the start of the page previous to + * the page containing the targetGranule to get the correct number of elapsed samples. + * Which is in short like: {@code pos(input) <= pos(targetPage.pageSequence - 1)}. + * + * @param input the {@link ExtractorInput} to read from. + * @param targetGranule the target granule (number of frames per channel). + * @return the number of elapsed samples at the start of the target page. + * @throws ParserException thrown if populating the page header fails. + * @throws IOException thrown if reading from the input fails. + * @throws InterruptedException thrown if interrupted while reading from the input. + */ + public long skipToPageOfGranule(ExtractorInput input, long targetGranule) + throws IOException, InterruptedException { + OggUtil.skipToNextPage(input); + OggUtil.populatePageHeader(input, pageHeader, headerArray, false); + while (pageHeader.granulePosition < targetGranule) { + input.skipFully(pageHeader.headerSize + pageHeader.bodySize); + // Store in a member field to be able to resume after IOExceptions. + elapsedSamples = pageHeader.granulePosition; + // Peek next header. + OggUtil.populatePageHeader(input, pageHeader, headerArray, false); + } + if (elapsedSamples == 0) { + throw new ParserException(); + } + input.resetPeekPosition(); + long returnValue = elapsedSamples; + // Reset member state. + elapsedSamples = 0; + currentSegmentIndex = -1; + return returnValue; + } + + /** + * Returns the {@link OggUtil.PageHeader} of the current page. The header might not have been + * populated if the first packet has yet to be read. + *

+ * Note that there is only a single instance of {@code OggParser.PageHeader} which is mutable. + * The value of the fields might be changed by the reader when reading the stream advances and + * the next page is read (which implies reading and populating the next header). + * + * @return the {@code PageHeader} of the current page or {@code null}. + */ + public OggUtil.PageHeader getPageHeader() { + return pageHeader; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ogg/OggSeeker.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ogg/OggSeeker.java new file mode 100755 index 000000000..1d93557f4 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ogg/OggSeeker.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2015 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.messenger.exoplayer.extractor.ogg; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.extractor.ExtractorInput; +import org.telegram.messenger.exoplayer.util.Assertions; +import org.telegram.messenger.exoplayer.util.ParsableByteArray; + +import java.io.IOException; + +/** + * Used to seek in an Ogg stream. + */ +/* package */ final class OggSeeker { + + private static final int MATCH_RANGE = 72000; + + private final OggUtil.PageHeader pageHeader = new OggUtil.PageHeader(); + private final ParsableByteArray headerArray = new ParsableByteArray(27 + 255); + private long audioDataLength = C.LENGTH_UNBOUNDED; + private long totalSamples; + + /** + * Setup the seeker with the data it needs to to an educated guess of seeking positions. + * + * @param audioDataLength the length of the audio data (total bytes - header bytes). + * @param totalSamples the total number of samples of audio data. + */ + public void setup(long audioDataLength, long totalSamples) { + Assertions.checkArgument(audioDataLength > 0 && totalSamples > 0); + this.audioDataLength = audioDataLength; + this.totalSamples = totalSamples; + } + + /** + * Returns a position converging to the {@code targetGranule} to which the {@link ExtractorInput} + * has to seek and then be passed for another call until -1 is return. If -1 is returned the + * input is at a position which is before the start of the page before the target page and at + * which it is sensible to just skip pages to the target granule and pre-roll instead of doing + * another seek request. + * + * @param targetGranule the target granule position to seek to. + * @param input the {@link ExtractorInput} to read from. + * @return the position to seek the {@link ExtractorInput} to for a next call or -1 if it's close + * enough to skip to the target page. + * @throws IOException thrown if reading from the input fails. + * @throws InterruptedException thrown if interrupted while reading from the input. + */ + public long getNextSeekPosition(long targetGranule, ExtractorInput input) + throws IOException, InterruptedException { + Assertions.checkState(audioDataLength != C.LENGTH_UNBOUNDED && totalSamples != 0); + OggUtil.populatePageHeader(input, pageHeader, headerArray, false); + long granuleDistance = targetGranule - pageHeader.granulePosition; + if (granuleDistance <= 0 || granuleDistance > MATCH_RANGE) { + // estimated position too high or too low + long offset = (pageHeader.bodySize + pageHeader.headerSize) + * (granuleDistance <= 0 ? 2 : 1); + return input.getPosition() - offset + (granuleDistance * audioDataLength / totalSamples); + } + // position accepted (below target granule and within MATCH_RANGE) + input.resetPeekPosition(); + return -1; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ogg/OggUtil.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ogg/OggUtil.java new file mode 100755 index 000000000..9840abcc6 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ogg/OggUtil.java @@ -0,0 +1,211 @@ +/* + * Copyright (C) 2015 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.messenger.exoplayer.extractor.ogg; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.ParserException; +import org.telegram.messenger.exoplayer.extractor.ExtractorInput; +import org.telegram.messenger.exoplayer.util.ParsableByteArray; +import org.telegram.messenger.exoplayer.util.Util; + +import java.io.EOFException; +import java.io.IOException; + +/** + * Utility methods for reading ogg streams. + */ +/* package */ final class OggUtil { + + public static final int PAGE_HEADER_SIZE = 27; + + private static final int TYPE_OGGS = Util.getIntegerCodeForString("OggS"); + + /** + * Reads an int of {@code length} bits from {@code src} starting at + * {@code leastSignificantBitIndex}. + * + * @param src the {@code byte} to read from. + * @param length the length in bits of the int to read. + * @param leastSignificantBitIndex the index of the least significant bit of the int to read. + * @return the int value read. + */ + public static int readBits(byte src, int length, int leastSignificantBitIndex) { + return (src >> leastSignificantBitIndex) & (255 >>> (8 - length)); + } + + /** + * Skips to the next page. + * + * @param input The {@code ExtractorInput} to skip to the next page. + * @throws IOException thrown if peeking/reading from the input fails. + * @throws InterruptedException thrown if interrupted while peeking/reading from the input. + */ + public static void skipToNextPage(ExtractorInput input) + throws IOException, InterruptedException { + + byte[] buffer = new byte[2048]; + int peekLength = buffer.length; + while (true) { + if (input.getLength() != C.LENGTH_UNBOUNDED + && input.getPosition() + peekLength > input.getLength()) { + // Make sure to not peek beyond the end of the input. + peekLength = (int) (input.getLength() - input.getPosition()); + if (peekLength < 4) { + // Not found until eof. + throw new EOFException(); + } + } + input.peekFully(buffer, 0, peekLength, false); + for (int i = 0; i < peekLength - 3; i++) { + if (buffer[i] == 'O' && buffer[i + 1] == 'g' && buffer[i + 2] == 'g' + && buffer[i + 3] == 'S') { + // Match! Skip to the start of the pattern. + input.skipFully(i); + return; + } + } + // Overlap by not skipping the entire peekLength. + input.skipFully(peekLength - 3); + } + } + + /** + * Peeks an Ogg page header and stores the data in the {@code header} object passed + * as argument. + * + * @param input the {@link ExtractorInput} to read from. + * @param header the {@link PageHeader} to read from. + * @param scratch a scratch array temporary use. Its size should be at least PAGE_HEADER_SIZE + * @param quite if {@code true} no Exceptions are thrown but {@code false} is return if something + * goes wrong. + * @return {@code true} if the read was successful. {@code false} if the end of the + * input was encountered having read no data. + * @throws IOException thrown if reading data fails or the stream is invalid. + * @throws InterruptedException thrown if thread is interrupted when reading/peeking. + */ + public static boolean populatePageHeader(ExtractorInput input, PageHeader header, + ParsableByteArray scratch, boolean quite) throws IOException, InterruptedException { + + scratch.reset(); + header.reset(); + boolean hasEnoughBytes = input.getLength() == C.LENGTH_UNBOUNDED + || input.getLength() - input.getPeekPosition() >= PAGE_HEADER_SIZE; + if (!hasEnoughBytes || !input.peekFully(scratch.data, 0, PAGE_HEADER_SIZE, true)) { + if (quite) { + return false; + } else { + throw new EOFException(); + } + } + if (scratch.readUnsignedInt() != TYPE_OGGS) { + if (quite) { + return false; + } else { + throw new ParserException("expected OggS capture pattern at begin of page"); + } + } + + header.revision = scratch.readUnsignedByte(); + if (header.revision != 0x00) { + if (quite) { + return false; + } else { + throw new ParserException("unsupported bit stream revision"); + } + } + header.type = scratch.readUnsignedByte(); + + header.granulePosition = scratch.readLittleEndianLong(); + header.streamSerialNumber = scratch.readLittleEndianUnsignedInt(); + header.pageSequenceNumber = scratch.readLittleEndianUnsignedInt(); + header.pageChecksum = scratch.readLittleEndianUnsignedInt(); + header.pageSegmentCount = scratch.readUnsignedByte(); + + scratch.reset(); + // calculate total size of header including laces + header.headerSize = PAGE_HEADER_SIZE + header.pageSegmentCount; + input.peekFully(scratch.data, 0, header.pageSegmentCount); + for (int i = 0; i < header.pageSegmentCount; i++) { + header.laces[i] = scratch.readUnsignedByte(); + header.bodySize += header.laces[i]; + } + return true; + } + + /** + * Calculates the size of the packet starting from {@code startSegmentIndex}. + * + * @param header the {@link PageHeader} with laces. + * @param startSegmentIndex the index of the first segment of the packet. + * @param holder a position holder to store the resulting size value. + */ + public static void calculatePacketSize(PageHeader header, int startSegmentIndex, + PacketInfoHolder holder) { + holder.segmentCount = 0; + holder.size = 0; + while (startSegmentIndex + holder.segmentCount < header.pageSegmentCount) { + int segmentLength = header.laces[startSegmentIndex + holder.segmentCount++]; + holder.size += segmentLength; + if (segmentLength != 255) { + // packets end at first lace < 255 + break; + } + } + } + + /** + * Data object to store header information. Be aware that {@code laces.length} is always 255. + * Instead use {@code pageSegmentCount} to iterate. + */ + public static final class PageHeader { + + public int revision; + public int type; + public long granulePosition; + public long streamSerialNumber; + public long pageSequenceNumber; + public long pageChecksum; + public int pageSegmentCount; + public int headerSize; + public int bodySize; + public final int[] laces = new int[255]; + + /** + * Resets all primitive member fields to zero. + */ + public void reset() { + revision = 0; + type = 0; + granulePosition = 0; + streamSerialNumber = 0; + pageSequenceNumber = 0; + pageChecksum = 0; + pageSegmentCount = 0; + headerSize = 0; + bodySize = 0; + } + + } + + /** + * Holds size and number of segments of a packet. + */ + public static class PacketInfoHolder { + public int size; + public int segmentCount; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ogg/StreamReader.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ogg/StreamReader.java new file mode 100755 index 000000000..c50acdded --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ogg/StreamReader.java @@ -0,0 +1,44 @@ +package org.telegram.messenger.exoplayer.extractor.ogg; + +import org.telegram.messenger.exoplayer.extractor.Extractor; +import org.telegram.messenger.exoplayer.extractor.ExtractorInput; +import org.telegram.messenger.exoplayer.extractor.ExtractorOutput; +import org.telegram.messenger.exoplayer.extractor.PositionHolder; +import org.telegram.messenger.exoplayer.extractor.TrackOutput; +import org.telegram.messenger.exoplayer.util.ParsableByteArray; + +import java.io.IOException; + +/** + * StreamReader abstract class. + */ +/* package */ abstract class StreamReader { + + protected final ParsableByteArray scratch = new ParsableByteArray( + new byte[OggParser.OGG_MAX_SEGMENT_SIZE * 255], 0); + + protected final OggParser oggParser = new OggParser(); + + protected TrackOutput trackOutput; + + protected ExtractorOutput extractorOutput; + + void init(ExtractorOutput output, TrackOutput trackOutput) { + this.extractorOutput = output; + this.trackOutput = trackOutput; + } + + /** + * @see Extractor#seek() + */ + void seek() { + oggParser.reset(); + scratch.reset(); + } + + /** + * @see Extractor#read(ExtractorInput, PositionHolder) + */ + abstract int read(ExtractorInput input, PositionHolder seekPosition) + throws IOException, InterruptedException; +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ogg/VorbisBitArray.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ogg/VorbisBitArray.java new file mode 100755 index 000000000..148421005 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ogg/VorbisBitArray.java @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2015 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.messenger.exoplayer.extractor.ogg; + +import org.telegram.messenger.exoplayer.util.Assertions; + +/** + * Wraps a byte array, providing methods that allow it to be read as a vorbis bitstream. + * + * @see Vorbis bitpacking + * specification + */ +/* package */ final class VorbisBitArray { + + public final byte[] data; + private int limit; + private int byteOffset; + private int bitOffset; + + /** + * Creates a new instance that wraps an existing array. + * + * @param data the array to wrap. + */ + public VorbisBitArray(byte[] data) { + this(data, data.length); + } + + /** + * Creates a new instance that wraps an existing array. + * + * @param data the array to wrap. + * @param limit the limit in bytes. + */ + public VorbisBitArray(byte[] data, int limit) { + this.data = data; + this.limit = limit * 8; + } + + /** Resets the reading position to zero. */ + public void reset() { + byteOffset = 0; + bitOffset = 0; + } + + /** + * Reads a single bit. + * + * @return {@code true} if the bit is set, {@code false} otherwise. + */ + public boolean readBit() { + return readBits(1) == 1; + } + + /** + * Reads up to 32 bits. + * + * @param numBits The number of bits to read. + * @return An int whose bottom {@code numBits} bits hold the read data. + */ + public int readBits(int numBits) { + Assertions.checkState(getPosition() + numBits <= limit); + if (numBits == 0) { + return 0; + } + int result = 0; + int bitCount = 0; + if (bitOffset != 0) { + bitCount = Math.min(numBits, 8 - bitOffset); + int mask = 0xFF >>> (8 - bitCount); + result = (data[byteOffset] >>> bitOffset) & mask; + bitOffset += bitCount; + if (bitOffset == 8) { + byteOffset++; + bitOffset = 0; + } + } + + if (numBits - bitCount > 7) { + int numBytes = (numBits - bitCount) / 8; + for (int i = 0; i < numBytes; i++) { + result |= (data[byteOffset++] & 0xFFL) << bitCount; + bitCount += 8; + } + } + + if (numBits > bitCount) { + int bitsOnNextByte = numBits - bitCount; + int mask = 0xFF >>> (8 - bitsOnNextByte); + result |= (data[byteOffset] & mask) << bitCount; + bitOffset += bitsOnNextByte; + } + return result; + } + + /** + * Skips {@code numberOfBits} bits. + * + * @param numberOfBits the number of bits to skip. + */ + public void skipBits(int numberOfBits) { + Assertions.checkState(getPosition() + numberOfBits <= limit); + byteOffset += numberOfBits / 8; + bitOffset += numberOfBits % 8; + if (bitOffset > 7) { + byteOffset++; + bitOffset -= 8; + } + } + + /** + * Gets the current reading position in bits. + * + * @return the current reading position in bits. + */ + public int getPosition() { + return byteOffset * 8 + bitOffset; + } + + /** + * Sets the index of the current reading position in bits. + * + * @param position the new reading position in bits. + */ + public void setPosition(int position) { + Assertions.checkArgument(position < limit && position >= 0); + byteOffset = position / 8; + bitOffset = position - (byteOffset * 8); + } + + /** + * Gets the number of remaining bits. + * + * @return number of remaining bits. + */ + public int bitsLeft() { + return limit - getPosition(); + } + + /** + * Returns the limit in bits. + * + * @return the limit in bits. + **/ + public int limit() { + return limit; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ogg/VorbisReader.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ogg/VorbisReader.java new file mode 100755 index 000000000..c70c33e8a --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ogg/VorbisReader.java @@ -0,0 +1,247 @@ +/* + * Copyright (C) 2015 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.messenger.exoplayer.extractor.ogg; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.MediaFormat; +import org.telegram.messenger.exoplayer.ParserException; +import org.telegram.messenger.exoplayer.extractor.Extractor; +import org.telegram.messenger.exoplayer.extractor.ExtractorInput; +import org.telegram.messenger.exoplayer.extractor.PositionHolder; +import org.telegram.messenger.exoplayer.extractor.SeekMap; +import org.telegram.messenger.exoplayer.extractor.ogg.VorbisUtil.Mode; +import org.telegram.messenger.exoplayer.util.MimeTypes; +import org.telegram.messenger.exoplayer.util.ParsableByteArray; + +import java.io.IOException; +import java.util.ArrayList; + +/** + * {@link StreamReader} to extract Vorbis data out of Ogg byte stream. + */ +/* package */ final class VorbisReader extends StreamReader implements SeekMap { + + private static final long LARGEST_EXPECTED_PAGE_SIZE = 8000; + + private VorbisSetup vorbisSetup; + private int previousPacketBlockSize; + private long elapsedSamples; + private boolean seenFirstAudioPacket; + + private final OggSeeker oggSeeker = new OggSeeker(); + private long targetGranule = -1; + + private VorbisUtil.VorbisIdHeader vorbisIdHeader; + private VorbisUtil.CommentHeader commentHeader; + private long inputLength; + private long audioStartPosition; + private long totalSamples; + private long duration; + + /* package */ static boolean verifyBitstreamType(ParsableByteArray data) { + try { + return VorbisUtil.verifyVorbisHeaderCapturePattern(0x01, data, true); + } catch (ParserException e) { + return false; + } + } + + @Override + public void seek() { + super.seek(); + previousPacketBlockSize = 0; + elapsedSamples = 0; + seenFirstAudioPacket = false; + } + + @Override + public int read(ExtractorInput input, PositionHolder seekPosition) + throws IOException, InterruptedException { + + // setup + if (totalSamples == 0) { + if (vorbisSetup == null) { + inputLength = input.getLength(); + vorbisSetup = readSetupHeaders(input, scratch); + audioStartPosition = input.getPosition(); + extractorOutput.seekMap(this); + if (inputLength != C.LENGTH_UNBOUNDED) { + // seek to the end just before the last page of stream to get the duration + seekPosition.position = Math.max(0, input.getLength() - LARGEST_EXPECTED_PAGE_SIZE); + return Extractor.RESULT_SEEK; + } + } + totalSamples = inputLength == C.LENGTH_UNBOUNDED ? -1 + : oggParser.readGranuleOfLastPage(input); + + ArrayList codecInitialisationData = new ArrayList<>(); + codecInitialisationData.add(vorbisSetup.idHeader.data); + codecInitialisationData.add(vorbisSetup.setupHeaderData); + + duration = inputLength == C.LENGTH_UNBOUNDED ? C.UNKNOWN_TIME_US + : totalSamples * C.MICROS_PER_SECOND / vorbisSetup.idHeader.sampleRate; + trackOutput.format(MediaFormat.createAudioFormat(null, MimeTypes.AUDIO_VORBIS, + this.vorbisSetup.idHeader.bitrateNominal, OggParser.OGG_MAX_SEGMENT_SIZE * 255, duration, + this.vorbisSetup.idHeader.channels, (int) this.vorbisSetup.idHeader.sampleRate, + codecInitialisationData, null)); + + if (inputLength != C.LENGTH_UNBOUNDED) { + oggSeeker.setup(inputLength - audioStartPosition, totalSamples); + // seek back to resume from where we finished reading vorbis headers + seekPosition.position = audioStartPosition; + return Extractor.RESULT_SEEK; + } + } + + // seeking requested + if (!seenFirstAudioPacket && targetGranule > -1) { + OggUtil.skipToNextPage(input); + long position = oggSeeker.getNextSeekPosition(targetGranule, input); + if (position != -1) { + seekPosition.position = position; + return Extractor.RESULT_SEEK; + } else { + elapsedSamples = oggParser.skipToPageOfGranule(input, targetGranule); + previousPacketBlockSize = vorbisIdHeader.blockSize0; + // we're never at the first packet after seeking + seenFirstAudioPacket = true; + } + } + + // playback + if (oggParser.readPacket(input, scratch)) { + // if this is an audio packet... + if ((scratch.data[0] & 0x01) != 1) { + // ... we need to decode the block size + int packetBlockSize = decodeBlockSize(scratch.data[0], vorbisSetup); + // a packet contains samples produced from overlapping the previous and current frame data + // (https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-350001.3.2) + int samplesInPacket = seenFirstAudioPacket ? (packetBlockSize + previousPacketBlockSize) / 4 + : 0; + if (elapsedSamples + samplesInPacket >= targetGranule) { + // codec expects the number of samples appended to audio data + appendNumberOfSamples(scratch, samplesInPacket); + // calculate time and send audio data to codec + long timeUs = elapsedSamples * C.MICROS_PER_SECOND / vorbisSetup.idHeader.sampleRate; + trackOutput.sampleData(scratch, scratch.limit()); + trackOutput.sampleMetadata(timeUs, C.SAMPLE_FLAG_SYNC, scratch.limit(), 0, null); + targetGranule = -1; + } + // update state in members for next iteration + seenFirstAudioPacket = true; + elapsedSamples += samplesInPacket; + previousPacketBlockSize = packetBlockSize; + } + scratch.reset(); + return Extractor.RESULT_CONTINUE; + } + return Extractor.RESULT_END_OF_INPUT; + } + + //@VisibleForTesting + /* package */ VorbisSetup readSetupHeaders(ExtractorInput input, ParsableByteArray scratch) + throws IOException, InterruptedException { + + if (vorbisIdHeader == null) { + oggParser.readPacket(input, scratch); + vorbisIdHeader = VorbisUtil.readVorbisIdentificationHeader(scratch); + scratch.reset(); + } + + if (commentHeader == null) { + oggParser.readPacket(input, scratch); + commentHeader = VorbisUtil.readVorbisCommentHeader(scratch); + scratch.reset(); + } + + oggParser.readPacket(input, scratch); + // the third packet contains the setup header + byte[] setupHeaderData = new byte[scratch.limit()]; + // raw data of vorbis setup header has to be passed to decoder as CSD buffer #2 + System.arraycopy(scratch.data, 0, setupHeaderData, 0, scratch.limit()); + // partially decode setup header to get the modes + Mode[] modes = VorbisUtil.readVorbisModes(scratch, vorbisIdHeader.channels); + // we need the ilog of modes all the time when extracting, so we compute it once + int iLogModes = VorbisUtil.iLog(modes.length - 1); + scratch.reset(); + + return new VorbisSetup(vorbisIdHeader, commentHeader, setupHeaderData, modes, iLogModes); + } + + //@VisibleForTesting + /* package */ static void appendNumberOfSamples(ParsableByteArray buffer, + long packetSampleCount) { + + buffer.setLimit(buffer.limit() + 4); + // The vorbis decoder expects the number of samples in the packet + // to be appended to the audio data as an int32 + buffer.data[buffer.limit() - 4] = (byte) ((packetSampleCount) & 0xFF); + buffer.data[buffer.limit() - 3] = (byte) ((packetSampleCount >>> 8) & 0xFF); + buffer.data[buffer.limit() - 2] = (byte) ((packetSampleCount >>> 16) & 0xFF); + buffer.data[buffer.limit() - 1] = (byte) ((packetSampleCount >>> 24) & 0xFF); + } + + private static int decodeBlockSize(byte firstByteOfAudioPacket, VorbisSetup vorbisSetup) { + // read modeNumber (https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-730004.3.1) + int modeNumber = OggUtil.readBits(firstByteOfAudioPacket, vorbisSetup.iLogModes, 1); + int currentBlockSize; + if (!vorbisSetup.modes[modeNumber].blockFlag) { + currentBlockSize = vorbisSetup.idHeader.blockSize0; + } else { + currentBlockSize = vorbisSetup.idHeader.blockSize1; + } + return currentBlockSize; + } + + @Override + public boolean isSeekable() { + return vorbisSetup != null && inputLength != C.LENGTH_UNBOUNDED; + } + + @Override + public long getPosition(long timeUs) { + if (timeUs == 0) { + targetGranule = -1; + return audioStartPosition; + } + targetGranule = vorbisSetup.idHeader.sampleRate * timeUs / C.MICROS_PER_SECOND; + return Math.max(audioStartPosition, ((inputLength - audioStartPosition) * timeUs + / duration) - 4000); + } + + /** + * Class to hold all data read from Vorbis setup headers. + */ + /* package */ static final class VorbisSetup { + + public final VorbisUtil.VorbisIdHeader idHeader; + public final VorbisUtil.CommentHeader commentHeader; + public final byte[] setupHeaderData; + public final Mode[] modes; + public final int iLogModes; + + public VorbisSetup(VorbisUtil.VorbisIdHeader idHeader, VorbisUtil.CommentHeader + commentHeader, byte[] setupHeaderData, Mode[] modes, int iLogModes) { + this.idHeader = idHeader; + this.commentHeader = commentHeader; + this.setupHeaderData = setupHeaderData; + this.modes = modes; + this.iLogModes = iLogModes; + } + + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ogg/VorbisUtil.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ogg/VorbisUtil.java new file mode 100755 index 000000000..558f6fb6b --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ogg/VorbisUtil.java @@ -0,0 +1,489 @@ +/* + * Copyright (C) 2015 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.messenger.exoplayer.extractor.ogg; + +import org.telegram.messenger.exoplayer.ParserException; +import org.telegram.messenger.exoplayer.util.ParsableByteArray; + +import android.util.Log; + +import java.util.Arrays; + +/** + * Utility methods for parsing vorbis streams. + */ +/* package */ final class VorbisUtil { + + private static final String TAG = "VorbisUtil"; + + /** + * Returns ilog(x), which is the index of the highest set bit in {@code x}. + * + * @see + * Vorbis spec + * @param x the value of which the ilog should be calculated. + * @return ilog(x) + */ + public static int iLog(int x) { + int val = 0; + while (x > 0) { + val++; + x >>>= 1; + } + return val; + } + + /** + * Reads a vorbis identification header from {@code headerData}. + * + * @see Vorbis + * spec/Identification header + * @param headerData a {@link ParsableByteArray} wrapping the header data. + * @return a {@link VorbisUtil.VorbisIdHeader} with meta data. + * @throws ParserException thrown if invalid capture pattern is detected. + */ + public static VorbisIdHeader readVorbisIdentificationHeader(ParsableByteArray headerData) + throws ParserException { + + verifyVorbisHeaderCapturePattern(0x01, headerData, false); + + long version = headerData.readLittleEndianUnsignedInt(); + int channels = headerData.readUnsignedByte(); + long sampleRate = headerData.readLittleEndianUnsignedInt(); + int bitrateMax = headerData.readLittleEndianInt(); + int bitrateNominal = headerData.readLittleEndianInt(); + int bitrateMin = headerData.readLittleEndianInt(); + + int blockSize = headerData.readUnsignedByte(); + int blockSize0 = (int) Math.pow(2, blockSize & 0x0F); + int blockSize1 = (int) Math.pow(2, (blockSize & 0xF0) >> 4); + + boolean framingFlag = (headerData.readUnsignedByte() & 0x01) > 0; + // raw data of vorbis setup header has to be passed to decoder as CSD buffer #1 + byte[] data = Arrays.copyOf(headerData.data, headerData.limit()); + + return new VorbisIdHeader(version, channels, sampleRate, bitrateMax, bitrateNominal, bitrateMin, + blockSize0, blockSize1, framingFlag, data); + } + + /** + * Reads a vorbis comment header. + * + * @see + * Vorbis spec/Comment header + * @param headerData a {@link ParsableByteArray} wrapping the header data. + * @return a {@link VorbisUtil.CommentHeader} with all the comments. + * @throws ParserException thrown if invalid capture pattern is detected. + */ + public static CommentHeader readVorbisCommentHeader(ParsableByteArray headerData) + throws ParserException { + + verifyVorbisHeaderCapturePattern(0x03, headerData, false); + int length = 7; + + int len = (int) headerData.readLittleEndianUnsignedInt(); + length += 4; + String vendor = headerData.readString(len); + length += vendor.length(); + + long commentListLen = headerData.readLittleEndianUnsignedInt(); + String[] comments = new String[(int) commentListLen]; + length += 4; + for (int i = 0; i < commentListLen; i++) { + len = (int) headerData.readLittleEndianUnsignedInt(); + length += 4; + comments[i] = headerData.readString(len); + length += comments[i].length(); + } + if ((headerData.readUnsignedByte() & 0x01) == 0) { + throw new ParserException("framing bit expected to be set"); + } + length += 1; + return new CommentHeader(vendor, comments, length); + } + + /** + * Verifies whether the next bytes in {@code header} are a vorbis header of the given + * {@code headerType}. + * + * @param headerType the type of the header expected. + * @param header the alleged header bytes. + * @param quite if {@code true} no exceptions are thrown. Instead {@code false} is returned. + * @return the number of bytes read. + * @throws ParserException thrown if header type or capture pattern is not as expected. + */ + public static boolean verifyVorbisHeaderCapturePattern(int headerType, ParsableByteArray header, + boolean quite) + throws ParserException { + if (header.readUnsignedByte() != headerType) { + if (quite) { + return false; + } else { + throw new ParserException("expected header type " + Integer.toHexString(headerType)); + } + } + + if (!(header.readUnsignedByte() == 'v' + && header.readUnsignedByte() == 'o' + && header.readUnsignedByte() == 'r' + && header.readUnsignedByte() == 'b' + && header.readUnsignedByte() == 'i' + && header.readUnsignedByte() == 's')) { + if (quite) { + return false; + } else { + throw new ParserException("expected characters 'vorbis'"); + } + } + return true; + } + + /** + * This method reads the modes which are located at the very end of the vorbis setup header. + * That's why we need to partially decode or at least read the entire setup header to know + * where to start reading the modes. + * + * @see + * Vorbis spec/Setup header + * @param headerData a {@link ParsableByteArray} containing setup header data. + * @param channels the number of channels. + * @return an array of {@link Mode}s. + * @throws ParserException thrown if bit stream is invalid. + */ + public static Mode[] readVorbisModes(ParsableByteArray headerData, int channels) + throws ParserException { + + verifyVorbisHeaderCapturePattern(0x05, headerData, false); + + int numberOfBooks = headerData.readUnsignedByte() + 1; + + VorbisBitArray bitArray = new VorbisBitArray(headerData.data); + bitArray.skipBits(headerData.getPosition() * 8); + + for (int i = 0; i < numberOfBooks; i++) { + readBook(bitArray); + } + + int timeCount = bitArray.readBits(6) + 1; + for (int i = 0; i < timeCount; i++) { + if (bitArray.readBits(16) != 0x00) { + throw new ParserException("placeholder of time domain transforms not zeroed out"); + } + } + readFloors(bitArray); + readResidues(bitArray); + readMappings(channels, bitArray); + + Mode[] modes = readModes(bitArray); + if (!bitArray.readBit()) { + throw new ParserException("framing bit after modes not set as expected"); + } + return modes; + } + + private static Mode[] readModes(VorbisBitArray bitArray) { + int modeCount = bitArray.readBits(6) + 1; + Mode[] modes = new Mode[modeCount]; + for (int i = 0; i < modeCount; i++) { + boolean blockFlag = bitArray.readBit(); + int windowType = bitArray.readBits(16); + int transformType = bitArray.readBits(16); + int mapping = bitArray.readBits(8); + modes[i] = new Mode(blockFlag, windowType, transformType, mapping); + } + return modes; + } + + private static void readMappings(int channels, VorbisBitArray bitArray) + throws ParserException { + int mappingsCount = bitArray.readBits(6) + 1; + for (int i = 0; i < mappingsCount; i++) { + int mappingType = bitArray.readBits(16); + switch (mappingType) { + case 0: + int submaps; + if (bitArray.readBit()) { + submaps = bitArray.readBits(4) + 1; + } else { + submaps = 1; + } + int couplingSteps; + if (bitArray.readBit()) { + couplingSteps = bitArray.readBits(8) + 1; + for (int j = 0; j < couplingSteps; j++) { + bitArray.skipBits(iLog(channels - 1)); // magnitude + bitArray.skipBits(iLog(channels - 1)); // angle + } + } /*else { + couplingSteps = 0; + }*/ + if (bitArray.readBits(2) != 0x00) { + throw new ParserException("to reserved bits must be zero after mapping coupling steps"); + } + if (submaps > 1) { + for (int j = 0; j < channels; j++) { + bitArray.skipBits(4); // mappingMux + } + } + for (int j = 0; j < submaps; j++) { + bitArray.skipBits(8); // discard + bitArray.skipBits(8); // submapFloor + bitArray.skipBits(8); // submapResidue + } + break; + default: + Log.e(TAG, "mapping type other than 0 not supported: " + mappingType); + } + } + } + + private static void readResidues(VorbisBitArray bitArray) throws ParserException { + int residueCount = bitArray.readBits(6) + 1; + for (int i = 0; i < residueCount; i++) { + int residueType = bitArray.readBits(16); + if (residueType > 2) { + throw new ParserException("residueType greater than 2 is not decodable"); + } else { + bitArray.skipBits(24); // begin + bitArray.skipBits(24); // end + bitArray.skipBits(24); // partitionSize (add one) + int classifications = bitArray.readBits(6) + 1; + bitArray.skipBits(8); // classbook + int[] cascade = new int[classifications]; + for (int j = 0; j < classifications; j++) { + int highBits = 0; + int lowBits = bitArray.readBits(3); + if (bitArray.readBit()) { + highBits = bitArray.readBits(5); + } + cascade[j] = highBits * 8 + lowBits; + } + for (int j = 0; j < classifications; j++) { + for (int k = 0; k < 8; k++) { + if ((cascade[j] & (0x01 << k)) != 0) { + bitArray.skipBits(8); // discard + } + } + } + } + } + } + + private static void readFloors(VorbisBitArray bitArray) throws ParserException { + int floorCount = bitArray.readBits(6) + 1; + for (int i = 0; i < floorCount; i++) { + int floorType = bitArray.readBits(16); + switch (floorType) { + case 0: + bitArray.skipBits(8); //order + bitArray.skipBits(16); // rate + bitArray.skipBits(16); // barkMapSize + bitArray.skipBits(6); // amplitudeBits + bitArray.skipBits(8); // amplitudeOffset + int floorNumberOfBooks = bitArray.readBits(4) + 1; + for (int j = 0; j < floorNumberOfBooks; j++) { + bitArray.skipBits(8); + } + break; + case 1: + int partitions = bitArray.readBits(5); + int maximumClass = -1; + int[] partitionClassList = new int[partitions]; + for (int j = 0; j < partitions; j++) { + partitionClassList[j] = bitArray.readBits(4); + if (partitionClassList[j] > maximumClass) { + maximumClass = partitionClassList[j]; + } + } + int[] classDimensions = new int[maximumClass + 1]; + for (int j = 0; j < classDimensions.length; j++) { + classDimensions[j] = bitArray.readBits(3) + 1; + int classSubclasses = bitArray.readBits(2); + if (classSubclasses > 0) { + bitArray.skipBits(8); // classMasterbooks + } + for (int k = 0; k < (1 << classSubclasses); k++) { + bitArray.skipBits(8); // subclassBook (subtract 1) + } + } + bitArray.skipBits(2); // multiplier (add one) + int rangeBits = bitArray.readBits(4); + int count = 0; + for (int j = 0, k = 0; j < partitions; j++) { + int idx = partitionClassList[j]; + count += classDimensions[idx]; + for (; k < count; k++) { + bitArray.skipBits(rangeBits); // floorValue + } + } + break; + default: + throw new ParserException("floor type greater than 1 not decodable: " + floorType); + } + } + } + + private static CodeBook readBook(VorbisBitArray bitArray) throws ParserException { + if (bitArray.readBits(24) != 0x564342) { + throw new ParserException("expected code book to start with [0x56, 0x43, 0x42] at " + + bitArray.getPosition()); + } + int dimensions = bitArray.readBits(16); + int entries = bitArray.readBits(24); + long[] lengthMap = new long[entries]; + + boolean isOrdered = bitArray.readBit(); + if (!isOrdered) { + boolean isSparse = bitArray.readBit(); + for (int i = 0; i < lengthMap.length; i++) { + if (isSparse) { + if (bitArray.readBit()) { + lengthMap[i] = bitArray.readBits(5) + 1; + } else { // entry unused + lengthMap[i] = 0; + } + } else { // not sparse + lengthMap[i] = bitArray.readBits(5) + 1; + } + } + } else { + int length = bitArray.readBits(5) + 1; + for (int i = 0; i < lengthMap.length;) { + int num = bitArray.readBits(iLog(entries - i)); + for (int j = 0; j < num && i < lengthMap.length; i++, j++) { + lengthMap[i] = length; + } + length++; + } + } + + int lookupType = bitArray.readBits(4); + if (lookupType > 2) { + throw new ParserException("lookup type greater than 2 not decodable: " + lookupType); + } else if (lookupType == 1 || lookupType == 2) { + bitArray.skipBits(32); // minimumValue + bitArray.skipBits(32); // deltaValue + int valueBits = bitArray.readBits(4) + 1; + bitArray.skipBits(1); // sequenceP + long lookupValuesCount; + if (lookupType == 1) { + if (dimensions != 0) { + lookupValuesCount = mapType1QuantValues(entries, dimensions); + } else { + // TODO no sample file found yet + lookupValuesCount = 0; + } + } else { + // TODO no sample file found yet + lookupValuesCount = entries * dimensions; + } + // discard (no decoding required yet) + bitArray.skipBits((int) (lookupValuesCount * valueBits)); + } + return new CodeBook(dimensions, entries, lengthMap, lookupType, isOrdered); + } + + /** + * @see _book_maptype1_quantvals + */ + private static long mapType1QuantValues(long entries, long dimension) { + return (long) Math.floor(Math.pow(entries, 1.d / dimension)); + } + + public static final class CodeBook { + + public final int dimensions; + public final int entries; + public final long[] lengthMap; + public final int lookupType; + public final boolean isOrdered; + + public CodeBook(int dimensions, int entries, long[] lengthMap, int lookupType, + boolean isOrdered) { + this.dimensions = dimensions; + this.entries = entries; + this.lengthMap = lengthMap; + this.lookupType = lookupType; + this.isOrdered = isOrdered; + } + + } + + public static final class CommentHeader { + + public final String vendor; + public final String[] comments; + public final int length; + + public CommentHeader(String vendor, String[] comments, int length) { + this.vendor = vendor; + this.comments = comments; + this.length = length; + } + + } + + public static final class VorbisIdHeader { + + public final long version; + public final int channels; + public final long sampleRate; + public final int bitrateMax; + public final int bitrateNominal; + public final int bitrateMin; + public final int blockSize0; + public final int blockSize1; + public final boolean framingFlag; + public final byte[] data; + + public VorbisIdHeader(long version, int channels, long sampleRate, int bitrateMax, + int bitrateNominal, int bitrateMin, int blockSize0, int blockSize1, boolean framingFlag, + byte[] data) { + this.version = version; + this.channels = channels; + this.sampleRate = sampleRate; + this.bitrateMax = bitrateMax; + this.bitrateNominal = bitrateNominal; + this.bitrateMin = bitrateMin; + this.blockSize0 = blockSize0; + this.blockSize1 = blockSize1; + this.framingFlag = framingFlag; + this.data = data; + } + + public int getApproximateBitrate() { + return bitrateNominal == 0 ? (bitrateMin + bitrateMax) / 2 : bitrateNominal; + } + + } + + public static final class Mode { + + public final boolean blockFlag; + public final int windowType; + public final int transformType; + public final int mapping; + + public Mode(boolean blockFlag, int windowType, int transformType, int mapping) { + this.blockFlag = blockFlag; + this.windowType = windowType; + this.transformType = transformType; + this.mapping = mapping; + } + + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ts/Ac3Reader.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ts/Ac3Reader.java new file mode 100755 index 000000000..7aee19ccd --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ts/Ac3Reader.java @@ -0,0 +1,181 @@ +/* + * 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.messenger.exoplayer.extractor.ts; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.MediaFormat; +import org.telegram.messenger.exoplayer.extractor.TrackOutput; +import org.telegram.messenger.exoplayer.util.Ac3Util; +import org.telegram.messenger.exoplayer.util.ParsableBitArray; +import org.telegram.messenger.exoplayer.util.ParsableByteArray; + +/** + * Parses a continuous (E-)AC-3 byte stream and extracts individual samples. + */ +/* package */ final class Ac3Reader extends ElementaryStreamReader { + + private static final int STATE_FINDING_SYNC = 0; + private static final int STATE_READING_HEADER = 1; + private static final int STATE_READING_SAMPLE = 2; + + private static final int HEADER_SIZE = 8; + + private final boolean isEac3; + private final ParsableBitArray headerScratchBits; + private final ParsableByteArray headerScratchBytes; + + private int state; + private int bytesRead; + + // Used to find the header. + private boolean lastByteWas0B; + + // Used when parsing the header. + private long sampleDurationUs; + private MediaFormat mediaFormat; + private int sampleSize; + + // Used when reading the samples. + private long timeUs; + + /** + * Constructs a new reader for (E-)AC-3 elementary streams. + * + * @param output Track output for extracted samples. + * @param isEac3 Whether the stream is E-AC-3 (ETSI TS 102 366 Annex E). Specify {@code false} to + * parse sample headers as AC-3. + */ + public Ac3Reader(TrackOutput output, boolean isEac3) { + super(output); + this.isEac3 = isEac3; + headerScratchBits = new ParsableBitArray(new byte[HEADER_SIZE]); + headerScratchBytes = new ParsableByteArray(headerScratchBits.data); + state = STATE_FINDING_SYNC; + } + + @Override + public void seek() { + state = STATE_FINDING_SYNC; + bytesRead = 0; + lastByteWas0B = false; + } + + @Override + public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { + timeUs = pesTimeUs; + } + + @Override + public void consume(ParsableByteArray data) { + while (data.bytesLeft() > 0) { + switch (state) { + case STATE_FINDING_SYNC: + if (skipToNextSync(data)) { + state = STATE_READING_HEADER; + headerScratchBytes.data[0] = 0x0B; + headerScratchBytes.data[1] = 0x77; + bytesRead = 2; + } + break; + case STATE_READING_HEADER: + if (continueRead(data, headerScratchBytes.data, HEADER_SIZE)) { + parseHeader(); + headerScratchBytes.setPosition(0); + output.sampleData(headerScratchBytes, HEADER_SIZE); + state = STATE_READING_SAMPLE; + } + break; + case STATE_READING_SAMPLE: + int bytesToRead = Math.min(data.bytesLeft(), sampleSize - bytesRead); + output.sampleData(data, bytesToRead); + bytesRead += bytesToRead; + if (bytesRead == sampleSize) { + output.sampleMetadata(timeUs, C.SAMPLE_FLAG_SYNC, sampleSize, 0, null); + timeUs += sampleDurationUs; + state = STATE_FINDING_SYNC; + } + break; + } + } + } + + @Override + public void packetFinished() { + // Do nothing. + } + + /** + * Continues a read from the provided {@code source} into a given {@code target}. It's assumed + * that the data should be written into {@code target} starting from an offset of zero. + * + * @param source The source from which to read. + * @param target The target into which data is to be read. + * @param targetLength The target length of the read. + * @return Whether the target length was reached. + */ + private boolean continueRead(ParsableByteArray source, byte[] target, int targetLength) { + int bytesToRead = Math.min(source.bytesLeft(), targetLength - bytesRead); + source.readBytes(target, bytesRead, bytesToRead); + bytesRead += bytesToRead; + return bytesRead == targetLength; + } + + /** + * Locates the next syncword, advancing the position to the byte that immediately follows it. If a + * syncword was not located, the position is advanced to the limit. + * + * @param pesBuffer The buffer whose position should be advanced. + * @return True if a syncword position was found. False otherwise. + */ + private boolean skipToNextSync(ParsableByteArray pesBuffer) { + while (pesBuffer.bytesLeft() > 0) { + if (!lastByteWas0B) { + lastByteWas0B = pesBuffer.readUnsignedByte() == 0x0B; + continue; + } + int secondByte = pesBuffer.readUnsignedByte(); + if (secondByte == 0x77) { + lastByteWas0B = false; + return true; + } else { + lastByteWas0B = secondByte == 0x0B; + } + } + return false; + } + + /** + * Parses the sample header. + */ + private void parseHeader() { + if (mediaFormat == null) { + mediaFormat = isEac3 + ? Ac3Util.parseEac3SyncframeFormat(headerScratchBits, null, C.UNKNOWN_TIME_US, null) + : Ac3Util.parseAc3SyncframeFormat(headerScratchBits, null, C.UNKNOWN_TIME_US, null); + output.format(mediaFormat); + } + sampleSize = isEac3 ? Ac3Util.parseEAc3SyncframeSize(headerScratchBits.data) + : Ac3Util.parseAc3SyncframeSize(headerScratchBits.data); + int audioSamplesPerSyncframe = isEac3 + ? Ac3Util.parseEAc3SyncframeAudioSampleCount(headerScratchBits.data) + : Ac3Util.getAc3SyncframeAudioSampleCount(); + // In this class a sample is an access unit (syncframe in AC-3), but the MediaFormat sample rate + // specifies the number of PCM audio samples per second. + sampleDurationUs = + (int) (C.MICROS_PER_SECOND * audioSamplesPerSyncframe / mediaFormat.sampleRate); + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ts/AdtsExtractor.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ts/AdtsExtractor.java new file mode 100755 index 000000000..4c9e9601f --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ts/AdtsExtractor.java @@ -0,0 +1,155 @@ +/* + * 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.messenger.exoplayer.extractor.ts; + +import org.telegram.messenger.exoplayer.extractor.Extractor; +import org.telegram.messenger.exoplayer.extractor.ExtractorInput; +import org.telegram.messenger.exoplayer.extractor.ExtractorOutput; +import org.telegram.messenger.exoplayer.extractor.PositionHolder; +import org.telegram.messenger.exoplayer.extractor.SeekMap; +import org.telegram.messenger.exoplayer.util.ParsableBitArray; +import org.telegram.messenger.exoplayer.util.ParsableByteArray; +import org.telegram.messenger.exoplayer.util.Util; + +import java.io.IOException; + +/** + * Facilitates the extraction of AAC samples from elementary audio files formatted as AAC with ADTS + * headers. + */ +public final class AdtsExtractor implements Extractor { + + private static final int MAX_PACKET_SIZE = 200; + private static final int ID3_TAG = Util.getIntegerCodeForString("ID3"); + /** + * The maximum number of bytes to search when sniffing, excluding the header, before giving up. + * Frame sizes are represented by 13-bit fields, so expect a valid frame in the first 8192 bytes. + */ + private static final int MAX_SNIFF_BYTES = 8 * 1024; + + private final long firstSampleTimestampUs; + private final ParsableByteArray packetBuffer; + + // Accessed only by the loading thread. + private AdtsReader adtsReader; + private boolean startedPacket; + + public AdtsExtractor() { + this(0); + } + + public AdtsExtractor(long firstSampleTimestampUs) { + this.firstSampleTimestampUs = firstSampleTimestampUs; + packetBuffer = new ParsableByteArray(MAX_PACKET_SIZE); + } + + @Override + public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + // Skip any ID3 headers. + ParsableByteArray scratch = new ParsableByteArray(10); + ParsableBitArray scratchBits = new ParsableBitArray(scratch.data); + int startPosition = 0; + while (true) { + input.peekFully(scratch.data, 0, 10); + scratch.setPosition(0); + if (scratch.readUnsignedInt24() != ID3_TAG) { + break; + } + int length = (scratch.data[6] & 0x7F) << 21 | ((scratch.data[7] & 0x7F) << 14) + | ((scratch.data[8] & 0x7F) << 7) | (scratch.data[9] & 0x7F); + startPosition += 10 + length; + input.advancePeekPosition(length); + } + input.resetPeekPosition(); + input.advancePeekPosition(startPosition); + + // Try to find four or more consecutive AAC audio frames, exceeding the MPEG TS packet size. + int headerPosition = startPosition; + int validFramesSize = 0; + int validFramesCount = 0; + while (true) { + input.peekFully(scratch.data, 0, 2); + scratch.setPosition(0); + int syncBytes = scratch.readUnsignedShort(); + if ((syncBytes & 0xFFF6) != 0xFFF0) { + validFramesCount = 0; + validFramesSize = 0; + input.resetPeekPosition(); + if (++headerPosition - startPosition >= MAX_SNIFF_BYTES) { + return false; + } + input.advancePeekPosition(headerPosition); + } else { + if (++validFramesCount >= 4 && validFramesSize > 188) { + return true; + } + + // Skip the frame. + input.peekFully(scratch.data, 0, 4); + scratchBits.setPosition(14); + int frameSize = scratchBits.readBits(13); + // Either the stream is malformed OR we're not parsing an ADTS stream. + if (frameSize <= 6) { + return false; + } + input.advancePeekPosition(frameSize - 6); + validFramesSize += frameSize; + } + } + } + + @Override + public void init(ExtractorOutput output) { + adtsReader = new AdtsReader(output.track(0), output.track(1)); + output.endTracks(); + output.seekMap(SeekMap.UNSEEKABLE); + } + + @Override + public void seek() { + startedPacket = false; + adtsReader.seek(); + } + + @Override + public void release() { + // Do nothing + } + + @Override + public int read(ExtractorInput input, PositionHolder seekPosition) + throws IOException, InterruptedException { + int bytesRead = input.read(packetBuffer.data, 0, MAX_PACKET_SIZE); + if (bytesRead == -1) { + return RESULT_END_OF_INPUT; + } + + // Feed whatever data we have to the reader, regardless of whether the read finished or not. + packetBuffer.setPosition(0); + packetBuffer.setLimit(bytesRead); + + // TODO: Make it possible for adtsReader to consume the dataSource directly, so that it becomes + // unnecessary to copy the data through packetBuffer. + if (!startedPacket) { + // Pass data to the reader as though it's contained within a single infinitely long packet. + adtsReader.packetStarted(firstSampleTimestampUs, true); + startedPacket = true; + } + adtsReader.consume(packetBuffer); + return RESULT_CONTINUE; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ts/AdtsReader.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ts/AdtsReader.java new file mode 100755 index 000000000..dfd93afe4 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ts/AdtsReader.java @@ -0,0 +1,311 @@ +/* + * 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.messenger.exoplayer.extractor.ts; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.MediaFormat; +import org.telegram.messenger.exoplayer.extractor.TrackOutput; +import org.telegram.messenger.exoplayer.util.CodecSpecificDataUtil; +import org.telegram.messenger.exoplayer.util.MimeTypes; +import org.telegram.messenger.exoplayer.util.ParsableBitArray; +import org.telegram.messenger.exoplayer.util.ParsableByteArray; + +import android.util.Log; +import android.util.Pair; + +import java.util.Arrays; +import java.util.Collections; + +/** + * Parses a continuous ADTS byte stream and extracts individual frames. + */ +/* package */ final class AdtsReader extends ElementaryStreamReader { + + private static final String TAG = "AdtsReader"; + + private static final int STATE_FINDING_SAMPLE = 0; + private static final int STATE_READING_ID3_HEADER = 1; + private static final int STATE_READING_ADTS_HEADER = 2; + private static final int STATE_READING_SAMPLE = 3; + + private static final int HEADER_SIZE = 5; + private static final int CRC_SIZE = 2; + + // Match states used while looking for the next sample + private static final int MATCH_STATE_VALUE_SHIFT = 8; + private static final int MATCH_STATE_START = 1 << MATCH_STATE_VALUE_SHIFT; + private static final int MATCH_STATE_FF = 2 << MATCH_STATE_VALUE_SHIFT; + private static final int MATCH_STATE_I = 3 << MATCH_STATE_VALUE_SHIFT; + private static final int MATCH_STATE_ID = 4 << MATCH_STATE_VALUE_SHIFT; + + private static final int ID3_HEADER_SIZE = 10; + private static final int ID3_SIZE_OFFSET = 6; + private static final byte[] ID3_IDENTIFIER = {'I', 'D', '3'}; + + private final ParsableBitArray adtsScratch; + private final ParsableByteArray id3HeaderBuffer; + private final TrackOutput id3Output; + + private int state; + private int bytesRead; + + private int matchState; + + private boolean hasCrc; + + // Used when parsing the header. + private boolean hasOutputFormat; + private long sampleDurationUs; + private int sampleSize; + + // Used when reading the samples. + private long timeUs; + + private TrackOutput currentOutput; + private long currentSampleDuration; + + /** + * @param output A {@link TrackOutput} to which AAC samples should be written. + * @param id3Output A {@link TrackOutput} to which ID3 samples should be written. + */ + public AdtsReader(TrackOutput output, TrackOutput id3Output) { + super(output); + this.id3Output = id3Output; + id3Output.format(MediaFormat.createId3Format()); + adtsScratch = new ParsableBitArray(new byte[HEADER_SIZE + CRC_SIZE]); + id3HeaderBuffer = new ParsableByteArray(Arrays.copyOf(ID3_IDENTIFIER, ID3_HEADER_SIZE)); + setFindingSampleState(); + } + + @Override + public void seek() { + setFindingSampleState(); + } + + @Override + public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { + timeUs = pesTimeUs; + } + + @Override + public void consume(ParsableByteArray data) { + while (data.bytesLeft() > 0) { + switch (state) { + case STATE_FINDING_SAMPLE: + findNextSample(data); + break; + case STATE_READING_ID3_HEADER: + if (continueRead(data, id3HeaderBuffer.data, ID3_HEADER_SIZE)) { + parseId3Header(); + } + break; + case STATE_READING_ADTS_HEADER: + int targetLength = hasCrc ? HEADER_SIZE + CRC_SIZE : HEADER_SIZE; + if (continueRead(data, adtsScratch.data, targetLength)) { + parseAdtsHeader(); + } + break; + case STATE_READING_SAMPLE: + readSample(data); + break; + } + } + } + + @Override + public void packetFinished() { + // Do nothing. + } + + /** + * Continues a read from the provided {@code source} into a given {@code target}. It's assumed + * that the data should be written into {@code target} starting from an offset of zero. + * + * @param source The source from which to read. + * @param target The target into which data is to be read. + * @param targetLength The target length of the read. + * @return Whether the target length was reached. + */ + private boolean continueRead(ParsableByteArray source, byte[] target, int targetLength) { + int bytesToRead = Math.min(source.bytesLeft(), targetLength - bytesRead); + source.readBytes(target, bytesRead, bytesToRead); + bytesRead += bytesToRead; + return bytesRead == targetLength; + } + + /** + * Sets the state to STATE_FINDING_SAMPLE. + */ + private void setFindingSampleState() { + state = STATE_FINDING_SAMPLE; + bytesRead = 0; + matchState = MATCH_STATE_START; + } + + /** + * Sets the state to STATE_READING_ID3_HEADER and resets the fields required for + * {@link #parseId3Header()}. + */ + private void setReadingId3HeaderState() { + state = STATE_READING_ID3_HEADER; + bytesRead = ID3_IDENTIFIER.length; + sampleSize = 0; + id3HeaderBuffer.setPosition(0); + } + + /** + * Sets the state to STATE_READING_SAMPLE. + * + * @param outputToUse TrackOutput object to write the sample to + * @param currentSampleDuration Duration of the sample to be read + * @param priorReadBytes Size of prior read bytes + * @param sampleSize Size of the sample + */ + private void setReadingSampleState(TrackOutput outputToUse, long currentSampleDuration, + int priorReadBytes, int sampleSize) { + state = STATE_READING_SAMPLE; + bytesRead = priorReadBytes; + this.currentOutput = outputToUse; + this.currentSampleDuration = currentSampleDuration; + this.sampleSize = sampleSize; + } + + /** + * Sets the state to STATE_READING_ADTS_HEADER. + */ + private void setReadingAdtsHeaderState() { + state = STATE_READING_ADTS_HEADER; + bytesRead = 0; + } + + /** + * Locates the next sample start, advancing the position to the byte that immediately follows + * identifier. If a sample was not located, the position is advanced to the limit. + * + * @param pesBuffer The buffer whose position should be advanced. + */ + private void findNextSample(ParsableByteArray pesBuffer) { + byte[] adtsData = pesBuffer.data; + int position = pesBuffer.getPosition(); + int endOffset = pesBuffer.limit(); + while (position < endOffset) { + int data = adtsData[position++] & 0xFF; + if (matchState == MATCH_STATE_FF && data >= 0xF0 && data != 0xFF) { + hasCrc = (data & 0x1) == 0; + setReadingAdtsHeaderState(); + pesBuffer.setPosition(position); + return; + } + switch (matchState | data) { + case MATCH_STATE_START | 0xFF: + matchState = MATCH_STATE_FF; + break; + case MATCH_STATE_START | 'I': + matchState = MATCH_STATE_I; + break; + case MATCH_STATE_I | 'D': + matchState = MATCH_STATE_ID; + break; + case MATCH_STATE_ID | '3': + setReadingId3HeaderState(); + pesBuffer.setPosition(position); + return; + default: + if (matchState != MATCH_STATE_START) { + // If matching fails in a later state, revert to MATCH_STATE_START and + // check this byte again + matchState = MATCH_STATE_START; + position--; + } + break; + } + } + pesBuffer.setPosition(position); + } + + /** + * Parses the Id3 header. + */ + private void parseId3Header() { + id3Output.sampleData(id3HeaderBuffer, ID3_HEADER_SIZE); + id3HeaderBuffer.setPosition(ID3_SIZE_OFFSET); + setReadingSampleState(id3Output, 0, ID3_HEADER_SIZE, + id3HeaderBuffer.readSynchSafeInt() + ID3_HEADER_SIZE); + } + + /** + * Parses the sample header. + */ + private void parseAdtsHeader() { + adtsScratch.setPosition(0); + + if (!hasOutputFormat) { + int audioObjectType = adtsScratch.readBits(2) + 1; + if (audioObjectType == 1) { + // The stream indicates AAC Main but it's more likely that the stream contains HE-AAC. + // HE-AAC cannot be represented correctly in the ADTS header because it has an + // audioObjectType value of 5 whereas an ADTS header can only represent values up to 4. + // Since most Android devices don't support AAC Main anyway, we pretend that we're dealing + // with AAC LC and hope for the best. In practice this often works. + Log.w(TAG, "Detected AAC Main audio, but assuming AAC LC."); + audioObjectType = 2; + } + + int sampleRateIndex = adtsScratch.readBits(4); + adtsScratch.skipBits(1); + int channelConfig = adtsScratch.readBits(3); + + byte[] audioSpecificConfig = CodecSpecificDataUtil.buildAacAudioSpecificConfig( + audioObjectType, sampleRateIndex, channelConfig); + Pair audioParams = CodecSpecificDataUtil.parseAacAudioSpecificConfig( + audioSpecificConfig); + + MediaFormat mediaFormat = MediaFormat.createAudioFormat(null, MimeTypes.AUDIO_AAC, + MediaFormat.NO_VALUE, MediaFormat.NO_VALUE, C.UNKNOWN_TIME_US, audioParams.second, + audioParams.first, Collections.singletonList(audioSpecificConfig), null); + // In this class a sample is an access unit, but the MediaFormat sample rate specifies the + // number of PCM audio samples per second. + sampleDurationUs = (C.MICROS_PER_SECOND * 1024) / mediaFormat.sampleRate; + output.format(mediaFormat); + hasOutputFormat = true; + } else { + adtsScratch.skipBits(10); + } + + adtsScratch.skipBits(4); + int sampleSize = adtsScratch.readBits(13) - 2 /* the sync word */ - HEADER_SIZE; + if (hasCrc) { + sampleSize -= CRC_SIZE; + } + + setReadingSampleState(output, sampleDurationUs, 0, sampleSize); + } + + /** + * Reads the rest of the sample + */ + private void readSample(ParsableByteArray data) { + int bytesToRead = Math.min(data.bytesLeft(), sampleSize - bytesRead); + currentOutput.sampleData(data, bytesToRead); + bytesRead += bytesToRead; + if (bytesRead == sampleSize) { + currentOutput.sampleMetadata(timeUs, C.SAMPLE_FLAG_SYNC, sampleSize, 0, null); + timeUs += currentSampleDuration; + setFindingSampleState(); + } + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ts/DtsReader.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ts/DtsReader.java new file mode 100755 index 000000000..8af21b4f0 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ts/DtsReader.java @@ -0,0 +1,168 @@ +/* + * 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.messenger.exoplayer.extractor.ts; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.MediaFormat; +import org.telegram.messenger.exoplayer.extractor.TrackOutput; +import org.telegram.messenger.exoplayer.util.DtsUtil; +import org.telegram.messenger.exoplayer.util.ParsableByteArray; + +/** + * Parses a continuous DTS byte stream and extracts individual samples. + */ +/* package */ final class DtsReader extends ElementaryStreamReader { + + private static final int STATE_FINDING_SYNC = 0; + private static final int STATE_READING_HEADER = 1; + private static final int STATE_READING_SAMPLE = 2; + + private static final int HEADER_SIZE = 15; + private static final int SYNC_VALUE = 0x7FFE8001; + private static final int SYNC_VALUE_SIZE = 4; + + private final ParsableByteArray headerScratchBytes; + + private int state; + private int bytesRead; + + // Used to find the header. + private int syncBytes; + + // Used when parsing the header. + private long sampleDurationUs; + private MediaFormat mediaFormat; + private int sampleSize; + + // Used when reading the samples. + private long timeUs; + + /** + * Constructs a new reader for DTS elementary streams. + * + * @param output Track output for extracted samples. + */ + public DtsReader(TrackOutput output) { + super(output); + headerScratchBytes = new ParsableByteArray(new byte[HEADER_SIZE]); + headerScratchBytes.data[0] = (byte) ((SYNC_VALUE >> 24) & 0xFF); + headerScratchBytes.data[1] = (byte) ((SYNC_VALUE >> 16) & 0xFF); + headerScratchBytes.data[2] = (byte) ((SYNC_VALUE >> 8) & 0xFF); + headerScratchBytes.data[3] = (byte) (SYNC_VALUE & 0xFF); + state = STATE_FINDING_SYNC; + } + + @Override + public void seek() { + state = STATE_FINDING_SYNC; + bytesRead = 0; + syncBytes = 0; + } + + @Override + public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { + timeUs = pesTimeUs; + } + + @Override + public void consume(ParsableByteArray data) { + while (data.bytesLeft() > 0) { + switch (state) { + case STATE_FINDING_SYNC: + if (skipToNextSync(data)) { + bytesRead = SYNC_VALUE_SIZE; + state = STATE_READING_HEADER; + } + break; + case STATE_READING_HEADER: + if (continueRead(data, headerScratchBytes.data, HEADER_SIZE)) { + parseHeader(); + headerScratchBytes.setPosition(0); + output.sampleData(headerScratchBytes, HEADER_SIZE); + state = STATE_READING_SAMPLE; + } + break; + case STATE_READING_SAMPLE: + int bytesToRead = Math.min(data.bytesLeft(), sampleSize - bytesRead); + output.sampleData(data, bytesToRead); + bytesRead += bytesToRead; + if (bytesRead == sampleSize) { + output.sampleMetadata(timeUs, C.SAMPLE_FLAG_SYNC, sampleSize, 0, null); + timeUs += sampleDurationUs; + state = STATE_FINDING_SYNC; + } + break; + } + } + } + + @Override + public void packetFinished() { + // Do nothing. + } + + /** + * Continues a read from the provided {@code source} into a given {@code target}. It's assumed + * that the data should be written into {@code target} starting from an offset of zero. + * + * @param source The source from which to read. + * @param target The target into which data is to be read. + * @param targetLength The target length of the read. + * @return Whether the target length was reached. + */ + private boolean continueRead(ParsableByteArray source, byte[] target, int targetLength) { + int bytesToRead = Math.min(source.bytesLeft(), targetLength - bytesRead); + source.readBytes(target, bytesRead, bytesToRead); + bytesRead += bytesToRead; + return bytesRead == targetLength; + } + + /** + * Locates the next SYNC value in the buffer, advancing the position to the byte that immediately + * follows it. If SYNC was not located, the position is advanced to the limit. + * + * @param pesBuffer The buffer whose position should be advanced. + * @return True if SYNC was found. False otherwise. + */ + private boolean skipToNextSync(ParsableByteArray pesBuffer) { + while (pesBuffer.bytesLeft() > 0) { + syncBytes <<= 8; + syncBytes |= pesBuffer.readUnsignedByte(); + if (syncBytes == SYNC_VALUE) { + syncBytes = 0; + return true; + } + } + return false; + } + + /** + * Parses the sample header. + */ + private void parseHeader() { + byte[] frameData = headerScratchBytes.data; + if (mediaFormat == null) { + mediaFormat = DtsUtil.parseDtsFormat(frameData, null, C.UNKNOWN_TIME_US, null); + output.format(mediaFormat); + } + sampleSize = DtsUtil.getDtsFrameSize(frameData); + // In this class a sample is an access unit (frame in DTS), but the MediaFormat sample rate + // specifies the number of PCM audio samples per second. + sampleDurationUs = (int) (C.MICROS_PER_SECOND + * DtsUtil.parseDtsAudioSampleCount(frameData) / mediaFormat.sampleRate); + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ts/ElementaryStreamReader.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ts/ElementaryStreamReader.java new file mode 100755 index 000000000..77576e5d9 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ts/ElementaryStreamReader.java @@ -0,0 +1,60 @@ +/* + * 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.messenger.exoplayer.extractor.ts; + +import org.telegram.messenger.exoplayer.extractor.TrackOutput; +import org.telegram.messenger.exoplayer.util.ParsableByteArray; + +/** + * Extracts individual samples from an elementary media stream, preserving original order. + */ +/* package */ abstract class ElementaryStreamReader { + + protected final TrackOutput output; + + /** + * @param output A {@link TrackOutput} to which samples should be written. + */ + protected ElementaryStreamReader(TrackOutput output) { + this.output = output; + } + + /** + * Notifies the reader that a seek has occurred. + */ + public abstract void seek(); + + /** + * Invoked when a packet starts. + * + * @param pesTimeUs The timestamp associated with the packet. + * @param dataAlignmentIndicator The data alignment indicator associated with the packet. + */ + public abstract void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator); + + /** + * Consumes (possibly partial) data from the current packet. + * + * @param data The data to consume. + */ + public abstract void consume(ParsableByteArray data); + + /** + * Invoked when a packet ends. + */ + public abstract void packetFinished(); + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ts/H262Reader.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ts/H262Reader.java new file mode 100755 index 000000000..253d7eefa --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ts/H262Reader.java @@ -0,0 +1,281 @@ +/* + * 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.messenger.exoplayer.extractor.ts; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.MediaFormat; +import org.telegram.messenger.exoplayer.extractor.TrackOutput; +import org.telegram.messenger.exoplayer.util.MimeTypes; +import org.telegram.messenger.exoplayer.util.NalUnitUtil; +import org.telegram.messenger.exoplayer.util.ParsableByteArray; + +import android.util.Pair; + +import java.util.Arrays; +import java.util.Collections; + +/** + * Parses a continuous H262 byte stream and extracts individual frames. + */ +/* package */ final class H262Reader extends ElementaryStreamReader { + + private static final int START_PICTURE = 0x00; + private static final int START_SEQUENCE_HEADER = 0xB3; + private static final int START_EXTENSION = 0xB5; + private static final int START_GROUP = 0xB8; + + // Maps (frame_rate_code - 1) indices to values, as defined in ITU-T H.262 Table 6-4. + private static final double[] FRAME_RATE_VALUES = new double[] { + 24000d / 1001, 24, 25, 30000d / 1001, 30, 50, 60000d / 1001, 60}; + + // State that should not be reset on seek. + private boolean hasOutputFormat; + private long frameDurationUs; + + // State that should be reset on seek. + private final boolean[] prefixFlags; + private final CsdBuffer csdBuffer; + private boolean foundFirstFrameInGroup; + private long totalBytesWritten; + + // Per packet state that gets reset at the start of each packet. + private long pesTimeUs; + private boolean pesPtsUsAvailable; + + // Per sample state that gets reset at the start of each frame. + private boolean isKeyframe; + private long framePosition; + private long frameTimeUs; + + public H262Reader(TrackOutput output) { + super(output); + prefixFlags = new boolean[4]; + csdBuffer = new CsdBuffer(128); + } + + @Override + public void seek() { + NalUnitUtil.clearPrefixFlags(prefixFlags); + csdBuffer.reset(); + pesPtsUsAvailable = false; + foundFirstFrameInGroup = false; + totalBytesWritten = 0; + } + + @Override + public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { + pesPtsUsAvailable = pesTimeUs != C.UNKNOWN_TIME_US; + if (pesPtsUsAvailable) { + this.pesTimeUs = pesTimeUs; + } + } + + @Override + public void consume(ParsableByteArray data) { + while (data.bytesLeft() > 0) { + int offset = data.getPosition(); + int limit = data.limit(); + byte[] dataArray = data.data; + + // Append the data to the buffer. + totalBytesWritten += data.bytesLeft(); + output.sampleData(data, data.bytesLeft()); + + int searchOffset = offset; + while (true) { + int startCodeOffset = NalUnitUtil.findNalUnit(dataArray, searchOffset, limit, prefixFlags); + + if (startCodeOffset == limit) { + // We've scanned to the end of the data without finding another start code. + if (!hasOutputFormat) { + csdBuffer.onData(dataArray, offset, limit); + } + return; + } + + // We've found a start code with the following value. + int startCodeValue = data.data[startCodeOffset + 3] & 0xFF; + + if (!hasOutputFormat) { + // This is the number of bytes from the current offset to the start of the next start + // code. It may be negative if the start code started in the previously consumed data. + int lengthToStartCode = startCodeOffset - offset; + if (lengthToStartCode > 0) { + csdBuffer.onData(dataArray, offset, startCodeOffset); + } + // This is the number of bytes belonging to the next start code that have already been + // passed to csdDataTargetBuffer. + int bytesAlreadyPassed = lengthToStartCode < 0 ? -lengthToStartCode : 0; + if (csdBuffer.onStartCode(startCodeValue, bytesAlreadyPassed)) { + // The csd data is complete, so we can parse and output the media format. + Pair result = parseCsdBuffer(csdBuffer); + output.format(result.first); + frameDurationUs = result.second; + hasOutputFormat = true; + } + } + + if (hasOutputFormat && (startCodeValue == START_GROUP || startCodeValue == START_PICTURE)) { + int bytesWrittenPastStartCode = limit - startCodeOffset; + if (foundFirstFrameInGroup) { + int flags = isKeyframe ? C.SAMPLE_FLAG_SYNC : 0; + int size = (int) (totalBytesWritten - framePosition) - bytesWrittenPastStartCode; + output.sampleMetadata(frameTimeUs, flags, size, bytesWrittenPastStartCode, null); + isKeyframe = false; + } + if (startCodeValue == START_GROUP) { + foundFirstFrameInGroup = false; + isKeyframe = true; + } else /* startCode == START_PICTURE */ { + frameTimeUs = pesPtsUsAvailable ? pesTimeUs : (frameTimeUs + frameDurationUs); + framePosition = totalBytesWritten - bytesWrittenPastStartCode; + pesPtsUsAvailable = false; + foundFirstFrameInGroup = true; + } + } + + offset = startCodeOffset; + searchOffset = offset + 3; + } + } + } + + @Override + public void packetFinished() { + // Do nothing. + } + + /** + * Parses the {@link MediaFormat} and frame duration from a csd buffer. + * + * @param csdBuffer The csd buffer. + * @return A pair consisting of the {@link MediaFormat} and the frame duration in microseconds, or + * 0 if the duration could not be determined. + */ + private static Pair parseCsdBuffer(CsdBuffer csdBuffer) { + byte[] csdData = Arrays.copyOf(csdBuffer.data, csdBuffer.length); + + int firstByte = csdData[4] & 0xFF; + int secondByte = csdData[5] & 0xFF; + int thirdByte = csdData[6] & 0xFF; + int width = (firstByte << 4) | (secondByte >> 4); + int height = (secondByte & 0x0F) << 8 | thirdByte; + + float pixelWidthHeightRatio = 1f; + int aspectRatioCode = (csdData[7] & 0xF0) >> 4; + switch(aspectRatioCode) { + case 2: + pixelWidthHeightRatio = (4 * height) / (float) (3 * width); + break; + case 3: + pixelWidthHeightRatio = (16 * height) / (float) (9 * width); + break; + case 4: + pixelWidthHeightRatio = (121 * height) / (float) (100 * width); + break; + default: + // Do nothing. + break; + } + + MediaFormat format = MediaFormat.createVideoFormat(null, MimeTypes.VIDEO_MPEG2, + MediaFormat.NO_VALUE, MediaFormat.NO_VALUE, C.UNKNOWN_TIME_US, width, height, + Collections.singletonList(csdData), MediaFormat.NO_VALUE, pixelWidthHeightRatio); + + long frameDurationUs = 0; + int frameRateCodeMinusOne = (csdData[7] & 0x0F) - 1; + if (0 <= frameRateCodeMinusOne && frameRateCodeMinusOne < FRAME_RATE_VALUES.length) { + double frameRate = FRAME_RATE_VALUES[frameRateCodeMinusOne]; + int sequenceExtensionPosition = csdBuffer.sequenceExtensionPosition; + int frameRateExtensionN = (csdData[sequenceExtensionPosition + 9] & 0x60) >> 5; + int frameRateExtensionD = (csdData[sequenceExtensionPosition + 9] & 0x1F); + if (frameRateExtensionN != frameRateExtensionD) { + frameRate *= (frameRateExtensionN + 1d) / (frameRateExtensionD + 1); + } + frameDurationUs = (long) (C.MICROS_PER_SECOND / frameRate); + } + + return Pair.create(format, frameDurationUs); + } + + private static final class CsdBuffer { + + private boolean isFilling; + + public int length; + public int sequenceExtensionPosition; + public byte[] data; + + public CsdBuffer(int initialCapacity) { + data = new byte[initialCapacity]; + } + + /** + * Resets the buffer, clearing any data that it holds. + */ + public void reset() { + isFilling = false; + length = 0; + sequenceExtensionPosition = 0; + } + + /** + * Invoked when a start code is encountered in the stream. + * + * @param startCodeValue The start code value. + * @param bytesAlreadyPassed The number of bytes of the start code that have already been + * passed to {@link #onData(byte[], int, int)}, or 0. + * @return True if the csd data is now complete. False otherwise. If true is returned, neither + * this method or {@link #onData(byte[], int, int)} should be called again without an + * interleaving call to {@link #reset()}. + */ + public boolean onStartCode(int startCodeValue, int bytesAlreadyPassed) { + if (isFilling) { + if (sequenceExtensionPosition == 0 && startCodeValue == START_EXTENSION) { + sequenceExtensionPosition = length; + } else { + length -= bytesAlreadyPassed; + isFilling = false; + return true; + } + } else if (startCodeValue == START_SEQUENCE_HEADER) { + isFilling = true; + } + return false; + } + + /** + * Invoked to pass stream data. + * + * @param newData Holds the data being passed. + * @param offset The offset of the data in {@code data}. + * @param limit The limit (exclusive) of the data in {@code data}. + */ + public void onData(byte[] newData, int offset, int limit) { + if (!isFilling) { + return; + } + int readLength = limit - offset; + if (data.length < length + readLength) { + data = Arrays.copyOf(data, (length + readLength) * 2); + } + System.arraycopy(newData, offset, data, length, readLength); + length += readLength; + } + + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ts/H264Reader.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ts/H264Reader.java new file mode 100755 index 000000000..935f110cc --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ts/H264Reader.java @@ -0,0 +1,520 @@ +/* + * 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.messenger.exoplayer.extractor.ts; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.MediaFormat; +import org.telegram.messenger.exoplayer.extractor.TrackOutput; +import org.telegram.messenger.exoplayer.util.MimeTypes; +import org.telegram.messenger.exoplayer.util.NalUnitUtil; +import org.telegram.messenger.exoplayer.util.NalUnitUtil.SpsData; +import org.telegram.messenger.exoplayer.util.ParsableBitArray; +import org.telegram.messenger.exoplayer.util.ParsableByteArray; + +import android.util.SparseArray; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Parses a continuous H264 byte stream and extracts individual frames. + */ +/* package */ final class H264Reader extends ElementaryStreamReader { + + private static final int NAL_UNIT_TYPE_SEI = 6; // Supplemental enhancement information + private static final int NAL_UNIT_TYPE_SPS = 7; // Sequence parameter set + private static final int NAL_UNIT_TYPE_PPS = 8; // Picture parameter set + + // State that should not be reset on seek. + private boolean hasOutputFormat; + + // State that should be reset on seek. + private final SeiReader seiReader; + private final boolean[] prefixFlags; + private final SampleReader sampleReader; + private final NalUnitTargetBuffer sps; + private final NalUnitTargetBuffer pps; + private final NalUnitTargetBuffer sei; + private long totalBytesWritten; + + // Per packet state that gets reset at the start of each packet. + private long pesTimeUs; + + // Scratch variables to avoid allocations. + private final ParsableByteArray seiWrapper; + + /** + * @param output A {@link TrackOutput} to which H.264 samples should be written. + * @param seiReader A reader for EIA-608 samples in SEI NAL units. + * @param allowNonIdrKeyframes Whether to treat samples consisting of non-IDR I slices as + * synchronization samples (key-frames). + * @param detectAccessUnits Whether to split the input stream into access units (samples) based on + * slice headers. Pass {@code false} if the stream contains access unit delimiters (AUDs). + */ + public H264Reader(TrackOutput output, SeiReader seiReader, boolean allowNonIdrKeyframes, + boolean detectAccessUnits) { + super(output); + this.seiReader = seiReader; + prefixFlags = new boolean[3]; + sampleReader = new SampleReader(output, allowNonIdrKeyframes, detectAccessUnits); + sps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SPS, 128); + pps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_PPS, 128); + sei = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SEI, 128); + seiWrapper = new ParsableByteArray(); + } + + @Override + public void seek() { + NalUnitUtil.clearPrefixFlags(prefixFlags); + sps.reset(); + pps.reset(); + sei.reset(); + sampleReader.reset(); + totalBytesWritten = 0; + } + + @Override + public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { + this.pesTimeUs = pesTimeUs; + } + + @Override + public void consume(ParsableByteArray data) { + while (data.bytesLeft() > 0) { + int offset = data.getPosition(); + int limit = data.limit(); + byte[] dataArray = data.data; + + // Append the data to the buffer. + totalBytesWritten += data.bytesLeft(); + output.sampleData(data, data.bytesLeft()); + + // Scan the appended data, processing NAL units as they are encountered + while (true) { + int nalUnitOffset = NalUnitUtil.findNalUnit(dataArray, offset, limit, prefixFlags); + + if (nalUnitOffset == limit) { + // We've scanned to the end of the data without finding the start of another NAL unit. + nalUnitData(dataArray, offset, limit); + return; + } + + // We've seen the start of a NAL unit of the following type. + int nalUnitType = NalUnitUtil.getNalUnitType(dataArray, nalUnitOffset); + + // This is the number of bytes from the current offset to the start of the next NAL unit. + // It may be negative if the NAL unit started in the previously consumed data. + int lengthToNalUnit = nalUnitOffset - offset; + if (lengthToNalUnit > 0) { + nalUnitData(dataArray, offset, nalUnitOffset); + } + int bytesWrittenPastPosition = limit - nalUnitOffset; + long absolutePosition = totalBytesWritten - bytesWrittenPastPosition; + // Indicate the end of the previous NAL unit. If the length to the start of the next unit + // is negative then we wrote too many bytes to the NAL buffers. Discard the excess bytes + // when notifying that the unit has ended. + endNalUnit(absolutePosition, bytesWrittenPastPosition, + lengthToNalUnit < 0 ? -lengthToNalUnit : 0, pesTimeUs); + // Indicate the start of the next NAL unit. + startNalUnit(absolutePosition, nalUnitType, pesTimeUs); + // Continue scanning the data. + offset = nalUnitOffset + 3; + } + } + } + + @Override + public void packetFinished() { + // Do nothing. + } + + private void startNalUnit(long position, int nalUnitType, long pesTimeUs) { + if (!hasOutputFormat || sampleReader.needsSpsPps()) { + sps.startNalUnit(nalUnitType); + pps.startNalUnit(nalUnitType); + } + sei.startNalUnit(nalUnitType); + sampleReader.startNalUnit(position, nalUnitType, pesTimeUs); + } + + private void nalUnitData(byte[] dataArray, int offset, int limit) { + if (!hasOutputFormat || sampleReader.needsSpsPps()) { + sps.appendToNalUnit(dataArray, offset, limit); + pps.appendToNalUnit(dataArray, offset, limit); + } + sei.appendToNalUnit(dataArray, offset, limit); + sampleReader.appendToNalUnit(dataArray, offset, limit); + } + + private void endNalUnit(long position, int offset, int discardPadding, long pesTimeUs) { + if (!hasOutputFormat || sampleReader.needsSpsPps()) { + sps.endNalUnit(discardPadding); + pps.endNalUnit(discardPadding); + if (!hasOutputFormat) { + if (sps.isCompleted() && pps.isCompleted()) { + List initializationData = new ArrayList<>(); + initializationData.add(Arrays.copyOf(sps.nalData, sps.nalLength)); + initializationData.add(Arrays.copyOf(pps.nalData, pps.nalLength)); + NalUnitUtil.SpsData spsData = NalUnitUtil.parseSpsNalUnit(unescape(sps)); + NalUnitUtil.PpsData ppsData = NalUnitUtil.parsePpsNalUnit(unescape(pps)); + output.format(MediaFormat.createVideoFormat(null, MimeTypes.VIDEO_H264, + MediaFormat.NO_VALUE, MediaFormat.NO_VALUE, C.UNKNOWN_TIME_US, spsData.width, + spsData.height, initializationData, MediaFormat.NO_VALUE, + spsData.pixelWidthAspectRatio)); + hasOutputFormat = true; + sampleReader.putSps(spsData); + sampleReader.putPps(ppsData); + sps.reset(); + pps.reset(); + } + } else if (sps.isCompleted()) { + NalUnitUtil.SpsData spsData = NalUnitUtil.parseSpsNalUnit(unescape(sps)); + sampleReader.putSps(spsData); + sps.reset(); + } else if (pps.isCompleted()) { + NalUnitUtil.PpsData ppsData = NalUnitUtil.parsePpsNalUnit(unescape(pps)); + sampleReader.putPps(ppsData); + pps.reset(); + } + } + if (sei.endNalUnit(discardPadding)) { + int unescapedLength = NalUnitUtil.unescapeStream(sei.nalData, sei.nalLength); + seiWrapper.reset(sei.nalData, unescapedLength); + seiWrapper.setPosition(4); // NAL prefix and nal_unit() header. + seiReader.consume(pesTimeUs, seiWrapper); + } + sampleReader.endNalUnit(position, offset); + } + + private static ParsableBitArray unescape(NalUnitTargetBuffer buffer) { + int length = NalUnitUtil.unescapeStream(buffer.nalData, buffer.nalLength); + ParsableBitArray bitArray = new ParsableBitArray(buffer.nalData, length); + bitArray.skipBits(32); // NAL header + return bitArray; + } + + /** + * Consumes a stream of NAL units and outputs samples. + */ + private static final class SampleReader { + + private static final int DEFAULT_BUFFER_SIZE = 128; + + private static final int NAL_UNIT_TYPE_NON_IDR = 1; // Coded slice of a non-IDR picture + private static final int NAL_UNIT_TYPE_PARTITION_A = 2; // Coded slice data partition A + private static final int NAL_UNIT_TYPE_IDR = 5; // Coded slice of an IDR picture + private static final int NAL_UNIT_TYPE_AUD = 9; // Access unit delimiter + + private final TrackOutput output; + private final boolean allowNonIdrKeyframes; + private final boolean detectAccessUnits; + private final ParsableBitArray scratch; + private final SparseArray sps; + private final SparseArray pps; + + private byte[] buffer; + private int bufferLength; + + // Per NAL unit state. A sample consists of one or more NAL units. + private int nalUnitType; + private long nalUnitStartPosition; + private boolean isFilling; + private long nalUnitTimeUs; + private SliceHeaderData previousSliceHeader; + private SliceHeaderData sliceHeader; + + // Per sample state that gets reset at the start of each sample. + private boolean readingSample; + private long samplePosition; + private long sampleTimeUs; + private boolean sampleIsKeyframe; + + public SampleReader(TrackOutput output, boolean allowNonIdrKeyframes, + boolean detectAccessUnits) { + this.output = output; + this.allowNonIdrKeyframes = allowNonIdrKeyframes; + this.detectAccessUnits = detectAccessUnits; + sps = new SparseArray<>(); + pps = new SparseArray<>(); + previousSliceHeader = new SliceHeaderData(); + sliceHeader = new SliceHeaderData(); + scratch = new ParsableBitArray(); + buffer = new byte[DEFAULT_BUFFER_SIZE]; + reset(); + } + + public boolean needsSpsPps() { + return detectAccessUnits; + } + + public void putSps(NalUnitUtil.SpsData spsData) { + sps.append(spsData.seqParameterSetId, spsData); + } + + public void putPps(NalUnitUtil.PpsData ppsData) { + pps.append(ppsData.picParameterSetId, ppsData); + } + + public void reset() { + isFilling = false; + readingSample = false; + sliceHeader.clear(); + } + + public void startNalUnit(long position, int type, long pesTimeUs) { + nalUnitType = type; + nalUnitTimeUs = pesTimeUs; + nalUnitStartPosition = position; + if ((allowNonIdrKeyframes && nalUnitType == NAL_UNIT_TYPE_NON_IDR) + || (detectAccessUnits && (nalUnitType == NAL_UNIT_TYPE_IDR + || nalUnitType == NAL_UNIT_TYPE_NON_IDR + || nalUnitType == NAL_UNIT_TYPE_PARTITION_A))) { + // Store the previous header and prepare to populate the new one. + SliceHeaderData newSliceHeader = previousSliceHeader; + previousSliceHeader = sliceHeader; + sliceHeader = newSliceHeader; + sliceHeader.clear(); + bufferLength = 0; + isFilling = true; + } + } + + /** + * Invoked to pass stream data. The data passed should not include the 3 byte start code. + * + * @param data Holds the data being passed. + * @param offset The offset of the data in {@code data}. + * @param limit The limit (exclusive) of the data in {@code data}. + */ + public void appendToNalUnit(byte[] data, int offset, int limit) { + if (!isFilling) { + return; + } + int readLength = limit - offset; + if (buffer.length < bufferLength + readLength) { + buffer = Arrays.copyOf(buffer, (bufferLength + readLength) * 2); + } + System.arraycopy(data, offset, buffer, bufferLength, readLength); + bufferLength += readLength; + + scratch.reset(buffer, bufferLength); + if (scratch.bitsLeft() < 8) { + return; + } + scratch.skipBits(1); // forbidden_zero_bit + int nalRefIdc = scratch.readBits(2); + scratch.skipBits(5); // nal_unit_type + + // Read the slice header using the syntax defined in ITU-T Recommendation H.264 (2013) + // subsection 7.3.3. + if (!scratch.canReadExpGolombCodedNum()) { + return; + } + scratch.readUnsignedExpGolombCodedInt(); // first_mb_in_slice + if (!scratch.canReadExpGolombCodedNum()) { + return; + } + int sliceType = scratch.readUnsignedExpGolombCodedInt(); + if (!detectAccessUnits) { + // There are AUDs in the stream so the rest of the header can be ignored. + isFilling = false; + sliceHeader.setSliceType(sliceType); + return; + } + if (!scratch.canReadExpGolombCodedNum()) { + return; + } + int picParameterSetId = scratch.readUnsignedExpGolombCodedInt(); + if (pps.indexOfKey(picParameterSetId) < 0) { + // We have not seen the PPS yet, so don't try to parse the slice header. + isFilling = false; + return; + } + NalUnitUtil.PpsData ppsData = pps.get(picParameterSetId); + NalUnitUtil.SpsData spsData = sps.get(ppsData.seqParameterSetId); + if (spsData.separateColorPlaneFlag) { + if (scratch.bitsLeft() < 2) { + return; + } + scratch.skipBits(2); // colour_plane_id + } + if (scratch.bitsLeft() < spsData.frameNumLength) { + return; + } + boolean fieldPicFlag = false; + boolean bottomFieldFlagPresent = false; + boolean bottomFieldFlag = false; + int frameNum = scratch.readBits(spsData.frameNumLength); + if (!spsData.frameMbsOnlyFlag) { + if (scratch.bitsLeft() < 1) { + return; + } + fieldPicFlag = scratch.readBit(); + if (fieldPicFlag) { + if (scratch.bitsLeft() < 1) { + return; + } + bottomFieldFlag = scratch.readBit(); + bottomFieldFlagPresent = true; + } + } + boolean idrPicFlag = nalUnitType == NAL_UNIT_TYPE_IDR; + int idrPicId = 0; + if (idrPicFlag) { + if (!scratch.canReadExpGolombCodedNum()) { + return; + } + idrPicId = scratch.readUnsignedExpGolombCodedInt(); + } + int picOrderCntLsb = 0; + int deltaPicOrderCntBottom = 0; + int deltaPicOrderCnt0 = 0; + int deltaPicOrderCnt1 = 0; + if (spsData.picOrderCountType == 0) { + if (scratch.bitsLeft() < spsData.picOrderCntLsbLength) { + return; + } + picOrderCntLsb = scratch.readBits(spsData.picOrderCntLsbLength); + if (ppsData.bottomFieldPicOrderInFramePresentFlag && !fieldPicFlag) { + if (!scratch.canReadExpGolombCodedNum()) { + return; + } + deltaPicOrderCntBottom = scratch.readSignedExpGolombCodedInt(); + } + } else if (spsData.picOrderCountType == 1 + && !spsData.deltaPicOrderAlwaysZeroFlag) { + if (!scratch.canReadExpGolombCodedNum()) { + return; + } + deltaPicOrderCnt0 = scratch.readSignedExpGolombCodedInt(); + if (ppsData.bottomFieldPicOrderInFramePresentFlag && !fieldPicFlag) { + if (!scratch.canReadExpGolombCodedNum()) { + return; + } + deltaPicOrderCnt1 = scratch.readSignedExpGolombCodedInt(); + } + } + sliceHeader.setAll(spsData, nalRefIdc, sliceType, frameNum, picParameterSetId, fieldPicFlag, + bottomFieldFlagPresent, bottomFieldFlag, idrPicFlag, idrPicId, picOrderCntLsb, + deltaPicOrderCntBottom, deltaPicOrderCnt0, deltaPicOrderCnt1); + isFilling = false; + } + + public void endNalUnit(long position, int offset) { + if (nalUnitType == NAL_UNIT_TYPE_AUD + || (detectAccessUnits && sliceHeader.isFirstVclNalUnitOfPicture(previousSliceHeader))) { + // If the NAL unit ending is the start of a new sample, output the previous one. + if (readingSample) { + int nalUnitLength = (int) (position - nalUnitStartPosition); + outputSample(offset + nalUnitLength); + } + samplePosition = nalUnitStartPosition; + sampleTimeUs = nalUnitTimeUs; + sampleIsKeyframe = false; + readingSample = true; + } + sampleIsKeyframe |= nalUnitType == NAL_UNIT_TYPE_IDR || (allowNonIdrKeyframes + && nalUnitType == NAL_UNIT_TYPE_NON_IDR && sliceHeader.isISlice()); + } + + private void outputSample(int offset) { + int flags = sampleIsKeyframe ? C.SAMPLE_FLAG_SYNC : 0; + int size = (int) (nalUnitStartPosition - samplePosition); + output.sampleMetadata(sampleTimeUs, flags, size, offset, null); + } + + private static final class SliceHeaderData { + + private static final int SLICE_TYPE_I = 2; + private static final int SLICE_TYPE_ALL_I = 7; + + private boolean isComplete; + private boolean hasSliceType; + + private SpsData spsData; + private int nalRefIdc; + private int sliceType; + private int frameNum; + private int picParameterSetId; + private boolean fieldPicFlag; + private boolean bottomFieldFlagPresent; + private boolean bottomFieldFlag; + private boolean idrPicFlag; + private int idrPicId; + private int picOrderCntLsb; + private int deltaPicOrderCntBottom; + private int deltaPicOrderCnt0; + private int deltaPicOrderCnt1; + + public void clear() { + hasSliceType = false; + isComplete = false; + } + + public void setSliceType(int sliceType) { + this.sliceType = sliceType; + hasSliceType = true; + } + + public void setAll(SpsData spsData, int nalRefIdc, int sliceType, int frameNum, + int picParameterSetId, boolean fieldPicFlag, boolean bottomFieldFlagPresent, + boolean bottomFieldFlag, boolean idrPicFlag, int idrPicId, int picOrderCntLsb, + int deltaPicOrderCntBottom, int deltaPicOrderCnt0, int deltaPicOrderCnt1) { + this.spsData = spsData; + this.nalRefIdc = nalRefIdc; + this.sliceType = sliceType; + this.frameNum = frameNum; + this.picParameterSetId = picParameterSetId; + this.fieldPicFlag = fieldPicFlag; + this.bottomFieldFlagPresent = bottomFieldFlagPresent; + this.bottomFieldFlag = bottomFieldFlag; + this.idrPicFlag = idrPicFlag; + this.idrPicId = idrPicId; + this.picOrderCntLsb = picOrderCntLsb; + this.deltaPicOrderCntBottom = deltaPicOrderCntBottom; + this.deltaPicOrderCnt0 = deltaPicOrderCnt0; + this.deltaPicOrderCnt1 = deltaPicOrderCnt1; + isComplete = true; + hasSliceType = true; + } + + public boolean isISlice() { + return hasSliceType && (sliceType == SLICE_TYPE_ALL_I || sliceType == SLICE_TYPE_I); + } + + private boolean isFirstVclNalUnitOfPicture(SliceHeaderData other) { + // See ISO 14496-10 subsection 7.4.1.2.4. + return isComplete && (!other.isComplete || frameNum != other.frameNum + || picParameterSetId != other.picParameterSetId || fieldPicFlag != other.fieldPicFlag + || (bottomFieldFlagPresent && other.bottomFieldFlagPresent + && bottomFieldFlag != other.bottomFieldFlag) + || (nalRefIdc != other.nalRefIdc && (nalRefIdc == 0 || other.nalRefIdc == 0)) + || (spsData.picOrderCountType == 0 && other.spsData.picOrderCountType == 0 + && (picOrderCntLsb != other.picOrderCntLsb + || deltaPicOrderCntBottom != other.deltaPicOrderCntBottom)) + || (spsData.picOrderCountType == 1 && other.spsData.picOrderCountType == 1 + && (deltaPicOrderCnt0 != other.deltaPicOrderCnt0 + || deltaPicOrderCnt1 != other.deltaPicOrderCnt1)) + || idrPicFlag != other.idrPicFlag + || (idrPicFlag && other.idrPicFlag && idrPicId != other.idrPicId)); + } + + } + + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ts/H265Reader.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ts/H265Reader.java new file mode 100755 index 000000000..3f67c9f48 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ts/H265Reader.java @@ -0,0 +1,483 @@ +/* + * 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.messenger.exoplayer.extractor.ts; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.MediaFormat; +import org.telegram.messenger.exoplayer.extractor.TrackOutput; +import org.telegram.messenger.exoplayer.util.MimeTypes; +import org.telegram.messenger.exoplayer.util.NalUnitUtil; +import org.telegram.messenger.exoplayer.util.ParsableBitArray; +import org.telegram.messenger.exoplayer.util.ParsableByteArray; + +import android.util.Log; + +import java.util.Collections; + +/** + * Parses a continuous H.265 byte stream and extracts individual frames. + */ +/* package */ final class H265Reader extends ElementaryStreamReader { + + private static final String TAG = "H265Reader"; + + // nal_unit_type values from H.265/HEVC (2014) Table 7-1. + private static final int RASL_R = 9; + private static final int BLA_W_LP = 16; + private static final int CRA_NUT = 21; + private static final int VPS_NUT = 32; + private static final int SPS_NUT = 33; + private static final int PPS_NUT = 34; + private static final int PREFIX_SEI_NUT = 39; + private static final int SUFFIX_SEI_NUT = 40; + + // State that should not be reset on seek. + private boolean hasOutputFormat; + + // State that should be reset on seek. + private final SeiReader seiReader; + private final boolean[] prefixFlags; + private final NalUnitTargetBuffer vps; + private final NalUnitTargetBuffer sps; + private final NalUnitTargetBuffer pps; + private final NalUnitTargetBuffer prefixSei; + private final NalUnitTargetBuffer suffixSei; // TODO: Are both needed? + private final SampleReader sampleReader; + private long totalBytesWritten; + + // Per packet state that gets reset at the start of each packet. + private long pesTimeUs; + + // Scratch variables to avoid allocations. + private final ParsableByteArray seiWrapper; + + /** + * @param output A {@link TrackOutput} to which H.265 samples should be written. + * @param seiReader A reader for EIA-608 samples in SEI NAL units. + */ + public H265Reader(TrackOutput output, SeiReader seiReader) { + super(output); + this.seiReader = seiReader; + prefixFlags = new boolean[3]; + vps = new NalUnitTargetBuffer(VPS_NUT, 128); + sps = new NalUnitTargetBuffer(SPS_NUT, 128); + pps = new NalUnitTargetBuffer(PPS_NUT, 128); + prefixSei = new NalUnitTargetBuffer(PREFIX_SEI_NUT, 128); + suffixSei = new NalUnitTargetBuffer(SUFFIX_SEI_NUT, 128); + sampleReader = new SampleReader(output); + seiWrapper = new ParsableByteArray(); + } + + @Override + public void seek() { + NalUnitUtil.clearPrefixFlags(prefixFlags); + vps.reset(); + sps.reset(); + pps.reset(); + prefixSei.reset(); + suffixSei.reset(); + sampleReader.reset(); + totalBytesWritten = 0; + } + + @Override + public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { + this.pesTimeUs = pesTimeUs; + } + + @Override + public void consume(ParsableByteArray data) { + while (data.bytesLeft() > 0) { + int offset = data.getPosition(); + int limit = data.limit(); + byte[] dataArray = data.data; + + // Append the data to the buffer. + totalBytesWritten += data.bytesLeft(); + output.sampleData(data, data.bytesLeft()); + + // Scan the appended data, processing NAL units as they are encountered + while (offset < limit) { + int nalUnitOffset = NalUnitUtil.findNalUnit(dataArray, offset, limit, prefixFlags); + + if (nalUnitOffset == limit) { + // We've scanned to the end of the data without finding the start of another NAL unit. + nalUnitData(dataArray, offset, limit); + return; + } + + // We've seen the start of a NAL unit of the following type. + int nalUnitType = NalUnitUtil.getH265NalUnitType(dataArray, nalUnitOffset); + + // This is the number of bytes from the current offset to the start of the next NAL unit. + // It may be negative if the NAL unit started in the previously consumed data. + int lengthToNalUnit = nalUnitOffset - offset; + if (lengthToNalUnit > 0) { + nalUnitData(dataArray, offset, nalUnitOffset); + } + + int bytesWrittenPastPosition = limit - nalUnitOffset; + long absolutePosition = totalBytesWritten - bytesWrittenPastPosition; + // Indicate the end of the previous NAL unit. If the length to the start of the next unit + // is negative then we wrote too many bytes to the NAL buffers. Discard the excess bytes + // when notifying that the unit has ended. + endNalUnit(absolutePosition, bytesWrittenPastPosition, + lengthToNalUnit < 0 ? -lengthToNalUnit : 0, pesTimeUs); + // Indicate the start of the next NAL unit. + startNalUnit(absolutePosition, bytesWrittenPastPosition, nalUnitType, pesTimeUs); + // Continue scanning the data. + offset = nalUnitOffset + 3; + } + } + } + + @Override + public void packetFinished() { + // Do nothing. + } + + private void startNalUnit(long position, int offset, int nalUnitType, long pesTimeUs) { + if (hasOutputFormat) { + sampleReader.startNalUnit(position, offset, nalUnitType, pesTimeUs); + } else { + vps.startNalUnit(nalUnitType); + sps.startNalUnit(nalUnitType); + pps.startNalUnit(nalUnitType); + } + prefixSei.startNalUnit(nalUnitType); + suffixSei.startNalUnit(nalUnitType); + } + + private void nalUnitData(byte[] dataArray, int offset, int limit) { + if (hasOutputFormat) { + sampleReader.readNalUnitData(dataArray, offset, limit); + } else { + vps.appendToNalUnit(dataArray, offset, limit); + sps.appendToNalUnit(dataArray, offset, limit); + pps.appendToNalUnit(dataArray, offset, limit); + } + prefixSei.appendToNalUnit(dataArray, offset, limit); + suffixSei.appendToNalUnit(dataArray, offset, limit); + } + + private void endNalUnit(long position, int offset, int discardPadding, long pesTimeUs) { + if (hasOutputFormat) { + sampleReader.endNalUnit(position, offset); + } else { + vps.endNalUnit(discardPadding); + sps.endNalUnit(discardPadding); + pps.endNalUnit(discardPadding); + if (vps.isCompleted() && sps.isCompleted() && pps.isCompleted()) { + output.format(parseMediaFormat(vps, sps, pps)); + hasOutputFormat = true; + } + } + if (prefixSei.endNalUnit(discardPadding)) { + int unescapedLength = NalUnitUtil.unescapeStream(prefixSei.nalData, prefixSei.nalLength); + seiWrapper.reset(prefixSei.nalData, unescapedLength); + + // Skip the NAL prefix and type. + seiWrapper.skipBytes(5); + seiReader.consume(pesTimeUs, seiWrapper); + } + if (suffixSei.endNalUnit(discardPadding)) { + int unescapedLength = NalUnitUtil.unescapeStream(suffixSei.nalData, suffixSei.nalLength); + seiWrapper.reset(suffixSei.nalData, unescapedLength); + + // Skip the NAL prefix and type. + seiWrapper.skipBytes(5); + seiReader.consume(pesTimeUs, seiWrapper); + } + } + + private static MediaFormat parseMediaFormat(NalUnitTargetBuffer vps, NalUnitTargetBuffer sps, + NalUnitTargetBuffer pps) { + // Build codec-specific data. + byte[] csd = new byte[vps.nalLength + sps.nalLength + pps.nalLength]; + System.arraycopy(vps.nalData, 0, csd, 0, vps.nalLength); + System.arraycopy(sps.nalData, 0, csd, vps.nalLength, sps.nalLength); + System.arraycopy(pps.nalData, 0, csd, vps.nalLength + sps.nalLength, pps.nalLength); + + // Unescape and then parse the SPS NAL unit, as per H.265/HEVC (2014) 7.3.2.2.1. + NalUnitUtil.unescapeStream(sps.nalData, sps.nalLength); + ParsableBitArray bitArray = new ParsableBitArray(sps.nalData); + bitArray.skipBits(40 + 4); // NAL header, sps_video_parameter_set_id + int maxSubLayersMinus1 = bitArray.readBits(3); + bitArray.skipBits(1); // sps_temporal_id_nesting_flag + + // profile_tier_level(1, sps_max_sub_layers_minus1) + bitArray.skipBits(88); // if (profilePresentFlag) {...} + bitArray.skipBits(8); // general_level_idc + int toSkip = 0; + for (int i = 0; i < maxSubLayersMinus1; i++) { + if (bitArray.readBit()) { // sub_layer_profile_present_flag[i] + toSkip += 89; + } + if (bitArray.readBit()) { // sub_layer_level_present_flag[i] + toSkip += 8; + } + } + bitArray.skipBits(toSkip); + if (maxSubLayersMinus1 > 0) { + bitArray.skipBits(2 * (8 - maxSubLayersMinus1)); + } + + bitArray.readUnsignedExpGolombCodedInt(); // sps_seq_parameter_set_id + int chromaFormatIdc = bitArray.readUnsignedExpGolombCodedInt(); + if (chromaFormatIdc == 3) { + bitArray.skipBits(1); // separate_colour_plane_flag + } + int picWidthInLumaSamples = bitArray.readUnsignedExpGolombCodedInt(); + int picHeightInLumaSamples = bitArray.readUnsignedExpGolombCodedInt(); + if (bitArray.readBit()) { // conformance_window_flag + int confWinLeftOffset = bitArray.readUnsignedExpGolombCodedInt(); + int confWinRightOffset = bitArray.readUnsignedExpGolombCodedInt(); + int confWinTopOffset = bitArray.readUnsignedExpGolombCodedInt(); + int confWinBottomOffset = bitArray.readUnsignedExpGolombCodedInt(); + // H.265/HEVC (2014) Table 6-1 + int subWidthC = chromaFormatIdc == 1 || chromaFormatIdc == 2 ? 2 : 1; + int subHeightC = chromaFormatIdc == 1 ? 2 : 1; + picWidthInLumaSamples -= subWidthC * (confWinLeftOffset + confWinRightOffset); + picHeightInLumaSamples -= subHeightC * (confWinTopOffset + confWinBottomOffset); + } + bitArray.readUnsignedExpGolombCodedInt(); // bit_depth_luma_minus8 + bitArray.readUnsignedExpGolombCodedInt(); // bit_depth_chroma_minus8 + int log2MaxPicOrderCntLsbMinus4 = bitArray.readUnsignedExpGolombCodedInt(); + // for (i = sps_sub_layer_ordering_info_present_flag ? 0 : sps_max_sub_layers_minus1; ...) + for (int i = bitArray.readBit() ? 0 : maxSubLayersMinus1; i <= maxSubLayersMinus1; i++) { + bitArray.readUnsignedExpGolombCodedInt(); // sps_max_dec_pic_buffering_minus1[i] + bitArray.readUnsignedExpGolombCodedInt(); // sps_max_num_reorder_pics[i] + bitArray.readUnsignedExpGolombCodedInt(); // sps_max_latency_increase_plus1[i] + } + bitArray.readUnsignedExpGolombCodedInt(); // log2_min_luma_coding_block_size_minus3 + bitArray.readUnsignedExpGolombCodedInt(); // log2_diff_max_min_luma_coding_block_size + bitArray.readUnsignedExpGolombCodedInt(); // log2_min_luma_transform_block_size_minus2 + bitArray.readUnsignedExpGolombCodedInt(); // log2_diff_max_min_luma_transform_block_size + bitArray.readUnsignedExpGolombCodedInt(); // max_transform_hierarchy_depth_inter + bitArray.readUnsignedExpGolombCodedInt(); // max_transform_hierarchy_depth_intra + // if (scaling_list_enabled_flag) { if (sps_scaling_list_data_present_flag) {...}} + if (bitArray.readBit() && bitArray.readBit()) { + skipScalingList(bitArray); + } + bitArray.skipBits(2); // amp_enabled_flag (1), sample_adaptive_offset_enabled_flag (1) + if (bitArray.readBit()) { // pcm_enabled_flag + // pcm_sample_bit_depth_luma_minus1 (4), pcm_sample_bit_depth_chroma_minus1 (4) + bitArray.skipBits(8); + bitArray.readUnsignedExpGolombCodedInt(); // log2_min_pcm_luma_coding_block_size_minus3 + bitArray.readUnsignedExpGolombCodedInt(); // log2_diff_max_min_pcm_luma_coding_block_size + bitArray.skipBits(1); // pcm_loop_filter_disabled_flag + } + // Skips all short term reference picture sets. + skipShortTermRefPicSets(bitArray); + if (bitArray.readBit()) { // long_term_ref_pics_present_flag + // num_long_term_ref_pics_sps + for (int i = 0; i < bitArray.readUnsignedExpGolombCodedInt(); i++) { + int ltRefPicPocLsbSpsLength = log2MaxPicOrderCntLsbMinus4 + 4; + // lt_ref_pic_poc_lsb_sps[i], used_by_curr_pic_lt_sps_flag[i] + bitArray.skipBits(ltRefPicPocLsbSpsLength + 1); + } + } + bitArray.skipBits(2); // sps_temporal_mvp_enabled_flag, strong_intra_smoothing_enabled_flag + float pixelWidthHeightRatio = 1; + if (bitArray.readBit()) { // vui_parameters_present_flag + if (bitArray.readBit()) { // aspect_ratio_info_present_flag + int aspectRatioIdc = bitArray.readBits(8); + if (aspectRatioIdc == NalUnitUtil.EXTENDED_SAR) { + int sarWidth = bitArray.readBits(16); + int sarHeight = bitArray.readBits(16); + if (sarWidth != 0 && sarHeight != 0) { + pixelWidthHeightRatio = (float) sarWidth / sarHeight; + } + } else if (aspectRatioIdc < NalUnitUtil.ASPECT_RATIO_IDC_VALUES.length) { + pixelWidthHeightRatio = NalUnitUtil.ASPECT_RATIO_IDC_VALUES[aspectRatioIdc]; + } else { + Log.w(TAG, "Unexpected aspect_ratio_idc value: " + aspectRatioIdc); + } + } + } + + return MediaFormat.createVideoFormat(null, MimeTypes.VIDEO_H265, MediaFormat.NO_VALUE, + MediaFormat.NO_VALUE, C.UNKNOWN_TIME_US, picWidthInLumaSamples, picHeightInLumaSamples, + Collections.singletonList(csd), MediaFormat.NO_VALUE, pixelWidthHeightRatio); + } + + /** + * Skips scaling_list_data(). See H.265/HEVC (2014) 7.3.4. + */ + private static void skipScalingList(ParsableBitArray bitArray) { + for (int sizeId = 0; sizeId < 4; sizeId++) { + for (int matrixId = 0; matrixId < 6; matrixId += sizeId == 3 ? 3 : 1) { + if (!bitArray.readBit()) { // scaling_list_pred_mode_flag[sizeId][matrixId] + // scaling_list_pred_matrix_id_delta[sizeId][matrixId] + bitArray.readUnsignedExpGolombCodedInt(); + } else { + int coefNum = Math.min(64, 1 << (4 + (sizeId << 1))); + if (sizeId > 1) { + // scaling_list_dc_coef_minus8[sizeId - 2][matrixId] + bitArray.readSignedExpGolombCodedInt(); + } + for (int i = 0; i < coefNum; i++) { + bitArray.readSignedExpGolombCodedInt(); // scaling_list_delta_coef + } + } + } + } + } + + /** + * Reads the number of short term reference picture sets in a SPS as ue(v), then skips all of + * them. See H.265/HEVC (2014) 7.3.7. + */ + private static void skipShortTermRefPicSets(ParsableBitArray bitArray) { + int numShortTermRefPicSets = bitArray.readUnsignedExpGolombCodedInt(); + boolean interRefPicSetPredictionFlag = false; + int numNegativePics = 0; + int numPositivePics = 0; + // As this method applies in a SPS, the only element of NumDeltaPocs accessed is the previous + // one, so we just keep track of that rather than storing the whole array. + // RefRpsIdx = stRpsIdx - (delta_idx_minus1 + 1) and delta_idx_minus1 is always zero in SPS. + int previousNumDeltaPocs = 0; + for (int stRpsIdx = 0; stRpsIdx < numShortTermRefPicSets; stRpsIdx++) { + if (stRpsIdx != 0) { + interRefPicSetPredictionFlag = bitArray.readBit(); + } + if (interRefPicSetPredictionFlag) { + bitArray.skipBits(1); // delta_rps_sign + bitArray.readUnsignedExpGolombCodedInt(); // abs_delta_rps_minus1 + for (int j = 0; j <= previousNumDeltaPocs; j++) { + if (bitArray.readBit()) { // used_by_curr_pic_flag[j] + bitArray.skipBits(1); // use_delta_flag[j] + } + } + } else { + numNegativePics = bitArray.readUnsignedExpGolombCodedInt(); + numPositivePics = bitArray.readUnsignedExpGolombCodedInt(); + previousNumDeltaPocs = numNegativePics + numPositivePics; + for (int i = 0; i < numNegativePics; i++) { + bitArray.readUnsignedExpGolombCodedInt(); // delta_poc_s0_minus1[i] + bitArray.skipBits(1); // used_by_curr_pic_s0_flag[i] + } + for (int i = 0; i < numPositivePics; i++) { + bitArray.readUnsignedExpGolombCodedInt(); // delta_poc_s1_minus1[i] + bitArray.skipBits(1); // used_by_curr_pic_s1_flag[i] + } + } + } + } + + private static final class SampleReader { + + /** + * Offset in bytes of the first_slice_segment_in_pic_flag in a NAL unit containing a + * slice_segment_layer_rbsp. + */ + private static final int FIRST_SLICE_FLAG_OFFSET = 2; + + private final TrackOutput output; + + // Per NAL unit state. A sample consists of one or more NAL units. + private long nalUnitStartPosition; + private boolean nalUnitHasKeyframeData; + private int nalUnitBytesRead; + private long nalUnitTimeUs; + private boolean lookingForFirstSliceFlag; + private boolean isFirstSlice; + private boolean isFirstParameterSet; + + // Per sample state that gets reset at the start of each sample. + private boolean readingSample; + private boolean writingParameterSets; + private long samplePosition; + private long sampleTimeUs; + private boolean sampleIsKeyframe; + + public SampleReader(TrackOutput output) { + this.output = output; + } + + public void reset() { + lookingForFirstSliceFlag = false; + isFirstSlice = false; + isFirstParameterSet = false; + readingSample = false; + writingParameterSets = false; + } + + public void startNalUnit(long position, int offset, int nalUnitType, long pesTimeUs) { + isFirstSlice = false; + isFirstParameterSet = false; + nalUnitTimeUs = pesTimeUs; + nalUnitBytesRead = 0; + nalUnitStartPosition = position; + + if (nalUnitType >= VPS_NUT) { + if (!writingParameterSets && readingSample) { + // This is a non-VCL NAL unit, so flush the previous sample. + outputSample(offset); + readingSample = false; + } + if (nalUnitType <= PPS_NUT) { + // This sample will have parameter sets at the start. + isFirstParameterSet = !writingParameterSets; + writingParameterSets = true; + } + } + + // Look for the flag if this NAL unit contains a slice_segment_layer_rbsp. + nalUnitHasKeyframeData = (nalUnitType >= BLA_W_LP && nalUnitType <= CRA_NUT); + lookingForFirstSliceFlag = nalUnitHasKeyframeData || nalUnitType <= RASL_R; + } + + public void readNalUnitData(byte[] data, int offset, int limit) { + if (lookingForFirstSliceFlag) { + int headerOffset = offset + FIRST_SLICE_FLAG_OFFSET - nalUnitBytesRead; + if (headerOffset < limit) { + isFirstSlice = (data[headerOffset] & 0x80) != 0; + lookingForFirstSliceFlag = false; + } else { + nalUnitBytesRead += limit - offset; + } + } + } + + public void endNalUnit(long position, int offset) { + if (writingParameterSets && isFirstSlice) { + // This sample has parameter sets. Reset the key-frame flag based on the first slice. + sampleIsKeyframe = nalUnitHasKeyframeData; + writingParameterSets = false; + } else if (isFirstParameterSet || isFirstSlice) { + // This NAL unit is at the start of a new sample (access unit). + if (readingSample) { + // Output the sample ending before this NAL unit. + int nalUnitLength = (int) (position - nalUnitStartPosition); + outputSample(offset + nalUnitLength); + } + samplePosition = nalUnitStartPosition; + sampleTimeUs = nalUnitTimeUs; + readingSample = true; + sampleIsKeyframe = nalUnitHasKeyframeData; + } + } + + private void outputSample(int offset) { + int flags = sampleIsKeyframe ? C.SAMPLE_FLAG_SYNC : 0; + int size = (int) (nalUnitStartPosition - samplePosition); + output.sampleMetadata(sampleTimeUs, flags, size, offset, null); + } + + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ts/Id3Reader.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ts/Id3Reader.java new file mode 100755 index 000000000..627a8b9f9 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ts/Id3Reader.java @@ -0,0 +1,94 @@ +/* + * 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.messenger.exoplayer.extractor.ts; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.MediaFormat; +import org.telegram.messenger.exoplayer.extractor.TrackOutput; +import org.telegram.messenger.exoplayer.util.ParsableByteArray; + +/** + * Parses ID3 data and extracts individual text information frames. + */ +/* package */ final class Id3Reader extends ElementaryStreamReader { + + private static final int ID3_HEADER_SIZE = 10; + + private final ParsableByteArray id3Header; + + // State that should be reset on seek. + private boolean writingSample; + + // Per sample state that gets reset at the start of each sample. + private long sampleTimeUs; + private int sampleSize; + private int sampleBytesRead; + + public Id3Reader(TrackOutput output) { + super(output); + output.format(MediaFormat.createId3Format()); + id3Header = new ParsableByteArray(ID3_HEADER_SIZE); + } + + @Override + public void seek() { + writingSample = false; + } + + @Override + public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { + if (!dataAlignmentIndicator) { + return; + } + writingSample = true; + sampleTimeUs = pesTimeUs; + sampleSize = 0; + sampleBytesRead = 0; + } + + @Override + public void consume(ParsableByteArray data) { + if (!writingSample) { + return; + } + int bytesAvailable = data.bytesLeft(); + if (sampleBytesRead < ID3_HEADER_SIZE) { + // We're still reading the ID3 header. + int headerBytesAvailable = Math.min(bytesAvailable, ID3_HEADER_SIZE - sampleBytesRead); + System.arraycopy(data.data, data.getPosition(), id3Header.data, sampleBytesRead, + headerBytesAvailable); + if (sampleBytesRead + headerBytesAvailable == ID3_HEADER_SIZE) { + // We've finished reading the ID3 header. Extract the sample size. + id3Header.setPosition(6); // 'ID3' (3) + version (2) + flags (1) + sampleSize = ID3_HEADER_SIZE + id3Header.readSynchSafeInt(); + } + } + // Write data to the output. + int bytesToWrite = Math.min(bytesAvailable, sampleSize - sampleBytesRead); + output.sampleData(data, bytesToWrite); + sampleBytesRead += bytesToWrite; + } + + @Override + public void packetFinished() { + if (!writingSample || sampleSize == 0 || sampleBytesRead != sampleSize) { + return; + } + output.sampleMetadata(sampleTimeUs, C.SAMPLE_FLAG_SYNC, sampleSize, 0, null); + writingSample = false; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ts/MpegAudioReader.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ts/MpegAudioReader.java new file mode 100755 index 000000000..640d1a361 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ts/MpegAudioReader.java @@ -0,0 +1,204 @@ +/* + * 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.messenger.exoplayer.extractor.ts; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.MediaFormat; +import org.telegram.messenger.exoplayer.extractor.TrackOutput; +import org.telegram.messenger.exoplayer.util.MpegAudioHeader; +import org.telegram.messenger.exoplayer.util.ParsableByteArray; + +/** + * Parses a continuous MPEG Audio byte stream and extracts individual frames. + */ +/* package */ final class MpegAudioReader extends ElementaryStreamReader { + + private static final int STATE_FINDING_HEADER = 0; + private static final int STATE_READING_HEADER = 1; + private static final int STATE_READING_FRAME = 2; + + private static final int HEADER_SIZE = 4; + + private final ParsableByteArray headerScratch; + private final MpegAudioHeader header; + + private int state; + private int frameBytesRead; + private boolean hasOutputFormat; + + // Used when finding the frame header. + private boolean lastByteWasFF; + + // Parsed from the frame header. + private long frameDurationUs; + private int frameSize; + + // The timestamp to attach to the next sample in the current packet. + private long timeUs; + + public MpegAudioReader(TrackOutput output) { + super(output); + state = STATE_FINDING_HEADER; + // The first byte of an MPEG Audio frame header is always 0xFF. + headerScratch = new ParsableByteArray(4); + headerScratch.data[0] = (byte) 0xFF; + header = new MpegAudioHeader(); + } + + @Override + public void seek() { + state = STATE_FINDING_HEADER; + frameBytesRead = 0; + lastByteWasFF = false; + } + + @Override + public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { + timeUs = pesTimeUs; + } + + @Override + public void consume(ParsableByteArray data) { + while (data.bytesLeft() > 0) { + switch (state) { + case STATE_FINDING_HEADER: + findHeader(data); + break; + case STATE_READING_HEADER: + readHeaderRemainder(data); + break; + case STATE_READING_FRAME: + readFrameRemainder(data); + break; + } + } + } + + @Override + public void packetFinished() { + // Do nothing. + } + + /** + * Attempts to locate the start of the next frame header. + *

+ * If a frame header is located then the state is changed to {@link #STATE_READING_HEADER}, the + * first two bytes of the header are written into {@link #headerScratch}, and the position of the + * source is advanced to the byte that immediately follows these two bytes. + *

+ * If a frame header is not located then the position of the source is advanced to the limit, and + * the method should be called again with the next source to continue the search. + * + * @param source The source from which to read. + */ + private void findHeader(ParsableByteArray source) { + byte[] data = source.data; + int startOffset = source.getPosition(); + int endOffset = source.limit(); + for (int i = startOffset; i < endOffset; i++) { + boolean byteIsFF = (data[i] & 0xFF) == 0xFF; + boolean found = lastByteWasFF && (data[i] & 0xE0) == 0xE0; + lastByteWasFF = byteIsFF; + if (found) { + source.setPosition(i + 1); + // Reset lastByteWasFF for next time. + lastByteWasFF = false; + headerScratch.data[1] = data[i]; + frameBytesRead = 2; + state = STATE_READING_HEADER; + return; + } + } + source.setPosition(endOffset); + } + + /** + * Attempts to read the remaining two bytes of the frame header. + *

+ * If a frame header is read in full then the state is changed to {@link #STATE_READING_FRAME}, + * the media format is output if this has not previously occurred, the four header bytes are + * output as sample data, and the position of the source is advanced to the byte that immediately + * follows the header. + *

+ * If a frame header is read in full but cannot be parsed then the state is changed to + * {@link #STATE_READING_HEADER}. + *

+ * If a frame header is not read in full then the position of the source is advanced to the limit, + * and the method should be called again with the next source to continue the read. + * + * @param source The source from which to read. + */ + private void readHeaderRemainder(ParsableByteArray source) { + int bytesToRead = Math.min(source.bytesLeft(), HEADER_SIZE - frameBytesRead); + source.readBytes(headerScratch.data, frameBytesRead, bytesToRead); + frameBytesRead += bytesToRead; + if (frameBytesRead < HEADER_SIZE) { + // We haven't read the whole header yet. + return; + } + + headerScratch.setPosition(0); + boolean parsedHeader = MpegAudioHeader.populateHeader(headerScratch.readInt(), header); + if (!parsedHeader) { + // We thought we'd located a frame header, but we hadn't. + frameBytesRead = 0; + state = STATE_READING_HEADER; + return; + } + + frameSize = header.frameSize; + if (!hasOutputFormat) { + frameDurationUs = (C.MICROS_PER_SECOND * header.samplesPerFrame) / header.sampleRate; + MediaFormat mediaFormat = MediaFormat.createAudioFormat(null, header.mimeType, + MediaFormat.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, C.UNKNOWN_TIME_US, + header.channels, header.sampleRate, null, null); + output.format(mediaFormat); + hasOutputFormat = true; + } + + headerScratch.setPosition(0); + output.sampleData(headerScratch, HEADER_SIZE); + state = STATE_READING_FRAME; + } + + /** + * Attempts to read the remainder of the frame. + *

+ * If a frame is read in full then true is returned. The frame will have been output, and the + * position of the source will have been advanced to the byte that immediately follows the end of + * the frame. + *

+ * If a frame is not read in full then the position of the source will have been advanced to the + * limit, and the method should be called again with the next source to continue the read. + * + * @param source The source from which to read. + */ + private void readFrameRemainder(ParsableByteArray source) { + int bytesToRead = Math.min(source.bytesLeft(), frameSize - frameBytesRead); + output.sampleData(source, bytesToRead); + frameBytesRead += bytesToRead; + if (frameBytesRead < frameSize) { + // We haven't read the whole of the frame yet. + return; + } + + output.sampleMetadata(timeUs, C.SAMPLE_FLAG_SYNC, frameSize, 0, null); + timeUs += frameDurationUs; + frameBytesRead = 0; + state = STATE_FINDING_HEADER; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ts/NalUnitTargetBuffer.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ts/NalUnitTargetBuffer.java new file mode 100755 index 000000000..630b4c7e5 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ts/NalUnitTargetBuffer.java @@ -0,0 +1,110 @@ +/* + * 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.messenger.exoplayer.extractor.ts; + +import org.telegram.messenger.exoplayer.util.Assertions; + +import java.util.Arrays; + +/** + * A buffer that fills itself with data corresponding to a specific NAL unit, as it is + * encountered in the stream. + */ +/* package */ final class NalUnitTargetBuffer { + + private final int targetType; + + private boolean isFilling; + private boolean isCompleted; + + public byte[] nalData; + public int nalLength; + + public NalUnitTargetBuffer(int targetType, int initialCapacity) { + this.targetType = targetType; + + // Initialize data with a start code in the first three bytes. + nalData = new byte[3 + initialCapacity]; + nalData[2] = 1; + } + + /** + * Resets the buffer, clearing any data that it holds. + */ + public void reset() { + isFilling = false; + isCompleted = false; + } + + /** + * Returns whether the buffer currently holds a complete NAL unit of the target type. + */ + public boolean isCompleted() { + return isCompleted; + } + + /** + * Invoked to indicate that a NAL unit has started. + * + * @param type The type of the NAL unit. + */ + public void startNalUnit(int type) { + Assertions.checkState(!isFilling); + isFilling = type == targetType; + if (isFilling) { + // Skip the three byte start code when writing data. + nalLength = 3; + isCompleted = false; + } + } + + /** + * Invoked to pass stream data. The data passed should not include the 3 byte start code. + * + * @param data Holds the data being passed. + * @param offset The offset of the data in {@code data}. + * @param limit The limit (exclusive) of the data in {@code data}. + */ + public void appendToNalUnit(byte[] data, int offset, int limit) { + if (!isFilling) { + return; + } + int readLength = limit - offset; + if (nalData.length < nalLength + readLength) { + nalData = Arrays.copyOf(nalData, (nalLength + readLength) * 2); + } + System.arraycopy(data, offset, nalData, nalLength, readLength); + nalLength += readLength; + } + + /** + * Invoked to indicate that a NAL unit has ended. + * + * @param discardPadding The number of excess bytes that were passed to + * {@link #appendToNalUnit(byte[], int, int)}, which should be discarded. + * @return True if the ended NAL unit is of the target type. False otherwise. + */ + public boolean endNalUnit(int discardPadding) { + if (!isFilling) { + return false; + } + nalLength -= discardPadding; + isFilling = false; + isCompleted = true; + return true; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ts/PsExtractor.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ts/PsExtractor.java new file mode 100755 index 000000000..465483dd0 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ts/PsExtractor.java @@ -0,0 +1,328 @@ +/* + * 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.messenger.exoplayer.extractor.ts; + +import org.telegram.messenger.exoplayer.extractor.Extractor; +import org.telegram.messenger.exoplayer.extractor.ExtractorInput; +import org.telegram.messenger.exoplayer.extractor.ExtractorOutput; +import org.telegram.messenger.exoplayer.extractor.PositionHolder; +import org.telegram.messenger.exoplayer.extractor.SeekMap; +import org.telegram.messenger.exoplayer.util.ParsableBitArray; +import org.telegram.messenger.exoplayer.util.ParsableByteArray; + +import android.util.SparseArray; + +import java.io.IOException; + +/** + * Facilitates the extraction of data from the MPEG-2 TS container format. + */ +public final class PsExtractor implements Extractor { + + private static final int PACK_START_CODE = 0x000001BA; + private static final int SYSTEM_HEADER_START_CODE = 0x000001BB; + private static final int PACKET_START_CODE_PREFIX = 0x000001; + private static final int MPEG_PROGRAM_END_CODE = 0x000001B9; + private static final long MAX_SEARCH_LENGTH = 1024 * 1024; + + public static final int PRIVATE_STREAM_1 = 0xBD; + public static final int AUDIO_STREAM = 0xC0; + public static final int AUDIO_STREAM_MASK = 0xE0; + public static final int VIDEO_STREAM = 0xE0; + public static final int VIDEO_STREAM_MASK = 0xF0; + + private final PtsTimestampAdjuster ptsTimestampAdjuster; + private final SparseArray psPayloadReaders; // Indexed by pid + private final ParsableByteArray psPacketBuffer; + private boolean foundAllTracks; + private boolean foundAudioTrack; + private boolean foundVideoTrack; + + // Accessed only by the loading thread. + private ExtractorOutput output; + + public PsExtractor() { + this(new PtsTimestampAdjuster(0)); + } + + public PsExtractor(PtsTimestampAdjuster ptsTimestampAdjuster) { + this.ptsTimestampAdjuster = ptsTimestampAdjuster; + psPacketBuffer = new ParsableByteArray(4096); + psPayloadReaders = new SparseArray<>(); + } + + // Extractor implementation. + + @Override + public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + byte[] scratch = new byte[14]; + input.peekFully(scratch, 0, 14); + + // Verify the PACK_START_CODE for the first 4 bytes + if (PACK_START_CODE != (((scratch[0] & 0xFF) << 24) | ((scratch[1] & 0xFF) << 16) + | ((scratch[2] & 0xFF) << 8) | (scratch[3] & 0xFF))) { + return false; + } + // Verify the 01xxx1xx marker on the 5th byte + if ((scratch[4] & 0xC4) != 0x44) { + return false; + } + // Verify the xxxxx1xx marker on the 7th byte + if ((scratch[6] & 0x04) != 0x04) { + return false; + } + // Verify the xxxxx1xx marker on the 9th byte + if ((scratch[8] & 0x04) != 0x04) { + return false; + } + // Verify the xxxxxxx1 marker on the 10th byte + if ((scratch[9] & 0x01) != 0x01) { + return false; + } + // Verify the xxxxxx11 marker on the 13th byte + if ((scratch[12] & 0x03) != 0x03) { + return false; + } + // Read the stuffing length from the 14th byte (last 3 bits) + int packStuffingLength = scratch[13] & 0x07; + input.advancePeekPosition(packStuffingLength); + // Now check that the next 3 bytes are the beginning of an MPEG start code + input.peekFully(scratch, 0, 3); + return (PACKET_START_CODE_PREFIX == (((scratch[0] & 0xFF) << 16) | ((scratch[1] & 0xFF) << 8) + | (scratch[2] & 0xFF))); + } + + @Override + public void init(ExtractorOutput output) { + this.output = output; + output.seekMap(SeekMap.UNSEEKABLE); + } + + @Override + public void seek() { + ptsTimestampAdjuster.reset(); + for (int i = 0; i < psPayloadReaders.size(); i++) { + psPayloadReaders.valueAt(i).seek(); + } + } + + @Override + public void release() { + // Do nothing + } + + @Override + public int read(ExtractorInput input, PositionHolder seekPosition) + throws IOException, InterruptedException { + // First peek and check what type of start code is next. + if (!input.peekFully(psPacketBuffer.data, 0, 4, true)) { + return RESULT_END_OF_INPUT; + } + + psPacketBuffer.setPosition(0); + int nextStartCode = psPacketBuffer.readInt(); + if (nextStartCode == MPEG_PROGRAM_END_CODE) { + return RESULT_END_OF_INPUT; + } else if (nextStartCode == PACK_START_CODE) { + // Now peek the rest of the pack_header. + input.peekFully(psPacketBuffer.data, 0, 10); + + // We only care about the pack_stuffing_length in here, skip the first 77 bits. + psPacketBuffer.setPosition(0); + psPacketBuffer.skipBytes(9); + + // Last 3 bits is the length. + int packStuffingLength = psPacketBuffer.readUnsignedByte() & 0x07; + + // Now skip the stuffing and the pack header. + input.skipFully(packStuffingLength + 14); + return RESULT_CONTINUE; + } else if (nextStartCode == SYSTEM_HEADER_START_CODE) { + // We just skip all this, but we need to get the length first. + input.peekFully(psPacketBuffer.data, 0, 2); + + // Length is the next 2 bytes. + psPacketBuffer.setPosition(0); + int systemHeaderLength = psPacketBuffer.readUnsignedShort(); + input.skipFully(systemHeaderLength + 6); + return RESULT_CONTINUE; + } else if (((nextStartCode & 0xFFFFFF00) >> 8) != PACKET_START_CODE_PREFIX) { + input.skipFully(1); // Skip bytes until we see a valid start code again. + return RESULT_CONTINUE; + } + + // We're at the start of a regular PES packet now. + // Get the stream ID off the last byte of the start code. + int streamId = nextStartCode & 0xFF; + + // Check to see if we have this one in our map yet, and if not, then add it. + PesReader payloadReader = psPayloadReaders.get(streamId); + if (!foundAllTracks) { + if (payloadReader == null) { + ElementaryStreamReader elementaryStreamReader = null; + if (!foundAudioTrack && streamId == PRIVATE_STREAM_1) { + // Private stream, used for AC3 audio. + // NOTE: This may need further parsing to determine if its DTS, but that's likely only + // valid for DVDs. + elementaryStreamReader = new Ac3Reader(output.track(streamId), false); + foundAudioTrack = true; + } else if (!foundAudioTrack && (streamId & AUDIO_STREAM_MASK) == AUDIO_STREAM) { + elementaryStreamReader = new MpegAudioReader(output.track(streamId)); + foundAudioTrack = true; + } else if (!foundVideoTrack && (streamId & VIDEO_STREAM_MASK) == VIDEO_STREAM) { + elementaryStreamReader = new H262Reader(output.track(streamId)); + foundVideoTrack = true; + } + if (elementaryStreamReader != null) { + payloadReader = new PesReader(elementaryStreamReader, ptsTimestampAdjuster); + psPayloadReaders.put(streamId, payloadReader); + } + } + if ((foundAudioTrack && foundVideoTrack) || input.getPosition() > MAX_SEARCH_LENGTH) { + foundAllTracks = true; + output.endTracks(); + } + } + + // The next 2 bytes are the length, once we have that we can consume the complete packet. + input.peekFully(psPacketBuffer.data, 0, 2); + psPacketBuffer.setPosition(0); + int payloadLength = psPacketBuffer.readUnsignedShort(); + int pesLength = payloadLength + 6; + + if (payloadReader == null) { + // Just skip this data. + input.skipFully(pesLength); + } else { + if (psPacketBuffer.capacity() < pesLength) { + // Reallocate for this and future packets. + psPacketBuffer.reset(new byte[pesLength], pesLength); + } + // Read the whole packet and the header for consumption. + input.readFully(psPacketBuffer.data, 0, pesLength); + psPacketBuffer.setPosition(6); + psPacketBuffer.setLimit(pesLength); + payloadReader.consume(psPacketBuffer, output); + psPacketBuffer.setLimit(psPacketBuffer.capacity()); + } + + return RESULT_CONTINUE; + } + + // Internals. + + /** + * Parses PES packet data and extracts samples. + */ + private static final class PesReader { + + private static final int PES_SCRATCH_SIZE = 64; + + private final ElementaryStreamReader pesPayloadReader; + private final PtsTimestampAdjuster ptsTimestampAdjuster; + private final ParsableBitArray pesScratch; + + private boolean ptsFlag; + private boolean dtsFlag; + private boolean seenFirstDts; + private int extendedHeaderLength; + private long timeUs; + + public PesReader(ElementaryStreamReader pesPayloadReader, + PtsTimestampAdjuster ptsTimestampAdjuster) { + this.pesPayloadReader = pesPayloadReader; + this.ptsTimestampAdjuster = ptsTimestampAdjuster; + pesScratch = new ParsableBitArray(new byte[PES_SCRATCH_SIZE]); + } + + /** + * Notifies the reader that a seek has occurred. + *

+ * Following a call to this method, the data passed to the next invocation of + * {@link #consume(ParsableByteArray, ExtractorOutput)} will not be a continuation of + * the data that was previously passed. Hence the reader should reset any internal state. + */ + public void seek() { + seenFirstDts = false; + pesPayloadReader.seek(); + } + + /** + * Consumes the payload of a PS packet. + * + * @param data The PES packet. The position will be set to the start of the payload. + * @param output The output to which parsed data should be written. + */ + public void consume(ParsableByteArray data, ExtractorOutput output) { + data.readBytes(pesScratch.data, 0, 3); + pesScratch.setPosition(0); + parseHeader(); + data.readBytes(pesScratch.data, 0, extendedHeaderLength); + pesScratch.setPosition(0); + parseHeaderExtension(); + pesPayloadReader.packetStarted(timeUs, true); + pesPayloadReader.consume(data); + // We always have complete PES packets with program stream. + pesPayloadReader.packetFinished(); + } + + private void parseHeader() { + // Note: see ISO/IEC 13818-1, section 2.4.3.6 for detailed information on the format of + // the header. + // First 8 bits are skipped: '10' (2), PES_scrambling_control (2), PES_priority (1), + // data_alignment_indicator (1), copyright (1), original_or_copy (1) + pesScratch.skipBits(8); + ptsFlag = pesScratch.readBit(); + dtsFlag = pesScratch.readBit(); + // ESCR_flag (1), ES_rate_flag (1), DSM_trick_mode_flag (1), + // additional_copy_info_flag (1), PES_CRC_flag (1), PES_extension_flag (1) + pesScratch.skipBits(6); + extendedHeaderLength = pesScratch.readBits(8); + } + + private void parseHeaderExtension() { + timeUs = 0; + if (ptsFlag) { + pesScratch.skipBits(4); // '0010' or '0011' + long pts = (long) pesScratch.readBits(3) << 30; + pesScratch.skipBits(1); // marker_bit + pts |= pesScratch.readBits(15) << 15; + pesScratch.skipBits(1); // marker_bit + pts |= pesScratch.readBits(15); + pesScratch.skipBits(1); // marker_bit + if (!seenFirstDts && dtsFlag) { + pesScratch.skipBits(4); // '0011' + long dts = (long) pesScratch.readBits(3) << 30; + pesScratch.skipBits(1); // marker_bit + dts |= pesScratch.readBits(15) << 15; + pesScratch.skipBits(1); // marker_bit + dts |= pesScratch.readBits(15); + pesScratch.skipBits(1); // marker_bit + // Subsequent PES packets may have earlier presentation timestamps than this one, but they + // should all be greater than or equal to this packet's decode timestamp. We feed the + // decode timestamp to the adjuster here so that in the case that this is the first to be + // fed, the adjuster will be able to compute an offset to apply such that the adjusted + // presentation timestamps of all future packets are non-negative. + ptsTimestampAdjuster.adjustTimestamp(dts); + seenFirstDts = true; + } + timeUs = ptsTimestampAdjuster.adjustTimestamp(pts); + } + } + + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ts/PtsTimestampAdjuster.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ts/PtsTimestampAdjuster.java new file mode 100755 index 000000000..0e55a3a82 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ts/PtsTimestampAdjuster.java @@ -0,0 +1,115 @@ +/* + * 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.messenger.exoplayer.extractor.ts; + +import org.telegram.messenger.exoplayer.C; + +/** + * Scales and adjusts MPEG-2 TS presentation timestamps, taking into account an initial offset and + * timestamp rollover. + */ +public final class PtsTimestampAdjuster { + + /** + * A special {@code firstSampleTimestampUs} value indicating that presentation timestamps should + * not be offset. + */ + public static final long DO_NOT_OFFSET = Long.MAX_VALUE; + + /** + * The value one greater than the largest representable (33 bit) presentation timestamp. + */ + private static final long MAX_PTS_PLUS_ONE = 0x200000000L; + + private final long firstSampleTimestampUs; + + private long timestampOffsetUs; + + // Volatile to allow isInitialized to be called on a different thread to adjustTimestamp. + private volatile long lastPts; + + /** + * @param firstSampleTimestampUs The desired result of the first call to + * {@link #adjustTimestamp(long)}, or {@link #DO_NOT_OFFSET} if presentation timestamps + * should not be offset. + */ + public PtsTimestampAdjuster(long firstSampleTimestampUs) { + this.firstSampleTimestampUs = firstSampleTimestampUs; + lastPts = Long.MIN_VALUE; + } + + /** + * Resets the instance to its initial state. + */ + public void reset() { + lastPts = Long.MIN_VALUE; + } + + /** + * Whether this adjuster has been initialized with a first MPEG-2 TS presentation timestamp. + */ + public boolean isInitialized() { + return lastPts != Long.MIN_VALUE; + } + + /** + * Scales and offsets an MPEG-2 TS presentation timestamp. + * + * @param pts The MPEG-2 TS presentation timestamp. + * @return The adjusted timestamp in microseconds. + */ + public long adjustTimestamp(long pts) { + if (lastPts != Long.MIN_VALUE) { + // The wrap count for the current PTS may be closestWrapCount or (closestWrapCount - 1), + // and we need to snap to the one closest to lastPts. + long closestWrapCount = (lastPts + (MAX_PTS_PLUS_ONE / 2)) / MAX_PTS_PLUS_ONE; + long ptsWrapBelow = pts + (MAX_PTS_PLUS_ONE * (closestWrapCount - 1)); + long ptsWrapAbove = pts + (MAX_PTS_PLUS_ONE * closestWrapCount); + pts = Math.abs(ptsWrapBelow - lastPts) < Math.abs(ptsWrapAbove - lastPts) + ? ptsWrapBelow : ptsWrapAbove; + } + // Calculate the corresponding timestamp. + long timeUs = ptsToUs(pts); + if (firstSampleTimestampUs != DO_NOT_OFFSET && lastPts == Long.MIN_VALUE) { + // Calculate the timestamp offset. + timestampOffsetUs = firstSampleTimestampUs - timeUs; + } + // Record the adjusted PTS to adjust for wraparound next time. + lastPts = pts; + return timeUs + timestampOffsetUs; + } + + /** + * Converts a value in MPEG-2 timestamp units to the corresponding value in microseconds. + * + * @param pts A value in MPEG-2 timestamp units. + * @return The corresponding value in microseconds. + */ + public static long ptsToUs(long pts) { + return (pts * C.MICROS_PER_SECOND) / 90000; + } + + /** + * Converts a value in microseconds to the corresponding values in MPEG-2 timestamp units. + * + * @param us A value in microseconds. + * @return The corresponding value in MPEG-2 timestamp units. + */ + public static long usToPts(long us) { + return (us * 90000) / C.MICROS_PER_SECOND; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ts/SeiReader.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ts/SeiReader.java new file mode 100755 index 000000000..52629314e --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ts/SeiReader.java @@ -0,0 +1,65 @@ +/* + * 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.messenger.exoplayer.extractor.ts; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.MediaFormat; +import org.telegram.messenger.exoplayer.extractor.TrackOutput; +import org.telegram.messenger.exoplayer.text.eia608.Eia608Parser; +import org.telegram.messenger.exoplayer.util.MimeTypes; +import org.telegram.messenger.exoplayer.util.ParsableByteArray; + +/** + * Consumes SEI buffers, outputting contained EIA608 messages to a {@link TrackOutput}. + */ +// TODO: Technically, we shouldn't allow a sample to be read from the queue until we're sure that +// a sample with an earlier timestamp won't be added to it. +/* package */ final class SeiReader { + + private final TrackOutput output; + + public SeiReader(TrackOutput output) { + this.output = output; + output.format(MediaFormat.createTextFormat(null, MimeTypes.APPLICATION_EIA608, + MediaFormat.NO_VALUE, C.UNKNOWN_TIME_US, null)); + } + + public void consume(long pesTimeUs, ParsableByteArray seiBuffer) { + int b; + while (seiBuffer.bytesLeft() > 1 /* last byte will be rbsp_trailing_bits */) { + // Parse payload type. + int payloadType = 0; + do { + b = seiBuffer.readUnsignedByte(); + payloadType += b; + } while (b == 0xFF); + // Parse payload size. + int payloadSize = 0; + do { + b = seiBuffer.readUnsignedByte(); + payloadSize += b; + } while (b == 0xFF); + // Process the payload. + if (Eia608Parser.isSeiMessageEia608(payloadType, payloadSize, seiBuffer)) { + output.sampleData(seiBuffer, payloadSize); + output.sampleMetadata(pesTimeUs, C.SAMPLE_FLAG_SYNC, payloadSize, 0, null); + } else { + seiBuffer.skipBytes(payloadSize); + } + } + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ts/TsExtractor.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ts/TsExtractor.java new file mode 100755 index 000000000..38d704071 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/ts/TsExtractor.java @@ -0,0 +1,641 @@ +/* + * 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.messenger.exoplayer.extractor.ts; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.extractor.DummyTrackOutput; +import org.telegram.messenger.exoplayer.extractor.Extractor; +import org.telegram.messenger.exoplayer.extractor.ExtractorInput; +import org.telegram.messenger.exoplayer.extractor.ExtractorOutput; +import org.telegram.messenger.exoplayer.extractor.PositionHolder; +import org.telegram.messenger.exoplayer.extractor.SeekMap; +import org.telegram.messenger.exoplayer.util.ParsableBitArray; +import org.telegram.messenger.exoplayer.util.ParsableByteArray; +import org.telegram.messenger.exoplayer.util.Util; + +import android.util.Log; +import android.util.SparseArray; +import android.util.SparseBooleanArray; + +import java.io.IOException; + +/** + * Facilitates the extraction of data from the MPEG-2 TS container format. + */ +public final class TsExtractor implements Extractor { + + public static final int WORKAROUND_ALLOW_NON_IDR_KEYFRAMES = 1; + public static final int WORKAROUND_IGNORE_AAC_STREAM = 2; + public static final int WORKAROUND_IGNORE_H264_STREAM = 4; + public static final int WORKAROUND_DETECT_ACCESS_UNITS = 8; + + private static final String TAG = "TsExtractor"; + + private static final int TS_PACKET_SIZE = 188; + private static final int TS_SYNC_BYTE = 0x47; // First byte of each TS packet. + private static final int TS_PAT_PID = 0; + + private static final int TS_STREAM_TYPE_MPA = 0x03; + private static final int TS_STREAM_TYPE_MPA_LSF = 0x04; + private static final int TS_STREAM_TYPE_AAC = 0x0F; + private static final int TS_STREAM_TYPE_AC3 = 0x81; + private static final int TS_STREAM_TYPE_DTS = 0x8A; + private static final int TS_STREAM_TYPE_HDMV_DTS = 0x82; + private static final int TS_STREAM_TYPE_E_AC3 = 0x87; + private static final int TS_STREAM_TYPE_H262 = 0x02; + private static final int TS_STREAM_TYPE_H264 = 0x1B; + private static final int TS_STREAM_TYPE_H265 = 0x24; + private static final int TS_STREAM_TYPE_ID3 = 0x15; + private static final int TS_STREAM_TYPE_EIA608 = 0x100; // 0xFF + 1 + + private static final long AC3_FORMAT_IDENTIFIER = Util.getIntegerCodeForString("AC-3"); + private static final long E_AC3_FORMAT_IDENTIFIER = Util.getIntegerCodeForString("EAC3"); + private static final long HEVC_FORMAT_IDENTIFIER = Util.getIntegerCodeForString("HEVC"); + + private final PtsTimestampAdjuster ptsTimestampAdjuster; + private final int workaroundFlags; + private final ParsableByteArray tsPacketBuffer; + private final ParsableBitArray tsScratch; + /* package */ final SparseArray tsPayloadReaders; // Indexed by pid + /* package */ final SparseBooleanArray streamTypes; + + // Accessed only by the loading thread. + private ExtractorOutput output; + /* package */ Id3Reader id3Reader; + + public TsExtractor() { + this(new PtsTimestampAdjuster(0)); + } + + public TsExtractor(PtsTimestampAdjuster ptsTimestampAdjuster) { + this(ptsTimestampAdjuster, 0); + } + + public TsExtractor(PtsTimestampAdjuster ptsTimestampAdjuster, int workaroundFlags) { + this.ptsTimestampAdjuster = ptsTimestampAdjuster; + this.workaroundFlags = workaroundFlags; + tsPacketBuffer = new ParsableByteArray(TS_PACKET_SIZE); + tsScratch = new ParsableBitArray(new byte[3]); + tsPayloadReaders = new SparseArray<>(); + tsPayloadReaders.put(TS_PAT_PID, new PatReader()); + streamTypes = new SparseBooleanArray(); + } + + // Extractor implementation. + + @Override + public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + byte[] scratch = new byte[1]; + for (int i = 0; i < 5; i++) { + input.peekFully(scratch, 0, 1); + if ((scratch[0] & 0xFF) != 0x47) { + return false; + } + input.advancePeekPosition(TS_PACKET_SIZE - 1); + } + return true; + } + + @Override + public void init(ExtractorOutput output) { + this.output = output; + output.seekMap(SeekMap.UNSEEKABLE); + } + + @Override + public void seek() { + ptsTimestampAdjuster.reset(); + for (int i = 0; i < tsPayloadReaders.size(); i++) { + tsPayloadReaders.valueAt(i).seek(); + } + } + + @Override + public void release() { + // Do nothing + } + + @Override + public int read(ExtractorInput input, PositionHolder seekPosition) + throws IOException, InterruptedException { + if (!input.readFully(tsPacketBuffer.data, 0, TS_PACKET_SIZE, true)) { + return RESULT_END_OF_INPUT; + } + + // Note: see ISO/IEC 13818-1, section 2.4.3.2 for detailed information on the format of + // the header. + tsPacketBuffer.setPosition(0); + tsPacketBuffer.setLimit(TS_PACKET_SIZE); + int syncByte = tsPacketBuffer.readUnsignedByte(); + if (syncByte != TS_SYNC_BYTE) { + return RESULT_CONTINUE; + } + + tsPacketBuffer.readBytes(tsScratch, 3); + tsScratch.skipBits(1); // transport_error_indicator + boolean payloadUnitStartIndicator = tsScratch.readBit(); + tsScratch.skipBits(1); // transport_priority + int pid = tsScratch.readBits(13); + tsScratch.skipBits(2); // transport_scrambling_control + boolean adaptationFieldExists = tsScratch.readBit(); + boolean payloadExists = tsScratch.readBit(); + // Last 4 bits of scratch are skipped: continuity_counter + + // Skip the adaptation field. + if (adaptationFieldExists) { + int adaptationFieldLength = tsPacketBuffer.readUnsignedByte(); + tsPacketBuffer.skipBytes(adaptationFieldLength); + } + + // Read the payload. + if (payloadExists) { + TsPayloadReader payloadReader = tsPayloadReaders.get(pid); + if (payloadReader != null) { + payloadReader.consume(tsPacketBuffer, payloadUnitStartIndicator, output); + } + } + + return RESULT_CONTINUE; + } + + // Internals. + + /** + * Parses TS packet payload data. + */ + private abstract static class TsPayloadReader { + + /** + * Notifies the reader that a seek has occurred. + *

+ * Following a call to this method, the data passed to the next invocation of + * {@link #consume(ParsableByteArray, boolean, ExtractorOutput)} will not be a continuation of + * the data that was previously passed. Hence the reader should reset any internal state. + */ + public abstract void seek(); + + /** + * Consumes the payload of a TS packet. + * + * @param data The TS packet. The position will be set to the start of the payload. + * @param payloadUnitStartIndicator Whether payloadUnitStartIndicator was set on the TS packet. + * @param output The output to which parsed data should be written. + */ + public abstract void consume(ParsableByteArray data, boolean payloadUnitStartIndicator, + ExtractorOutput output); + + } + + /** + * Parses Program Association Table data. + */ + private class PatReader extends TsPayloadReader { + + private final ParsableBitArray patScratch; + + public PatReader() { + patScratch = new ParsableBitArray(new byte[4]); + } + + @Override + public void seek() { + // Do nothing. + } + + @Override + public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator, + ExtractorOutput output) { + // Skip pointer. + if (payloadUnitStartIndicator) { + int pointerField = data.readUnsignedByte(); + data.skipBytes(pointerField); + } + + data.readBytes(patScratch, 3); + patScratch.skipBits(12); // table_id (8), section_syntax_indicator (1), '0' (1), reserved (2) + int sectionLength = patScratch.readBits(12); + // transport_stream_id (16), reserved (2), version_number (5), current_next_indicator (1), + // section_number (8), last_section_number (8) + data.skipBytes(5); + + int programCount = (sectionLength - 9) / 4; + for (int i = 0; i < programCount; i++) { + data.readBytes(patScratch, 4); + int programNumber = patScratch.readBits(16); + patScratch.skipBits(3); // reserved (3) + if (programNumber == 0) { + patScratch.skipBits(13); // network_PID (13) + } else { + int pid = patScratch.readBits(13); + tsPayloadReaders.put(pid, new PmtReader()); + } + } + + // Skip CRC_32. + } + + } + + /** + * Parses Program Map Table. + */ + private class PmtReader extends TsPayloadReader { + + private final ParsableBitArray pmtScratch; + private final ParsableByteArray sectionData; + + private int sectionLength; + private int sectionBytesRead; + + public PmtReader() { + pmtScratch = new ParsableBitArray(new byte[5]); + sectionData = new ParsableByteArray(); + } + + @Override + public void seek() { + // Do nothing. + } + + @Override + public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator, + ExtractorOutput output) { + if (payloadUnitStartIndicator) { + // Skip pointer. + int pointerField = data.readUnsignedByte(); + data.skipBytes(pointerField); + + // Note: see ISO/IEC 13818-1, section 2.4.4.8 for detailed information on the format of + // the header. + data.readBytes(pmtScratch, 3); + pmtScratch.skipBits(12); // table_id (8), section_syntax_indicator (1), 0 (1), reserved (2) + sectionLength = pmtScratch.readBits(12); + + if (sectionData.capacity() < sectionLength) { + sectionData.reset(new byte[sectionLength], sectionLength); + } else { + sectionData.reset(); + sectionData.setLimit(sectionLength); + } + } + + int bytesToRead = Math.min(data.bytesLeft(), sectionLength - sectionBytesRead); + data.readBytes(sectionData.data, sectionBytesRead, bytesToRead); + sectionBytesRead += bytesToRead; + if (sectionBytesRead < sectionLength) { + // Not yet fully read. + return; + } + + // program_number (16), reserved (2), version_number (5), current_next_indicator (1), + // section_number (8), last_section_number (8), reserved (3), PCR_PID (13) + // Skip the rest of the PMT header. + sectionData.skipBytes(7); + + // Read program_info_length. + sectionData.readBytes(pmtScratch, 2); + pmtScratch.skipBits(4); + int programInfoLength = pmtScratch.readBits(12); + + // Skip the descriptors. + sectionData.skipBytes(programInfoLength); + + if (id3Reader == null) { + // Setup an ID3 track regardless of whether there's a corresponding entry, in case one + // appears intermittently during playback. See b/20261500. + id3Reader = new Id3Reader(output.track(TS_STREAM_TYPE_ID3)); + } + + int remainingEntriesLength = sectionLength - 9 /* Length of fields before descriptors */ + - programInfoLength - 4 /* CRC length */; + while (remainingEntriesLength > 0) { + sectionData.readBytes(pmtScratch, 5); + int streamType = pmtScratch.readBits(8); + pmtScratch.skipBits(3); // reserved + int elementaryPid = pmtScratch.readBits(13); + pmtScratch.skipBits(4); // reserved + int esInfoLength = pmtScratch.readBits(12); // ES_info_length + if (streamType == 0x06) { + // Read descriptors in PES packets containing private data. + streamType = readPrivateDataStreamType(sectionData, esInfoLength); + } else { + sectionData.skipBytes(esInfoLength); + } + remainingEntriesLength -= esInfoLength + 5; + if (streamTypes.get(streamType)) { + continue; + } + + ElementaryStreamReader pesPayloadReader; + switch (streamType) { + case TS_STREAM_TYPE_MPA: + pesPayloadReader = new MpegAudioReader(output.track(TS_STREAM_TYPE_MPA)); + break; + case TS_STREAM_TYPE_MPA_LSF: + pesPayloadReader = new MpegAudioReader(output.track(TS_STREAM_TYPE_MPA_LSF)); + break; + case TS_STREAM_TYPE_AAC: + pesPayloadReader = (workaroundFlags & WORKAROUND_IGNORE_AAC_STREAM) != 0 ? null + : new AdtsReader(output.track(TS_STREAM_TYPE_AAC), new DummyTrackOutput()); + break; + case TS_STREAM_TYPE_AC3: + pesPayloadReader = new Ac3Reader(output.track(TS_STREAM_TYPE_AC3), false); + break; + case TS_STREAM_TYPE_E_AC3: + pesPayloadReader = new Ac3Reader(output.track(TS_STREAM_TYPE_E_AC3), true); + break; + case TS_STREAM_TYPE_DTS: + case TS_STREAM_TYPE_HDMV_DTS: + pesPayloadReader = new DtsReader(output.track(TS_STREAM_TYPE_DTS)); + break; + case TS_STREAM_TYPE_H262: + pesPayloadReader = new H262Reader(output.track(TS_STREAM_TYPE_H262)); + break; + case TS_STREAM_TYPE_H264: + pesPayloadReader = (workaroundFlags & WORKAROUND_IGNORE_H264_STREAM) != 0 ? null + : new H264Reader(output.track(TS_STREAM_TYPE_H264), + new SeiReader(output.track(TS_STREAM_TYPE_EIA608)), + (workaroundFlags & WORKAROUND_ALLOW_NON_IDR_KEYFRAMES) != 0, + (workaroundFlags & WORKAROUND_DETECT_ACCESS_UNITS) != 0); + break; + case TS_STREAM_TYPE_H265: + pesPayloadReader = new H265Reader(output.track(TS_STREAM_TYPE_H265), + new SeiReader(output.track(TS_STREAM_TYPE_EIA608))); + break; + case TS_STREAM_TYPE_ID3: + pesPayloadReader = id3Reader; + break; + default: + pesPayloadReader = null; + break; + } + + if (pesPayloadReader != null) { + streamTypes.put(streamType, true); + tsPayloadReaders.put(elementaryPid, + new PesReader(pesPayloadReader, ptsTimestampAdjuster)); + } + } + + output.endTracks(); + } + + /** + * Returns the stream type read from a registration descriptor in private data, or -1 if no + * stream type is present. Sets {@code data}'s position to the end of the descriptors. + * + * @param data A buffer with its position set to the start of the first descriptor. + * @param length The length of descriptors to read from the current position in {@code data}. + * @return The stream type read from a registration descriptor in private data, or -1 if no + * stream type is present. + */ + private int readPrivateDataStreamType(ParsableByteArray data, int length) { + int streamType = -1; + int descriptorsEndPosition = data.getPosition() + length; + while (data.getPosition() < descriptorsEndPosition) { + int descriptorTag = data.readUnsignedByte(); + int descriptorLength = data.readUnsignedByte(); + if (descriptorTag == 0x05) { // registration_descriptor + long formatIdentifier = data.readUnsignedInt(); + if (formatIdentifier == AC3_FORMAT_IDENTIFIER) { + streamType = TS_STREAM_TYPE_AC3; + } else if (formatIdentifier == E_AC3_FORMAT_IDENTIFIER) { + streamType = TS_STREAM_TYPE_E_AC3; + } else if (formatIdentifier == HEVC_FORMAT_IDENTIFIER) { + streamType = TS_STREAM_TYPE_H265; + } + break; + } else if (descriptorTag == 0x6A) { // AC-3_descriptor in DVB (ETSI EN 300 468) + streamType = TS_STREAM_TYPE_AC3; + } else if (descriptorTag == 0x7A) { // enhanced_AC-3_descriptor + streamType = TS_STREAM_TYPE_E_AC3; + } else if (descriptorTag == 0x7B) { // DTS_descriptor + streamType = TS_STREAM_TYPE_DTS; + } + + data.skipBytes(descriptorLength); + } + data.setPosition(descriptorsEndPosition); + return streamType; + } + + } + + /** + * Parses PES packet data and extracts samples. + */ + private static final class PesReader extends TsPayloadReader { + + private static final int STATE_FINDING_HEADER = 0; + private static final int STATE_READING_HEADER = 1; + private static final int STATE_READING_HEADER_EXTENSION = 2; + private static final int STATE_READING_BODY = 3; + + private static final int HEADER_SIZE = 9; + private static final int MAX_HEADER_EXTENSION_SIZE = 10; + private static final int PES_SCRATCH_SIZE = 10; // max(HEADER_SIZE, MAX_HEADER_EXTENSION_SIZE) + + private final ElementaryStreamReader pesPayloadReader; + private final PtsTimestampAdjuster ptsTimestampAdjuster; + private final ParsableBitArray pesScratch; + + private int state; + private int bytesRead; + + private boolean ptsFlag; + private boolean dtsFlag; + private boolean seenFirstDts; + private int extendedHeaderLength; + private int payloadSize; + private boolean dataAlignmentIndicator; + private long timeUs; + + public PesReader(ElementaryStreamReader pesPayloadReader, + PtsTimestampAdjuster ptsTimestampAdjuster) { + this.pesPayloadReader = pesPayloadReader; + this.ptsTimestampAdjuster = ptsTimestampAdjuster; + pesScratch = new ParsableBitArray(new byte[PES_SCRATCH_SIZE]); + state = STATE_FINDING_HEADER; + } + + @Override + public void seek() { + state = STATE_FINDING_HEADER; + bytesRead = 0; + seenFirstDts = false; + pesPayloadReader.seek(); + } + + @Override + public void consume(ParsableByteArray data, boolean payloadUnitStartIndicator, + ExtractorOutput output) { + if (payloadUnitStartIndicator) { + switch (state) { + case STATE_FINDING_HEADER: + case STATE_READING_HEADER: + // Expected. + break; + case STATE_READING_HEADER_EXTENSION: + Log.w(TAG, "Unexpected start indicator reading extended header"); + break; + case STATE_READING_BODY: + // If payloadSize == -1 then the length of the previous packet was unspecified, and so + // we only know that it's finished now that we've seen the start of the next one. This + // is expected. If payloadSize != -1, then the length of the previous packet was known, + // but we didn't receive that amount of data. This is not expected. + if (payloadSize != -1) { + Log.w(TAG, "Unexpected start indicator: expected " + payloadSize + " more bytes"); + } + // Either way, notify the reader that it has now finished. + pesPayloadReader.packetFinished(); + break; + } + setState(STATE_READING_HEADER); + } + + while (data.bytesLeft() > 0) { + switch (state) { + case STATE_FINDING_HEADER: + data.skipBytes(data.bytesLeft()); + break; + case STATE_READING_HEADER: + if (continueRead(data, pesScratch.data, HEADER_SIZE)) { + setState(parseHeader() ? STATE_READING_HEADER_EXTENSION : STATE_FINDING_HEADER); + } + break; + case STATE_READING_HEADER_EXTENSION: + int readLength = Math.min(MAX_HEADER_EXTENSION_SIZE, extendedHeaderLength); + // Read as much of the extended header as we're interested in, and skip the rest. + if (continueRead(data, pesScratch.data, readLength) + && continueRead(data, null, extendedHeaderLength)) { + parseHeaderExtension(); + pesPayloadReader.packetStarted(timeUs, dataAlignmentIndicator); + setState(STATE_READING_BODY); + } + break; + case STATE_READING_BODY: + readLength = data.bytesLeft(); + int padding = payloadSize == -1 ? 0 : readLength - payloadSize; + if (padding > 0) { + readLength -= padding; + data.setLimit(data.getPosition() + readLength); + } + pesPayloadReader.consume(data); + if (payloadSize != -1) { + payloadSize -= readLength; + if (payloadSize == 0) { + pesPayloadReader.packetFinished(); + setState(STATE_READING_HEADER); + } + } + break; + } + } + } + + private void setState(int state) { + this.state = state; + bytesRead = 0; + } + + /** + * Continues a read from the provided {@code source} into a given {@code target}. It's assumed + * that the data should be written into {@code target} starting from an offset of zero. + * + * @param source The source from which to read. + * @param target The target into which data is to be read, or {@code null} to skip. + * @param targetLength The target length of the read. + * @return Whether the target length has been reached. + */ + private boolean continueRead(ParsableByteArray source, byte[] target, int targetLength) { + int bytesToRead = Math.min(source.bytesLeft(), targetLength - bytesRead); + if (bytesToRead <= 0) { + return true; + } else if (target == null) { + source.skipBytes(bytesToRead); + } else { + source.readBytes(target, bytesRead, bytesToRead); + } + bytesRead += bytesToRead; + return bytesRead == targetLength; + } + + private boolean parseHeader() { + // Note: see ISO/IEC 13818-1, section 2.4.3.6 for detailed information on the format of + // the header. + pesScratch.setPosition(0); + int startCodePrefix = pesScratch.readBits(24); + if (startCodePrefix != 0x000001) { + Log.w(TAG, "Unexpected start code prefix: " + startCodePrefix); + payloadSize = -1; + return false; + } + + pesScratch.skipBits(8); // stream_id. + int packetLength = pesScratch.readBits(16); + pesScratch.skipBits(5); // '10' (2), PES_scrambling_control (2), PES_priority (1) + dataAlignmentIndicator = pesScratch.readBit(); + pesScratch.skipBits(2); // copyright (1), original_or_copy (1) + ptsFlag = pesScratch.readBit(); + dtsFlag = pesScratch.readBit(); + // ESCR_flag (1), ES_rate_flag (1), DSM_trick_mode_flag (1), + // additional_copy_info_flag (1), PES_CRC_flag (1), PES_extension_flag (1) + pesScratch.skipBits(6); + extendedHeaderLength = pesScratch.readBits(8); + + if (packetLength == 0) { + payloadSize = -1; + } else { + payloadSize = packetLength + 6 /* packetLength does not include the first 6 bytes */ + - HEADER_SIZE - extendedHeaderLength; + } + return true; + } + + private void parseHeaderExtension() { + pesScratch.setPosition(0); + timeUs = C.UNKNOWN_TIME_US; + if (ptsFlag) { + pesScratch.skipBits(4); // '0010' or '0011' + long pts = (long) pesScratch.readBits(3) << 30; + pesScratch.skipBits(1); // marker_bit + pts |= pesScratch.readBits(15) << 15; + pesScratch.skipBits(1); // marker_bit + pts |= pesScratch.readBits(15); + pesScratch.skipBits(1); // marker_bit + if (!seenFirstDts && dtsFlag) { + pesScratch.skipBits(4); // '0011' + long dts = (long) pesScratch.readBits(3) << 30; + pesScratch.skipBits(1); // marker_bit + dts |= pesScratch.readBits(15) << 15; + pesScratch.skipBits(1); // marker_bit + dts |= pesScratch.readBits(15); + pesScratch.skipBits(1); // marker_bit + // Subsequent PES packets may have earlier presentation timestamps than this one, but they + // should all be greater than or equal to this packet's decode timestamp. We feed the + // decode timestamp to the adjuster here so that in the case that this is the first to be + // fed, the adjuster will be able to compute an offset to apply such that the adjusted + // presentation timestamps of all future packets are non-negative. + ptsTimestampAdjuster.adjustTimestamp(dts); + seenFirstDts = true; + } + timeUs = ptsTimestampAdjuster.adjustTimestamp(pts); + } + } + + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/wav/WavExtractor.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/wav/WavExtractor.java new file mode 100755 index 000000000..efad17aef --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/wav/WavExtractor.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2016 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.messenger.exoplayer.extractor.wav; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.MediaFormat; +import org.telegram.messenger.exoplayer.ParserException; +import org.telegram.messenger.exoplayer.extractor.Extractor; +import org.telegram.messenger.exoplayer.extractor.ExtractorInput; +import org.telegram.messenger.exoplayer.extractor.ExtractorOutput; +import org.telegram.messenger.exoplayer.extractor.PositionHolder; +import org.telegram.messenger.exoplayer.extractor.SeekMap; +import org.telegram.messenger.exoplayer.extractor.TrackOutput; +import org.telegram.messenger.exoplayer.util.MimeTypes; + +import java.io.IOException; + +/** {@link Extractor} to extract samples from a WAV byte stream. */ +public final class WavExtractor implements Extractor, SeekMap { + + /** Arbitrary maximum input size of 32KB, which is ~170ms of 16-bit stereo PCM audio at 48KHz. */ + private static final int MAX_INPUT_SIZE = 32 * 1024; + + private ExtractorOutput extractorOutput; + private TrackOutput trackOutput; + private WavHeader wavHeader; + private int bytesPerFrame; + private int pendingBytes; + + @Override + public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + return WavHeaderReader.peek(input) != null; + } + + @Override + public void init(ExtractorOutput output) { + extractorOutput = output; + trackOutput = output.track(0); + wavHeader = null; + output.endTracks(); + } + + @Override + public void seek() { + pendingBytes = 0; + } + + @Override + public void release() { + // Do nothing + } + + @Override + public int read(ExtractorInput input, PositionHolder seekPosition) + throws IOException, InterruptedException { + + if (wavHeader == null) { + wavHeader = WavHeaderReader.peek(input); + if (wavHeader == null) { + // Someone tried to read a non-WAV or unsupported WAV without sniffing first. + throw new ParserException("Error initializing WavHeader. Did you sniff first?"); + } + bytesPerFrame = wavHeader.getBytesPerFrame(); + } + + // If we haven't read in the data start and size, read and store them. + if (!wavHeader.hasDataBounds()) { + WavHeaderReader.skipToData(input, wavHeader); + + trackOutput.format( + MediaFormat.createAudioFormat( + null, + MimeTypes.AUDIO_RAW, + wavHeader.getBitrate(), + MAX_INPUT_SIZE, + wavHeader.getDurationUs(), + wavHeader.getNumChannels(), + wavHeader.getSampleRateHz(), + null, + null, + wavHeader.getEncoding())); + extractorOutput.seekMap(this); + } + + int bytesAppended = trackOutput.sampleData(input, MAX_INPUT_SIZE - pendingBytes, true); + + if (bytesAppended != RESULT_END_OF_INPUT) { + pendingBytes += bytesAppended; + } + + // Round down the pending number of bytes to the nearest frame. + int frameBytes = pendingBytes / bytesPerFrame * bytesPerFrame; + if (frameBytes > 0) { + long sampleStartPosition = input.getPosition() - pendingBytes; + pendingBytes -= frameBytes; + trackOutput.sampleMetadata( + wavHeader.getTimeUs(sampleStartPosition), + C.SAMPLE_FLAG_SYNC, + frameBytes, + pendingBytes, + null); + } + + if (bytesAppended == RESULT_END_OF_INPUT) { + return RESULT_END_OF_INPUT; + } + + return RESULT_CONTINUE; + } + + // SeekMap implementation. + + @Override + public boolean isSeekable() { + return true; + } + + @Override + public long getPosition(long timeUs) { + return wavHeader.getPosition(timeUs); + } +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/wav/WavHeader.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/wav/WavHeader.java new file mode 100755 index 000000000..8f5e15c5e --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/wav/WavHeader.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2016 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.messenger.exoplayer.extractor.wav; + +import org.telegram.messenger.exoplayer.C; + +/** Header for a WAV file. */ +/*package*/ final class WavHeader { + + /** Number of audio chanels. */ + private final int numChannels; + /** Sample rate in Hertz. */ + private final int sampleRateHz; + /** Average bytes per second for the sample data. */ + private final int averageBytesPerSecond; + /** Alignment for frames of audio data; should equal {@code numChannels * bitsPerSample / 8}. */ + private final int blockAlignment; + /** Bits per sample for the audio data. */ + private final int bitsPerSample; + /** The PCM encoding */ + private final int encoding; + + /** Offset to the start of sample data. */ + private long dataStartPosition; + /** Total size in bytes of the sample data. */ + private long dataSize; + + public WavHeader( + int numChannels, + int sampleRateHz, + int averageBytesPerSecond, + int blockAlignment, + int bitsPerSample, + int encoding) { + this.numChannels = numChannels; + this.sampleRateHz = sampleRateHz; + this.averageBytesPerSecond = averageBytesPerSecond; + this.blockAlignment = blockAlignment; + this.bitsPerSample = bitsPerSample; + this.encoding = encoding; + } + + /** Returns the duration in microseconds of this WAV. */ + public long getDurationUs() { + long numFrames = dataSize / blockAlignment; + return (numFrames * C.MICROS_PER_SECOND) / sampleRateHz; + } + + /** Returns the bytes per frame of this WAV. */ + public int getBytesPerFrame() { + return blockAlignment; + } + + /** Returns the bitrate of this WAV. */ + public int getBitrate() { + return sampleRateHz * bitsPerSample * numChannels; + } + + /** Returns the sample rate in Hertz of this WAV. */ + public int getSampleRateHz() { + return sampleRateHz; + } + + /** Returns the number of audio channels in this WAV. */ + public int getNumChannels() { + return numChannels; + } + + /** Returns the position in bytes in this WAV for the given time in microseconds. */ + public long getPosition(long timeUs) { + long unroundedPosition = (timeUs * averageBytesPerSecond) / C.MICROS_PER_SECOND; + // Round down to nearest frame. + return (unroundedPosition / blockAlignment) * blockAlignment + dataStartPosition; + } + + /** Returns the time in microseconds for the given position in bytes in this WAV. */ + public long getTimeUs(long position) { + return position * C.MICROS_PER_SECOND / averageBytesPerSecond; + } + + /** Returns true if the data start position and size have been set. */ + public boolean hasDataBounds() { + return dataStartPosition != 0 && dataSize != 0; + } + + /** Sets the start position and size in bytes of sample data in this WAV. */ + public void setDataBounds(long dataStartPosition, long dataSize) { + this.dataStartPosition = dataStartPosition; + this.dataSize = dataSize; + } + + /** Returns the PCM encoding. **/ + public int getEncoding() { + return encoding; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/wav/WavHeaderReader.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/wav/WavHeaderReader.java new file mode 100755 index 000000000..030eff809 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/wav/WavHeaderReader.java @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2016 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.messenger.exoplayer.extractor.wav; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.ParserException; +import org.telegram.messenger.exoplayer.extractor.ExtractorInput; +import org.telegram.messenger.exoplayer.util.Assertions; +import org.telegram.messenger.exoplayer.util.ParsableByteArray; +import org.telegram.messenger.exoplayer.util.Util; + +import android.util.Log; + +import java.io.IOException; + +/** Reads a {@code WavHeader} from an input stream; supports resuming from input failures. */ +/*package*/ final class WavHeaderReader { + + private static final String TAG = "WavHeaderReader"; + + /** Integer PCM audio data. */ + private static final int TYPE_PCM = 0x0001; + /** Extended WAVE format. */ + private static final int TYPE_WAVE_FORMAT_EXTENSIBLE = 0xFFFE; + + /** + * Peeks and returns a {@code WavHeader}. + * + * @param input Input stream to peek the WAV header from. + * @throws IOException If peeking from the input fails. + * @throws InterruptedException If interrupted while peeking from input. + * @throws ParserException If the input file is an incorrect RIFF WAV. + * @return A new {@code WavHeader} peeked from {@code input}, or null if the input is not a + * supported WAV format. + */ + public static WavHeader peek(ExtractorInput input) + throws IOException, InterruptedException, ParserException { + Assertions.checkNotNull(input); + + // Allocate a scratch buffer large enough to store the format chunk. + ParsableByteArray scratch = new ParsableByteArray(16); + + // Attempt to read the RIFF chunk. + ChunkHeader chunkHeader = ChunkHeader.peek(input, scratch); + if (chunkHeader.id != Util.getIntegerCodeForString("RIFF")) { + return null; + } + + input.peekFully(scratch.data, 0, 4); + scratch.setPosition(0); + int riffFormat = scratch.readInt(); + if (riffFormat != Util.getIntegerCodeForString("WAVE")) { + Log.e(TAG, "Unsupported RIFF format: " + riffFormat); + return null; + } + + // Skip chunks until we find the format chunk. + chunkHeader = ChunkHeader.peek(input, scratch); + while (chunkHeader.id != Util.getIntegerCodeForString("fmt ")) { + input.advancePeekPosition((int) chunkHeader.size); + chunkHeader = ChunkHeader.peek(input, scratch); + } + + Assertions.checkState(chunkHeader.size >= 16); + input.peekFully(scratch.data, 0, 16); + scratch.setPosition(0); + int type = scratch.readLittleEndianUnsignedShort(); + int numChannels = scratch.readLittleEndianUnsignedShort(); + int sampleRateHz = scratch.readLittleEndianUnsignedIntToInt(); + int averageBytesPerSecond = scratch.readLittleEndianUnsignedIntToInt(); + int blockAlignment = scratch.readLittleEndianUnsignedShort(); + int bitsPerSample = scratch.readLittleEndianUnsignedShort(); + + int expectedBlockAlignment = numChannels * bitsPerSample / 8; + if (blockAlignment != expectedBlockAlignment) { + throw new ParserException("Expected block alignment: " + expectedBlockAlignment + "; got: " + + blockAlignment); + } + + int encoding = Util.getPcmEncoding(bitsPerSample); + if (encoding == C.ENCODING_INVALID) { + Log.e(TAG, "Unsupported WAV bit depth: " + bitsPerSample); + return null; + } + + if (type != TYPE_PCM && type != TYPE_WAVE_FORMAT_EXTENSIBLE) { + Log.e(TAG, "Unsupported WAV format type: " + type); + return null; + } + + // If present, skip extensionSize, validBitsPerSample, channelMask, subFormatGuid, ... + input.advancePeekPosition((int) chunkHeader.size - 16); + + return new WavHeader(numChannels, sampleRateHz, averageBytesPerSecond, blockAlignment, + bitsPerSample, encoding); + } + + /** + * Skips to the data in the given WAV input stream and returns its data size. After calling, the + * input stream's position will point to the start of sample data in the WAV. + *

+ * If an exception is thrown, the input position will be left pointing to a chunk header. + * + * @param input Input stream to skip to the data chunk in. Its peek position must be pointing to + * a valid chunk header. + * @param wavHeader WAV header to populate with data bounds. + * @throws IOException If reading from the input fails. + * @throws InterruptedException If interrupted while reading from input. + * @throws ParserException If an error occurs parsing chunks. + */ + public static void skipToData(ExtractorInput input, WavHeader wavHeader) + throws IOException, InterruptedException, ParserException { + Assertions.checkNotNull(input); + Assertions.checkNotNull(wavHeader); + + // Make sure the peek position is set to the read position before we peek the first header. + input.resetPeekPosition(); + + ParsableByteArray scratch = new ParsableByteArray(ChunkHeader.SIZE_IN_BYTES); + // Skip all chunks until we hit the data header. + ChunkHeader chunkHeader = ChunkHeader.peek(input, scratch); + while (chunkHeader.id != Util.getIntegerCodeForString("data")) { + Log.w(TAG, "Ignoring unknown WAV chunk: " + chunkHeader.id); + long bytesToSkip = ChunkHeader.SIZE_IN_BYTES + chunkHeader.size; + // Override size of RIFF chunk, since it describes its size as the entire file. + if (chunkHeader.id == Util.getIntegerCodeForString("RIFF")) { + bytesToSkip = ChunkHeader.SIZE_IN_BYTES + 4; + } + if (bytesToSkip > Integer.MAX_VALUE) { + throw new ParserException("Chunk is too large (~2GB+) to skip; id: " + chunkHeader.id); + } + input.skipFully((int) bytesToSkip); + chunkHeader = ChunkHeader.peek(input, scratch); + } + // Skip past the "data" header. + input.skipFully(ChunkHeader.SIZE_IN_BYTES); + + wavHeader.setDataBounds(input.getPosition(), chunkHeader.size); + } + + /** Container for a WAV chunk header. */ + private static final class ChunkHeader { + + /** Size in bytes of a WAV chunk header. */ + public static final int SIZE_IN_BYTES = 8; + + /** 4-character identifier, stored as an integer, for this chunk. */ + public final int id; + /** Size of this chunk in bytes. */ + public final long size; + + private ChunkHeader(int id, long size) { + this.id = id; + this.size = size; + } + + /** + * Peeks and returns a {@link ChunkHeader}. + * + * @param input Input stream to peek the chunk header from. + * @param scratch Buffer for temporary use. + * @throws IOException If peeking from the input fails. + * @throws InterruptedException If interrupted while peeking from input. + * @return A new {@code ChunkHeader} peeked from {@code input}. + */ + public static ChunkHeader peek(ExtractorInput input, ParsableByteArray scratch) + throws IOException, InterruptedException { + input.peekFully(scratch.data, 0, SIZE_IN_BYTES); + scratch.setPosition(0); + + int id = scratch.readInt(); + long size = scratch.readLittleEndianUnsignedInt(); + + return new ChunkHeader(id, size); + } + } +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/webm/DefaultEbmlReader.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/webm/DefaultEbmlReader.java new file mode 100755 index 000000000..477a35607 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/webm/DefaultEbmlReader.java @@ -0,0 +1,240 @@ +/* + * 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.messenger.exoplayer.extractor.webm; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.ParserException; +import org.telegram.messenger.exoplayer.extractor.ExtractorInput; +import org.telegram.messenger.exoplayer.util.Assertions; + +import java.io.EOFException; +import java.io.IOException; +import java.util.Stack; + +/** + * Default implementation of {@link EbmlReader}. + */ +/* package */ final class DefaultEbmlReader implements EbmlReader { + + private static final int ELEMENT_STATE_READ_ID = 0; + private static final int ELEMENT_STATE_READ_CONTENT_SIZE = 1; + private static final int ELEMENT_STATE_READ_CONTENT = 2; + + private static final int MAX_ID_BYTES = 4; + private static final int MAX_LENGTH_BYTES = 8; + + private static final int MAX_INTEGER_ELEMENT_SIZE_BYTES = 8; + private static final int VALID_FLOAT32_ELEMENT_SIZE_BYTES = 4; + private static final int VALID_FLOAT64_ELEMENT_SIZE_BYTES = 8; + + private final byte[] scratch = new byte[8]; + private final Stack masterElementsStack = new Stack<>(); + private final VarintReader varintReader = new VarintReader(); + + private EbmlReaderOutput output; + private int elementState; + private int elementId; + private long elementContentSize; + + @Override + public void init(EbmlReaderOutput eventHandler) { + this.output = eventHandler; + } + + @Override + public void reset() { + elementState = ELEMENT_STATE_READ_ID; + masterElementsStack.clear(); + varintReader.reset(); + } + + @Override + public boolean read(ExtractorInput input) throws IOException, InterruptedException { + Assertions.checkState(output != null); + while (true) { + if (!masterElementsStack.isEmpty() + && input.getPosition() >= masterElementsStack.peek().elementEndPosition) { + output.endMasterElement(masterElementsStack.pop().elementId); + return true; + } + + if (elementState == ELEMENT_STATE_READ_ID) { + long result = varintReader.readUnsignedVarint(input, true, false, MAX_ID_BYTES); + if (result == C.RESULT_MAX_LENGTH_EXCEEDED) { + result = maybeResyncToNextLevel1Element(input); + } + if (result == C.RESULT_END_OF_INPUT) { + return false; + } + // Element IDs are at most 4 bytes, so we can cast to integers. + elementId = (int) result; + elementState = ELEMENT_STATE_READ_CONTENT_SIZE; + } + + if (elementState == ELEMENT_STATE_READ_CONTENT_SIZE) { + elementContentSize = varintReader.readUnsignedVarint(input, false, true, MAX_LENGTH_BYTES); + elementState = ELEMENT_STATE_READ_CONTENT; + } + + int type = output.getElementType(elementId); + switch (type) { + case TYPE_MASTER: + long elementContentPosition = input.getPosition(); + long elementEndPosition = elementContentPosition + elementContentSize; + masterElementsStack.add(new MasterElement(elementId, elementEndPosition)); + output.startMasterElement(elementId, elementContentPosition, elementContentSize); + elementState = ELEMENT_STATE_READ_ID; + return true; + case TYPE_UNSIGNED_INT: + if (elementContentSize > MAX_INTEGER_ELEMENT_SIZE_BYTES) { + throw new ParserException("Invalid integer size: " + elementContentSize); + } + output.integerElement(elementId, readInteger(input, (int) elementContentSize)); + elementState = ELEMENT_STATE_READ_ID; + return true; + case TYPE_FLOAT: + if (elementContentSize != VALID_FLOAT32_ELEMENT_SIZE_BYTES + && elementContentSize != VALID_FLOAT64_ELEMENT_SIZE_BYTES) { + throw new ParserException("Invalid float size: " + elementContentSize); + } + output.floatElement(elementId, readFloat(input, (int) elementContentSize)); + elementState = ELEMENT_STATE_READ_ID; + return true; + case TYPE_STRING: + if (elementContentSize > Integer.MAX_VALUE) { + throw new ParserException("String element size: " + elementContentSize); + } + output.stringElement(elementId, readString(input, (int) elementContentSize)); + elementState = ELEMENT_STATE_READ_ID; + return true; + case TYPE_BINARY: + output.binaryElement(elementId, (int) elementContentSize, input); + elementState = ELEMENT_STATE_READ_ID; + return true; + case TYPE_UNKNOWN: + input.skipFully((int) elementContentSize); + elementState = ELEMENT_STATE_READ_ID; + break; + default: + throw new ParserException("Invalid element type " + type); + } + } + } + + /** + * Does a byte by byte search to try and find the next level 1 element. This method is called if + * some invalid data is encountered in the parser. + * + * @param input The {@link ExtractorInput} from which data has to be read. + * @return id of the next level 1 element that has been found. + * @throws EOFException If the end of input was encountered when searching for the next level 1 + * element. + * @throws IOException If an error occurs reading from the input. + * @throws InterruptedException If the thread is interrupted. + */ + private long maybeResyncToNextLevel1Element(ExtractorInput input) throws EOFException, + IOException, InterruptedException { + input.resetPeekPosition(); + while (true) { + input.peekFully(scratch, 0, MAX_ID_BYTES); + int varintLength = VarintReader.parseUnsignedVarintLength(scratch[0]); + if (varintLength != -1 && varintLength <= MAX_ID_BYTES) { + int potentialId = (int) VarintReader.assembleVarint(scratch, varintLength, false); + if (output.isLevel1Element(potentialId)) { + input.skipFully(varintLength); + return potentialId; + } + } + input.skipFully(1); + } + } + + /** + * Reads and returns an integer of length {@code byteLength} from the {@link ExtractorInput}. + * + * @param input The {@link ExtractorInput} from which to read. + * @param byteLength The length of the integer being read. + * @return The read integer value. + * @throws IOException If an error occurs reading from the input. + * @throws InterruptedException If the thread is interrupted. + */ + private long readInteger(ExtractorInput input, int byteLength) + throws IOException, InterruptedException { + input.readFully(scratch, 0, byteLength); + long value = 0; + for (int i = 0; i < byteLength; i++) { + value = (value << 8) | (scratch[i] & 0xFF); + } + return value; + } + + /** + * Reads and returns a float of length {@code byteLength} from the {@link ExtractorInput}. + * + * @param input The {@link ExtractorInput} from which to read. + * @param byteLength The length of the float being read. + * @return The read float value. + * @throws IOException If an error occurs reading from the input. + * @throws InterruptedException If the thread is interrupted. + */ + private double readFloat(ExtractorInput input, int byteLength) + throws IOException, InterruptedException { + long integerValue = readInteger(input, byteLength); + double floatValue; + if (byteLength == VALID_FLOAT32_ELEMENT_SIZE_BYTES) { + floatValue = Float.intBitsToFloat((int) integerValue); + } else { + floatValue = Double.longBitsToDouble(integerValue); + } + return floatValue; + } + + /** + * Reads and returns a string of length {@code byteLength} from the {@link ExtractorInput}. + * + * @param input The {@link ExtractorInput} from which to read. + * @param byteLength The length of the float being read. + * @return The read string value. + * @throws IOException If an error occurs reading from the input. + * @throws InterruptedException If the thread is interrupted. + */ + private String readString(ExtractorInput input, int byteLength) + throws IOException, InterruptedException { + if (byteLength == 0) { + return ""; + } + byte[] stringBytes = new byte[byteLength]; + input.readFully(stringBytes, 0, byteLength); + return new String(stringBytes); + } + + /** + * Used in {@link #masterElementsStack} to track when the current master element ends, so that + * {@link EbmlReaderOutput#endMasterElement(int)} can be called. + */ + private static final class MasterElement { + + private final int elementId; + private final long elementEndPosition; + + private MasterElement(int elementId, long elementEndPosition) { + this.elementId = elementId; + this.elementEndPosition = elementEndPosition; + } + + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/webm/EbmlReader.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/webm/EbmlReader.java new file mode 100755 index 000000000..bf044ee6c --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/webm/EbmlReader.java @@ -0,0 +1,84 @@ +/* + * 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.messenger.exoplayer.extractor.webm; + +import org.telegram.messenger.exoplayer.ParserException; +import org.telegram.messenger.exoplayer.extractor.ExtractorInput; + +import java.io.IOException; + +/** + * Event-driven EBML reader that delivers events to an {@link EbmlReaderOutput}. + *

+ * EBML can be summarized as a binary XML format somewhat similar to Protocol Buffers. It was + * originally designed for the Matroska container format. More information about EBML and + * Matroska is available here. + */ +/* package */ interface EbmlReader { + + /** + * Type for unknown elements. + */ + public static final int TYPE_UNKNOWN = 0; + /** + * Type for elements that contain child elements. + */ + public static final int TYPE_MASTER = 1; + /** + * Type for integer value elements of up to 8 bytes. + */ + public static final int TYPE_UNSIGNED_INT = 2; + /** + * Type for string elements. + */ + public static final int TYPE_STRING = 3; + /** + * Type for binary elements. + */ + public static final int TYPE_BINARY = 4; + /** + * Type for IEEE floating point value elements of either 4 or 8 bytes. + */ + public static final int TYPE_FLOAT = 5; + + /** + * Initializes the extractor with an {@link EbmlReaderOutput}. + * + * @param output An {@link EbmlReaderOutput} to receive events. + */ + public void init(EbmlReaderOutput output); + + /** + * Resets the state of the reader. + *

+ * Subsequent calls to {@link #read(ExtractorInput)} will start reading a new EBML structure + * from scratch. + */ + public void reset(); + + /** + * Reads from an {@link ExtractorInput}, invoking an event callback if possible. + * + * @param input The {@link ExtractorInput} from which data should be read. + * @return True if data can continue to be read. False if the end of the input was encountered. + * @throws ParserException If parsing fails. + * @throws IOException If an error occurs reading from the input. + * @throws InterruptedException If the thread is interrupted. + */ + public boolean read(ExtractorInput input) throws ParserException, IOException, + InterruptedException; + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/webm/EbmlReaderOutput.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/webm/EbmlReaderOutput.java new file mode 100755 index 000000000..cd1b8b873 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/webm/EbmlReaderOutput.java @@ -0,0 +1,117 @@ +/* + * 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.messenger.exoplayer.extractor.webm; + +import org.telegram.messenger.exoplayer.ParserException; +import org.telegram.messenger.exoplayer.extractor.ExtractorInput; + +import java.io.IOException; + +/** + * Defines EBML element IDs/types and reacts to events. + */ +/* package */ interface EbmlReaderOutput { + + /** + * Maps an element ID to a corresponding type. + *

+ * If {@link EbmlReader#TYPE_UNKNOWN} is returned then the element is skipped. Note that all + * children of a skipped element are also skipped. + * + * @param id The element ID to map. + * @return One of the {@code TYPE_} constants defined in {@link EbmlReader}. + */ + int getElementType(int id); + + /** + * Checks if the given id is that of a level 1 element. + * + * @param id The element ID. + * @return True the given id is that of a level 1 element. false otherwise. + */ + boolean isLevel1Element(int id); + + /** + * Called when the start of a master element is encountered. + *

+ * Following events should be considered as taking place within this element until a matching call + * to {@link #endMasterElement(int)} is made. + *

+ * Note that it is possible for another master element of the same element ID to be nested within + * itself. + * + * @param id The element ID. + * @param contentPosition The position of the start of the element's content in the stream. + * @param contentSize The size of the element's content in bytes. + * @throws ParserException If a parsing error occurs. + */ + void startMasterElement(int id, long contentPosition, long contentSize) throws ParserException; + + /** + * Called when the end of a master element is encountered. + * + * @param id The element ID. + * @throws ParserException If a parsing error occurs. + */ + void endMasterElement(int id) throws ParserException; + + /** + * Called when an integer element is encountered. + * + * @param id The element ID. + * @param value The integer value that the element contains. + * @throws ParserException If a parsing error occurs. + */ + void integerElement(int id, long value) throws ParserException; + + /** + * Called when a float element is encountered. + * + * @param id The element ID. + * @param value The float value that the element contains + * @throws ParserException If a parsing error occurs. + */ + void floatElement(int id, double value) throws ParserException; + + /** + * Called when a string element is encountered. + * + * @param id The element ID. + * @param value The string value that the element contains. + * @throws ParserException If a parsing error occurs. + */ + void stringElement(int id, String value) throws ParserException; + + /** + * Called when a binary element is encountered. + *

+ * The element header (containing the element ID and content size) will already have been read. + * Implementations are required to consume the whole remainder of the element, which is + * {@code contentSize} bytes in length, before returning. Implementations are permitted to fail + * (by throwing an exception) having partially consumed the data, however if they do this, they + * must consume the remainder of the content when invoked again. + * + * @param id The element ID. + * @param contentsSize The element's content size. + * @param input The {@link ExtractorInput} from which data should be read. + * @throws ParserException If a parsing error occurs. + * @throws IOException If an error occurs reading from the input. + * @throws InterruptedException If the thread is interrupted. + */ + void binaryElement(int id, int contentsSize, ExtractorInput input) + throws ParserException, IOException, InterruptedException; + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/webm/Sniffer.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/webm/Sniffer.java new file mode 100755 index 000000000..00391528e --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/webm/Sniffer.java @@ -0,0 +1,115 @@ +/* + * 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.messenger.exoplayer.extractor.webm; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.extractor.Extractor; +import org.telegram.messenger.exoplayer.extractor.ExtractorInput; +import org.telegram.messenger.exoplayer.util.ParsableByteArray; + +import java.io.IOException; + +/** + * Utility class that peeks from the input stream in order to determine whether it appears to be + * compatible input for this extractor. + */ +/* package */ final class Sniffer { + + /** + * The number of bytes to search for a valid header in {@link #sniff(ExtractorInput)}. + */ + private static final int SEARCH_LENGTH = 1024; + private static final int ID_EBML = 0x1A45DFA3; + + private final ParsableByteArray scratch; + private int peekLength; + + public Sniffer() { + scratch = new ParsableByteArray(8); + } + + /** + * @see Extractor#sniff + */ + public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + long inputLength = input.getLength(); + int bytesToSearch = (int) (inputLength == C.LENGTH_UNBOUNDED || inputLength > SEARCH_LENGTH + ? SEARCH_LENGTH : inputLength); + // Find four bytes equal to ID_EBML near the start of the input. + input.peekFully(scratch.data, 0, 4); + long tag = scratch.readUnsignedInt(); + peekLength = 4; + while (tag != ID_EBML) { + if (++peekLength == bytesToSearch) { + return false; + } + input.peekFully(scratch.data, 0, 1); + tag = (tag << 8) & 0xFFFFFF00; + tag |= scratch.data[0] & 0xFF; + } + + // Read the size of the EBML header and make sure it is within the stream. + long headerSize = readUint(input); + long headerStart = peekLength; + if (headerSize == Long.MIN_VALUE + || (inputLength != C.LENGTH_UNBOUNDED && headerStart + headerSize >= inputLength)) { + return false; + } + + // Read the payload elements in the EBML header. + while (peekLength < headerStart + headerSize) { + long id = readUint(input); + if (id == Long.MIN_VALUE) { + return false; + } + long size = readUint(input); + if (size < 0 || size > Integer.MAX_VALUE) { + return false; + } + if (size != 0) { + input.advancePeekPosition((int) size); + peekLength += size; + } + } + return peekLength == headerStart + headerSize; + } + + /** + * Peeks a variable-length unsigned EBML integer from the input. + */ + private long readUint(ExtractorInput input) throws IOException, InterruptedException { + input.peekFully(scratch.data, 0, 1); + int value = scratch.data[0] & 0xFF; + if (value == 0) { + return Long.MIN_VALUE; + } + int mask = 0x80; + int length = 0; + while ((value & mask) == 0) { + mask >>= 1; + length++; + } + value &= ~mask; + input.peekFully(scratch.data, 1, length); + for (int i = 0; i < length; i++) { + value <<= 8; + value += scratch.data[i + 1] & 0xFF; + } + peekLength += length + 1; + return value; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/webm/VarintReader.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/webm/VarintReader.java new file mode 100755 index 000000000..ec24c9770 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/webm/VarintReader.java @@ -0,0 +1,140 @@ +package org.telegram.messenger.exoplayer.extractor.webm; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.extractor.ExtractorInput; + +import java.io.EOFException; +import java.io.IOException; + +/** + * Reads EBML variable-length integers (varints) from an {@link ExtractorInput}. + */ +/* package */ final class VarintReader { + + private static final int STATE_BEGIN_READING = 0; + private static final int STATE_READ_CONTENTS = 1; + + /** + * The first byte of a variable-length integer (varint) will have one of these bit masks + * indicating the total length in bytes. + * + *

{@code 0x80} is a one-byte integer, {@code 0x40} is two bytes, and so on up to eight bytes. + */ + private static final long[] VARINT_LENGTH_MASKS = new long[] { + 0x80L, 0x40L, 0x20L, 0x10L, 0x08L, 0x04L, 0x02L, 0x01L + }; + + private final byte[] scratch; + + private int state; + private int length; + + public VarintReader() { + scratch = new byte[8]; + } + + /** + * Resets the reader to start reading a new variable-length integer. + */ + public void reset() { + state = STATE_BEGIN_READING; + length = 0; + } + + /** + * Reads an EBML variable-length integer (varint) from an {@link ExtractorInput} such that + * reading can be resumed later if an error occurs having read only some of it. + *

+ * If an value is successfully read, then the reader will automatically reset itself ready to + * read another value. + *

+ * If an {@link IOException} or {@link InterruptedException} is throw, the read can be resumed + * later by calling this method again, passing an {@link ExtractorInput} providing data starting + * where the previous one left off. + * + * @param input The {@link ExtractorInput} from which the integer should be read. + * @param allowEndOfInput True if encountering the end of the input having read no data is + * allowed, and should result in {@link C#RESULT_END_OF_INPUT} being returned. False if it + * should be considered an error, causing an {@link EOFException} to be thrown. + * @param removeLengthMask Removes the variable-length integer length mask from the value. + * @param maximumAllowedLength Maximum allowed length of the variable integer to be read. + * @return The read value, or {@link C#RESULT_END_OF_INPUT} if {@code allowEndOfStream} is true + * and the end of the input was encountered, or {@link C#RESULT_MAX_LENGTH_EXCEEDED} if the + * length of the varint exceeded maximumAllowedLength. + * @throws IOException If an error occurs reading from the input. + * @throws InterruptedException If the thread is interrupted. + */ + public long readUnsignedVarint(ExtractorInput input, boolean allowEndOfInput, + boolean removeLengthMask, int maximumAllowedLength) throws IOException, InterruptedException { + if (state == STATE_BEGIN_READING) { + // Read the first byte to establish the length. + if (!input.readFully(scratch, 0, 1, allowEndOfInput)) { + return C.RESULT_END_OF_INPUT; + } + int firstByte = scratch[0] & 0xFF; + length = parseUnsignedVarintLength(firstByte); + if (length == -1) { + throw new IllegalStateException("No valid varint length mask found"); + } + state = STATE_READ_CONTENTS; + } + + if (length > maximumAllowedLength) { + state = STATE_BEGIN_READING; + return C.RESULT_MAX_LENGTH_EXCEEDED; + } + + if (length != 1) { + // Read the remaining bytes. + input.readFully(scratch, 1, length - 1); + } + + state = STATE_BEGIN_READING; + return assembleVarint(scratch, length, removeLengthMask); + } + + /** + * Returns the number of bytes occupied by the most recently parsed varint. + */ + public int getLastLength() { + return length; + } + + /** + * Parses and the length of the varint given the first byte. + * + * @param firstByte First byte of the varint. + * @return Length of the varint beginning with the given byte if it was valid, -1 otherwise. + */ + public static int parseUnsignedVarintLength(int firstByte) { + int varIntLength = -1; + for (int i = 0; i < VARINT_LENGTH_MASKS.length; i++) { + if ((VARINT_LENGTH_MASKS[i] & firstByte) != 0) { + varIntLength = i + 1; + break; + } + } + return varIntLength; + } + + /** + * Assemble a varint from the given byte array. + * + * @param varintBytes Bytes that make up the varint. + * @param varintLength Length of the varint to assemble. + * @param removeLengthMask Removes the variable-length integer length mask from the value. + * @return Parsed and assembled varint. + */ + public static long assembleVarint(byte[] varintBytes, int varintLength, + boolean removeLengthMask) { + long varint = varintBytes[0] & 0xFFL; + if (removeLengthMask) { + varint &= ~VARINT_LENGTH_MASKS[varintLength - 1]; + } + for (int i = 1; i < varintLength; i++) { + varint = (varint << 8) | (varintBytes[i] & 0xFFL); + } + return varint; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/webm/WebmExtractor.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/webm/WebmExtractor.java new file mode 100755 index 000000000..da6495850 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/extractor/webm/WebmExtractor.java @@ -0,0 +1,1560 @@ +/* + * 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.messenger.exoplayer.extractor.webm; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.MediaFormat; +import org.telegram.messenger.exoplayer.ParserException; +import org.telegram.messenger.exoplayer.drm.DrmInitData; +import org.telegram.messenger.exoplayer.drm.DrmInitData.SchemeInitData; +import org.telegram.messenger.exoplayer.extractor.ChunkIndex; +import org.telegram.messenger.exoplayer.extractor.Extractor; +import org.telegram.messenger.exoplayer.extractor.ExtractorInput; +import org.telegram.messenger.exoplayer.extractor.ExtractorOutput; +import org.telegram.messenger.exoplayer.extractor.PositionHolder; +import org.telegram.messenger.exoplayer.extractor.SeekMap; +import org.telegram.messenger.exoplayer.extractor.TrackOutput; +import org.telegram.messenger.exoplayer.util.LongArray; +import org.telegram.messenger.exoplayer.util.MimeTypes; +import org.telegram.messenger.exoplayer.util.NalUnitUtil; +import org.telegram.messenger.exoplayer.util.ParsableByteArray; +import org.telegram.messenger.exoplayer.util.Util; + +import android.util.Pair; +import android.util.SparseArray; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.UUID; + +/** + * An extractor to facilitate data retrieval from the WebM container format. + *

+ * WebM is a subset of the EBML elements defined for Matroska. More information about EBML and + * Matroska is available here. + * More info about WebM is here. + * RFC on encrypted WebM can be found + * here. + */ +public final class WebmExtractor implements Extractor { + + private static final int BLOCK_STATE_START = 0; + private static final int BLOCK_STATE_HEADER = 1; + private static final int BLOCK_STATE_DATA = 2; + + private static final String DOC_TYPE_WEBM = "webm"; + private static final String DOC_TYPE_MATROSKA = "matroska"; + private static final String CODEC_ID_VP8 = "V_VP8"; + private static final String CODEC_ID_VP9 = "V_VP9"; + private static final String CODEC_ID_MPEG2 = "V_MPEG2"; + private static final String CODEC_ID_MPEG4_SP = "V_MPEG4/ISO/SP"; + private static final String CODEC_ID_MPEG4_ASP = "V_MPEG4/ISO/ASP"; + private static final String CODEC_ID_MPEG4_AP = "V_MPEG4/ISO/AP"; + private static final String CODEC_ID_H264 = "V_MPEG4/ISO/AVC"; + private static final String CODEC_ID_H265 = "V_MPEGH/ISO/HEVC"; + private static final String CODEC_ID_FOURCC = "V_MS/VFW/FOURCC"; + private static final String CODEC_ID_VORBIS = "A_VORBIS"; + private static final String CODEC_ID_OPUS = "A_OPUS"; + private static final String CODEC_ID_AAC = "A_AAC"; + private static final String CODEC_ID_MP3 = "A_MPEG/L3"; + private static final String CODEC_ID_AC3 = "A_AC3"; + private static final String CODEC_ID_E_AC3 = "A_EAC3"; + private static final String CODEC_ID_TRUEHD = "A_TRUEHD"; + private static final String CODEC_ID_DTS = "A_DTS"; + private static final String CODEC_ID_DTS_EXPRESS = "A_DTS/EXPRESS"; + private static final String CODEC_ID_DTS_LOSSLESS = "A_DTS/LOSSLESS"; + private static final String CODEC_ID_FLAC = "A_FLAC"; + private static final String CODEC_ID_ACM = "A_MS/ACM"; + private static final String CODEC_ID_PCM_INT_LIT = "A_PCM/INT/LIT"; + private static final String CODEC_ID_SUBRIP = "S_TEXT/UTF8"; + private static final String CODEC_ID_VOBSUB = "S_VOBSUB"; + private static final String CODEC_ID_PGS = "S_HDMV/PGS"; + + private static final int VORBIS_MAX_INPUT_SIZE = 8192; + private static final int OPUS_MAX_INPUT_SIZE = 5760; + private static final int MP3_MAX_INPUT_SIZE = 4096; + private static final int ENCRYPTION_IV_SIZE = 8; + private static final int TRACK_TYPE_AUDIO = 2; + private static final int UNKNOWN = -1; + + private static final int ID_EBML = 0x1A45DFA3; + private static final int ID_EBML_READ_VERSION = 0x42F7; + private static final int ID_DOC_TYPE = 0x4282; + private static final int ID_DOC_TYPE_READ_VERSION = 0x4285; + private static final int ID_SEGMENT = 0x18538067; + private static final int ID_SEGMENT_INFO = 0x1549A966; + private static final int ID_SEEK_HEAD = 0x114D9B74; + private static final int ID_SEEK = 0x4DBB; + private static final int ID_SEEK_ID = 0x53AB; + private static final int ID_SEEK_POSITION = 0x53AC; + private static final int ID_INFO = 0x1549A966; + private static final int ID_TIMECODE_SCALE = 0x2AD7B1; + private static final int ID_DURATION = 0x4489; + private static final int ID_CLUSTER = 0x1F43B675; + private static final int ID_TIME_CODE = 0xE7; + private static final int ID_SIMPLE_BLOCK = 0xA3; + private static final int ID_BLOCK_GROUP = 0xA0; + private static final int ID_BLOCK = 0xA1; + private static final int ID_BLOCK_DURATION = 0x9B; + private static final int ID_REFERENCE_BLOCK = 0xFB; + private static final int ID_TRACKS = 0x1654AE6B; + private static final int ID_TRACK_ENTRY = 0xAE; + private static final int ID_TRACK_NUMBER = 0xD7; + private static final int ID_TRACK_TYPE = 0x83; + private static final int ID_DEFAULT_DURATION = 0x23E383; + private static final int ID_CODEC_ID = 0x86; + private static final int ID_CODEC_PRIVATE = 0x63A2; + private static final int ID_CODEC_DELAY = 0x56AA; + private static final int ID_SEEK_PRE_ROLL = 0x56BB; + private static final int ID_VIDEO = 0xE0; + private static final int ID_PIXEL_WIDTH = 0xB0; + private static final int ID_PIXEL_HEIGHT = 0xBA; + private static final int ID_DISPLAY_WIDTH = 0x54B0; + private static final int ID_DISPLAY_HEIGHT = 0x54BA; + private static final int ID_DISPLAY_UNIT = 0x54B2; + private static final int ID_AUDIO = 0xE1; + private static final int ID_CHANNELS = 0x9F; + private static final int ID_AUDIO_BIT_DEPTH = 0x6264; + private static final int ID_SAMPLING_FREQUENCY = 0xB5; + private static final int ID_CONTENT_ENCODINGS = 0x6D80; + private static final int ID_CONTENT_ENCODING = 0x6240; + private static final int ID_CONTENT_ENCODING_ORDER = 0x5031; + private static final int ID_CONTENT_ENCODING_SCOPE = 0x5032; + private static final int ID_CONTENT_COMPRESSION = 0x5034; + private static final int ID_CONTENT_COMPRESSION_ALGORITHM = 0x4254; + private static final int ID_CONTENT_COMPRESSION_SETTINGS = 0x4255; + private static final int ID_CONTENT_ENCRYPTION = 0x5035; + private static final int ID_CONTENT_ENCRYPTION_ALGORITHM = 0x47E1; + private static final int ID_CONTENT_ENCRYPTION_KEY_ID = 0x47E2; + private static final int ID_CONTENT_ENCRYPTION_AES_SETTINGS = 0x47E7; + private static final int ID_CONTENT_ENCRYPTION_AES_SETTINGS_CIPHER_MODE = 0x47E8; + private static final int ID_CUES = 0x1C53BB6B; + private static final int ID_CUE_POINT = 0xBB; + private static final int ID_CUE_TIME = 0xB3; + private static final int ID_CUE_TRACK_POSITIONS = 0xB7; + private static final int ID_CUE_CLUSTER_POSITION = 0xF1; + private static final int ID_LANGUAGE = 0x22B59C; + + private static final int LACING_NONE = 0; + private static final int LACING_XIPH = 1; + private static final int LACING_FIXED_SIZE = 2; + private static final int LACING_EBML = 3; + + private static final int FOURCC_COMPRESSION_VC1 = 0x31435657; + + /** + * A template for the prefix that must be added to each subrip sample. The 12 byte end timecode + * starting at {@link #SUBRIP_PREFIX_END_TIMECODE_OFFSET} is set to a dummy value, and must be + * replaced with the duration of the subtitle. + *

+ * Equivalent to the UTF-8 string: "1\n00:00:00,000 --> 00:00:00,000\n". + */ + private static final byte[] SUBRIP_PREFIX = new byte[] {49, 10, 48, 48, 58, 48, 48, 58, 48, 48, + 44, 48, 48, 48, 32, 45, 45, 62, 32, 48, 48, 58, 48, 48, 58, 48, 48, 44, 48, 48, 48, 10}; + /** + * A special end timecode indicating that a subtitle should be displayed until the next subtitle, + * or until the end of the media in the case of the last subtitle. + *

+ * Equivalent to the UTF-8 string: " ". + */ + private static final byte[] SUBRIP_TIMECODE_EMPTY = + new byte[] {32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32}; + /** + * The byte offset of the end timecode in {@link #SUBRIP_PREFIX}. + */ + private static final int SUBRIP_PREFIX_END_TIMECODE_OFFSET = 19; + /** + * The length in bytes of a timecode in a subrip prefix. + */ + private static final int SUBRIP_TIMECODE_LENGTH = 12; + + /** + * The length in bytes of a WAVEFORMATEX structure. + */ + private static final int WAVE_FORMAT_SIZE = 18; + /** + * Format tag indicating a WAVEFORMATEXTENSIBLE structure. + */ + private static final int WAVE_FORMAT_EXTENSIBLE = 0xFFFE; + /** + * Format tag for PCM. + */ + private static final int WAVE_FORMAT_PCM = 1; + /** + * Sub format for PCM. + */ + private static final UUID WAVE_SUBFORMAT_PCM = new UUID(0x0100000000001000L, 0x800000AA00389B71L); + + private final EbmlReader reader; + private final VarintReader varintReader; + private final SparseArray tracks; + + // Temporary arrays. + private final ParsableByteArray nalStartCode; + private final ParsableByteArray nalLength; + private final ParsableByteArray scratch; + private final ParsableByteArray vorbisNumPageSamples; + private final ParsableByteArray seekEntryIdBytes; + private final ParsableByteArray sampleStrippedBytes; + private final ParsableByteArray subripSample; + + private long segmentContentPosition = UNKNOWN; + private long segmentContentSize = UNKNOWN; + private long timecodeScale = C.UNKNOWN_TIME_US; + private long durationTimecode = C.UNKNOWN_TIME_US; + private long durationUs = C.UNKNOWN_TIME_US; + + // The track corresponding to the current TrackEntry element, or null. + private Track currentTrack; + + // Whether drm init data has been sent to the output. + private boolean sentDrmInitData; + private boolean sentSeekMap; + + // Master seek entry related elements. + private int seekEntryId; + private long seekEntryPosition; + + // Cue related elements. + private boolean seekForCues; + private long cuesContentPosition = UNKNOWN; + private long seekPositionAfterBuildingCues = UNKNOWN; + private long clusterTimecodeUs = UNKNOWN; + private LongArray cueTimesUs; + private LongArray cueClusterPositions; + private boolean seenClusterPositionForCurrentCuePoint; + + // Block reading state. + private int blockState; + private long blockTimeUs; + private long blockDurationUs; + private int blockLacingSampleIndex; + private int blockLacingSampleCount; + private int[] blockLacingSampleSizes; + private int blockTrackNumber; + private int blockTrackNumberLength; + private int blockFlags; + + // Sample reading state. + private int sampleBytesRead; + private boolean sampleEncodingHandled; + private int sampleCurrentNalBytesRemaining; + private int sampleBytesWritten; + private boolean sampleRead; + private boolean sampleSeenReferenceBlock; + + // Extractor outputs. + private ExtractorOutput extractorOutput; + + public WebmExtractor() { + this(new DefaultEbmlReader()); + } + + /* package */ WebmExtractor(EbmlReader reader) { + this.reader = reader; + this.reader.init(new InnerEbmlReaderOutput()); + varintReader = new VarintReader(); + tracks = new SparseArray<>(); + scratch = new ParsableByteArray(4); + vorbisNumPageSamples = new ParsableByteArray(ByteBuffer.allocate(4).putInt(-1).array()); + seekEntryIdBytes = new ParsableByteArray(4); + nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE); + nalLength = new ParsableByteArray(4); + sampleStrippedBytes = new ParsableByteArray(); + subripSample = new ParsableByteArray(); + } + + @Override + public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + return new Sniffer().sniff(input); + } + + @Override + public void init(ExtractorOutput output) { + extractorOutput = output; + } + + @Override + public void seek() { + clusterTimecodeUs = UNKNOWN; + blockState = BLOCK_STATE_START; + reader.reset(); + varintReader.reset(); + resetSample(); + } + + @Override + public void release() { + // Do nothing + } + + @Override + public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException, + InterruptedException { + sampleRead = false; + boolean continueReading = true; + while (continueReading && !sampleRead) { + continueReading = reader.read(input); + if (continueReading && maybeSeekForCues(seekPosition, input.getPosition())) { + return Extractor.RESULT_SEEK; + } + } + return continueReading ? Extractor.RESULT_CONTINUE : Extractor.RESULT_END_OF_INPUT; + } + + /* package */ int getElementType(int id) { + switch (id) { + case ID_EBML: + case ID_SEGMENT: + case ID_SEEK_HEAD: + case ID_SEEK: + case ID_INFO: + case ID_CLUSTER: + case ID_TRACKS: + case ID_TRACK_ENTRY: + case ID_AUDIO: + case ID_VIDEO: + case ID_CONTENT_ENCODINGS: + case ID_CONTENT_ENCODING: + case ID_CONTENT_COMPRESSION: + case ID_CONTENT_ENCRYPTION: + case ID_CONTENT_ENCRYPTION_AES_SETTINGS: + case ID_CUES: + case ID_CUE_POINT: + case ID_CUE_TRACK_POSITIONS: + case ID_BLOCK_GROUP: + return EbmlReader.TYPE_MASTER; + case ID_EBML_READ_VERSION: + case ID_DOC_TYPE_READ_VERSION: + case ID_SEEK_POSITION: + case ID_TIMECODE_SCALE: + case ID_TIME_CODE: + case ID_BLOCK_DURATION: + case ID_PIXEL_WIDTH: + case ID_PIXEL_HEIGHT: + case ID_DISPLAY_WIDTH: + case ID_DISPLAY_HEIGHT: + case ID_DISPLAY_UNIT: + case ID_TRACK_NUMBER: + case ID_TRACK_TYPE: + case ID_DEFAULT_DURATION: + case ID_CODEC_DELAY: + case ID_SEEK_PRE_ROLL: + case ID_CHANNELS: + case ID_AUDIO_BIT_DEPTH: + case ID_CONTENT_ENCODING_ORDER: + case ID_CONTENT_ENCODING_SCOPE: + case ID_CONTENT_COMPRESSION_ALGORITHM: + case ID_CONTENT_ENCRYPTION_ALGORITHM: + case ID_CONTENT_ENCRYPTION_AES_SETTINGS_CIPHER_MODE: + case ID_CUE_TIME: + case ID_CUE_CLUSTER_POSITION: + case ID_REFERENCE_BLOCK: + return EbmlReader.TYPE_UNSIGNED_INT; + case ID_DOC_TYPE: + case ID_CODEC_ID: + case ID_LANGUAGE: + return EbmlReader.TYPE_STRING; + case ID_SEEK_ID: + case ID_CONTENT_COMPRESSION_SETTINGS: + case ID_CONTENT_ENCRYPTION_KEY_ID: + case ID_SIMPLE_BLOCK: + case ID_BLOCK: + case ID_CODEC_PRIVATE: + return EbmlReader.TYPE_BINARY; + case ID_DURATION: + case ID_SAMPLING_FREQUENCY: + return EbmlReader.TYPE_FLOAT; + default: + return EbmlReader.TYPE_UNKNOWN; + } + } + + /* package */ boolean isLevel1Element(int id) { + return id == ID_SEGMENT_INFO || id == ID_CLUSTER || id == ID_CUES || id == ID_TRACKS; + } + + /* package */ void startMasterElement(int id, long contentPosition, long contentSize) + throws ParserException { + switch (id) { + case ID_SEGMENT: + if (segmentContentPosition != UNKNOWN && segmentContentPosition != contentPosition) { + throw new ParserException("Multiple Segment elements not supported"); + } + segmentContentPosition = contentPosition; + segmentContentSize = contentSize; + return; + case ID_SEEK: + seekEntryId = UNKNOWN; + seekEntryPosition = UNKNOWN; + return; + case ID_CUES: + cueTimesUs = new LongArray(); + cueClusterPositions = new LongArray(); + return; + case ID_CUE_POINT: + seenClusterPositionForCurrentCuePoint = false; + return; + case ID_CLUSTER: + if (!sentSeekMap) { + // We need to build cues before parsing the cluster. + if (cuesContentPosition != UNKNOWN) { + // We know where the Cues element is located. Seek to request it. + seekForCues = true; + } else { + // We don't know where the Cues element is located. It's most likely omitted. Allow + // playback, but disable seeking. + extractorOutput.seekMap(SeekMap.UNSEEKABLE); + sentSeekMap = true; + } + } + return; + case ID_BLOCK_GROUP: + sampleSeenReferenceBlock = false; + return; + case ID_CONTENT_ENCODING: + // TODO: check and fail if more than one content encoding is present. + return; + case ID_CONTENT_ENCRYPTION: + currentTrack.hasContentEncryption = true; + return; + case ID_TRACK_ENTRY: + currentTrack = new Track(); + return; + default: + return; + } + } + + /* package */ void endMasterElement(int id) throws ParserException { + switch (id) { + case ID_SEGMENT_INFO: + if (timecodeScale == C.UNKNOWN_TIME_US) { + // timecodeScale was omitted. Use the default value. + timecodeScale = 1000000; + } + if (durationTimecode != C.UNKNOWN_TIME_US) { + durationUs = scaleTimecodeToUs(durationTimecode); + } + return; + case ID_SEEK: + if (seekEntryId == UNKNOWN || seekEntryPosition == UNKNOWN) { + throw new ParserException("Mandatory element SeekID or SeekPosition not found"); + } + if (seekEntryId == ID_CUES) { + cuesContentPosition = seekEntryPosition; + } + return; + case ID_CUES: + if (!sentSeekMap) { + extractorOutput.seekMap(buildSeekMap()); + sentSeekMap = true; + } else { + // We have already built the cues. Ignore. + } + return; + case ID_BLOCK_GROUP: + if (blockState != BLOCK_STATE_DATA) { + // We've skipped this block (due to incompatible track number). + return; + } + // If the ReferenceBlock element was not found for this sample, then it is a keyframe. + if (!sampleSeenReferenceBlock) { + blockFlags |= C.SAMPLE_FLAG_SYNC; + } + commitSampleToOutput(tracks.get(blockTrackNumber), blockTimeUs); + blockState = BLOCK_STATE_START; + return; + case ID_CONTENT_ENCODING: + if (currentTrack.hasContentEncryption) { + if (currentTrack.encryptionKeyId == null) { + throw new ParserException("Encrypted Track found but ContentEncKeyID was not found"); + } + if (!sentDrmInitData) { + extractorOutput.drmInitData(new DrmInitData.Universal( + new SchemeInitData(MimeTypes.VIDEO_WEBM, currentTrack.encryptionKeyId))); + sentDrmInitData = true; + } + } + return; + case ID_CONTENT_ENCODINGS: + if (currentTrack.hasContentEncryption && currentTrack.sampleStrippedBytes != null) { + throw new ParserException("Combining encryption and compression is not supported"); + } + return; + case ID_TRACK_ENTRY: + if (tracks.get(currentTrack.number) == null && isCodecSupported(currentTrack.codecId)) { + currentTrack.initializeOutput(extractorOutput, currentTrack.number, durationUs); + tracks.put(currentTrack.number, currentTrack); + } else { + // We've seen this track entry before, or the codec is unsupported. Do nothing. + } + currentTrack = null; + return; + case ID_TRACKS: + if (tracks.size() == 0) { + throw new ParserException("No valid tracks were found"); + } + extractorOutput.endTracks(); + return; + default: + return; + } + } + + /* package */ void integerElement(int id, long value) throws ParserException { + switch (id) { + case ID_EBML_READ_VERSION: + // Validate that EBMLReadVersion is supported. This extractor only supports v1. + if (value != 1) { + throw new ParserException("EBMLReadVersion " + value + " not supported"); + } + return; + case ID_DOC_TYPE_READ_VERSION: + // Validate that DocTypeReadVersion is supported. This extractor only supports up to v2. + if (value < 1 || value > 2) { + throw new ParserException("DocTypeReadVersion " + value + " not supported"); + } + return; + case ID_SEEK_POSITION: + // Seek Position is the relative offset beginning from the Segment. So to get absolute + // offset from the beginning of the file, we need to add segmentContentPosition to it. + seekEntryPosition = value + segmentContentPosition; + return; + case ID_TIMECODE_SCALE: + timecodeScale = value; + return; + case ID_PIXEL_WIDTH: + currentTrack.width = (int) value; + return; + case ID_PIXEL_HEIGHT: + currentTrack.height = (int) value; + return; + case ID_DISPLAY_WIDTH: + currentTrack.displayWidth = (int) value; + return; + case ID_DISPLAY_HEIGHT: + currentTrack.displayHeight = (int) value; + return; + case ID_DISPLAY_UNIT: + currentTrack.displayUnit = (int) value; + return; + case ID_TRACK_NUMBER: + currentTrack.number = (int) value; + return; + case ID_TRACK_TYPE: + currentTrack.type = (int) value; + return; + case ID_DEFAULT_DURATION: + currentTrack.defaultSampleDurationNs = (int) value; + return; + case ID_CODEC_DELAY: + currentTrack.codecDelayNs = value; + return; + case ID_SEEK_PRE_ROLL: + currentTrack.seekPreRollNs = value; + return; + case ID_CHANNELS: + currentTrack.channelCount = (int) value; + return; + case ID_AUDIO_BIT_DEPTH: + currentTrack.audioBitDepth = (int) value; + return; + case ID_REFERENCE_BLOCK: + sampleSeenReferenceBlock = true; + return; + case ID_CONTENT_ENCODING_ORDER: + // This extractor only supports one ContentEncoding element and hence the order has to be 0. + if (value != 0) { + throw new ParserException("ContentEncodingOrder " + value + " not supported"); + } + return; + case ID_CONTENT_ENCODING_SCOPE: + // This extractor only supports the scope of all frames. + if (value != 1) { + throw new ParserException("ContentEncodingScope " + value + " not supported"); + } + return; + case ID_CONTENT_COMPRESSION_ALGORITHM: + // This extractor only supports header stripping. + if (value != 3) { + throw new ParserException("ContentCompAlgo " + value + " not supported"); + } + return; + case ID_CONTENT_ENCRYPTION_ALGORITHM: + // Only the value 5 (AES) is allowed according to the WebM specification. + if (value != 5) { + throw new ParserException("ContentEncAlgo " + value + " not supported"); + } + return; + case ID_CONTENT_ENCRYPTION_AES_SETTINGS_CIPHER_MODE: + // Only the value 1 is allowed according to the WebM specification. + if (value != 1) { + throw new ParserException("AESSettingsCipherMode " + value + " not supported"); + } + return; + case ID_CUE_TIME: + cueTimesUs.add(scaleTimecodeToUs(value)); + return; + case ID_CUE_CLUSTER_POSITION: + if (!seenClusterPositionForCurrentCuePoint) { + // If there's more than one video/audio track, then there could be more than one + // CueTrackPositions within a single CuePoint. In such a case, ignore all but the first + // one (since the cluster position will be quite close for all the tracks). + cueClusterPositions.add(value); + seenClusterPositionForCurrentCuePoint = true; + } + return; + case ID_TIME_CODE: + clusterTimecodeUs = scaleTimecodeToUs(value); + return; + case ID_BLOCK_DURATION: + blockDurationUs = scaleTimecodeToUs(value); + return; + default: + return; + } + } + + /* package */ void floatElement(int id, double value) { + switch (id) { + case ID_DURATION: + durationTimecode = (long) value; + return; + case ID_SAMPLING_FREQUENCY: + currentTrack.sampleRate = (int) value; + return; + default: + return; + } + } + + /* package */ void stringElement(int id, String value) throws ParserException { + switch (id) { + case ID_DOC_TYPE: + // Validate that DocType is supported. + if (!DOC_TYPE_WEBM.equals(value) && !DOC_TYPE_MATROSKA.equals(value)) { + throw new ParserException("DocType " + value + " not supported"); + } + return; + case ID_CODEC_ID: + currentTrack.codecId = value; + return; + case ID_LANGUAGE: + currentTrack.language = value; + return; + default: + return; + } + } + + /* package */ void binaryElement(int id, int contentSize, ExtractorInput input) + throws IOException, InterruptedException { + switch (id) { + case ID_SEEK_ID: + Arrays.fill(seekEntryIdBytes.data, (byte) 0); + input.readFully(seekEntryIdBytes.data, 4 - contentSize, contentSize); + seekEntryIdBytes.setPosition(0); + seekEntryId = (int) seekEntryIdBytes.readUnsignedInt(); + return; + case ID_CODEC_PRIVATE: + currentTrack.codecPrivate = new byte[contentSize]; + input.readFully(currentTrack.codecPrivate, 0, contentSize); + return; + case ID_CONTENT_COMPRESSION_SETTINGS: + // This extractor only supports header stripping, so the payload is the stripped bytes. + currentTrack.sampleStrippedBytes = new byte[contentSize]; + input.readFully(currentTrack.sampleStrippedBytes, 0, contentSize); + return; + case ID_CONTENT_ENCRYPTION_KEY_ID: + currentTrack.encryptionKeyId = new byte[contentSize]; + input.readFully(currentTrack.encryptionKeyId, 0, contentSize); + return; + case ID_SIMPLE_BLOCK: + case ID_BLOCK: + // Please refer to http://www.matroska.org/technical/specs/index.html#simpleblock_structure + // and http://matroska.org/technical/specs/index.html#block_structure + // for info about how data is organized in SimpleBlock and Block elements respectively. They + // differ only in the way flags are specified. + + if (blockState == BLOCK_STATE_START) { + blockTrackNumber = (int) varintReader.readUnsignedVarint(input, false, true, 8); + blockTrackNumberLength = varintReader.getLastLength(); + blockDurationUs = UNKNOWN; + blockState = BLOCK_STATE_HEADER; + scratch.reset(); + } + + Track track = tracks.get(blockTrackNumber); + + // Ignore the block if we don't know about the track to which it belongs. + if (track == null) { + input.skipFully(contentSize - blockTrackNumberLength); + blockState = BLOCK_STATE_START; + return; + } + + if (blockState == BLOCK_STATE_HEADER) { + // Read the relative timecode (2 bytes) and flags (1 byte). + readScratch(input, 3); + int lacing = (scratch.data[2] & 0x06) >> 1; + if (lacing == LACING_NONE) { + blockLacingSampleCount = 1; + blockLacingSampleSizes = ensureArrayCapacity(blockLacingSampleSizes, 1); + blockLacingSampleSizes[0] = contentSize - blockTrackNumberLength - 3; + } else { + if (id != ID_SIMPLE_BLOCK) { + throw new ParserException("Lacing only supported in SimpleBlocks."); + } + + // Read the sample count (1 byte). + readScratch(input, 4); + blockLacingSampleCount = (scratch.data[3] & 0xFF) + 1; + blockLacingSampleSizes = + ensureArrayCapacity(blockLacingSampleSizes, blockLacingSampleCount); + if (lacing == LACING_FIXED_SIZE) { + int blockLacingSampleSize = + (contentSize - blockTrackNumberLength - 4) / blockLacingSampleCount; + Arrays.fill(blockLacingSampleSizes, 0, blockLacingSampleCount, blockLacingSampleSize); + } else if (lacing == LACING_XIPH) { + int totalSamplesSize = 0; + int headerSize = 4; + for (int sampleIndex = 0; sampleIndex < blockLacingSampleCount - 1; sampleIndex++) { + blockLacingSampleSizes[sampleIndex] = 0; + int byteValue; + do { + readScratch(input, ++headerSize); + byteValue = scratch.data[headerSize - 1] & 0xFF; + blockLacingSampleSizes[sampleIndex] += byteValue; + } while (byteValue == 0xFF); + totalSamplesSize += blockLacingSampleSizes[sampleIndex]; + } + blockLacingSampleSizes[blockLacingSampleCount - 1] = + contentSize - blockTrackNumberLength - headerSize - totalSamplesSize; + } else if (lacing == LACING_EBML) { + int totalSamplesSize = 0; + int headerSize = 4; + for (int sampleIndex = 0; sampleIndex < blockLacingSampleCount - 1; sampleIndex++) { + blockLacingSampleSizes[sampleIndex] = 0; + readScratch(input, ++headerSize); + if (scratch.data[headerSize - 1] == 0) { + throw new ParserException("No valid varint length mask found"); + } + long readValue = 0; + for (int i = 0; i < 8; i++) { + int lengthMask = 1 << (7 - i); + if ((scratch.data[headerSize - 1] & lengthMask) != 0) { + int readPosition = headerSize - 1; + headerSize += i; + readScratch(input, headerSize); + readValue = (scratch.data[readPosition++] & 0xFF) & ~lengthMask; + while (readPosition < headerSize) { + readValue <<= 8; + readValue |= (scratch.data[readPosition++] & 0xFF); + } + // The first read value is the first size. Later values are signed offsets. + if (sampleIndex > 0) { + readValue -= (1L << 6 + i * 7) - 1; + } + break; + } + } + if (readValue < Integer.MIN_VALUE || readValue > Integer.MAX_VALUE) { + throw new ParserException("EBML lacing sample size out of range."); + } + int intReadValue = (int) readValue; + blockLacingSampleSizes[sampleIndex] = sampleIndex == 0 + ? intReadValue : blockLacingSampleSizes[sampleIndex - 1] + intReadValue; + totalSamplesSize += blockLacingSampleSizes[sampleIndex]; + } + blockLacingSampleSizes[blockLacingSampleCount - 1] = + contentSize - blockTrackNumberLength - headerSize - totalSamplesSize; + } else { + // Lacing is always in the range 0--3. + throw new ParserException("Unexpected lacing value: " + lacing); + } + } + + int timecode = (scratch.data[0] << 8) | (scratch.data[1] & 0xFF); + blockTimeUs = clusterTimecodeUs + scaleTimecodeToUs(timecode); + boolean isInvisible = (scratch.data[2] & 0x08) == 0x08; + boolean isKeyframe = track.type == TRACK_TYPE_AUDIO + || (id == ID_SIMPLE_BLOCK && (scratch.data[2] & 0x80) == 0x80); + blockFlags = (isKeyframe ? C.SAMPLE_FLAG_SYNC : 0) + | (isInvisible ? C.SAMPLE_FLAG_DECODE_ONLY : 0); + blockState = BLOCK_STATE_DATA; + blockLacingSampleIndex = 0; + } + + if (id == ID_SIMPLE_BLOCK) { + // For SimpleBlock, we have metadata for each sample here. + while (blockLacingSampleIndex < blockLacingSampleCount) { + writeSampleData(input, track, blockLacingSampleSizes[blockLacingSampleIndex]); + long sampleTimeUs = this.blockTimeUs + + (blockLacingSampleIndex * track.defaultSampleDurationNs) / 1000; + commitSampleToOutput(track, sampleTimeUs); + blockLacingSampleIndex++; + } + blockState = BLOCK_STATE_START; + } else { + // For Block, we send the metadata at the end of the BlockGroup element since we'll know + // if the sample is a keyframe or not only at that point. + writeSampleData(input, track, blockLacingSampleSizes[0]); + } + + return; + default: + throw new ParserException("Unexpected id: " + id); + } + } + + private void commitSampleToOutput(Track track, long timeUs) { + if (CODEC_ID_SUBRIP.equals(track.codecId)) { + writeSubripSample(track); + } + track.output.sampleMetadata(timeUs, blockFlags, sampleBytesWritten, 0, track.encryptionKeyId); + sampleRead = true; + resetSample(); + } + + private void resetSample() { + sampleBytesRead = 0; + sampleBytesWritten = 0; + sampleCurrentNalBytesRemaining = 0; + sampleEncodingHandled = false; + sampleStrippedBytes.reset(); + } + + /** + * Ensures {@link #scratch} contains at least {@code requiredLength} bytes of data, reading from + * the extractor input if necessary. + */ + private void readScratch(ExtractorInput input, int requiredLength) + throws IOException, InterruptedException { + if (scratch.limit() >= requiredLength) { + return; + } + if (scratch.capacity() < requiredLength) { + scratch.reset(Arrays.copyOf(scratch.data, Math.max(scratch.data.length * 2, requiredLength)), + scratch.limit()); + } + input.readFully(scratch.data, scratch.limit(), requiredLength - scratch.limit()); + scratch.setLimit(requiredLength); + } + + private void writeSampleData(ExtractorInput input, Track track, int size) + throws IOException, InterruptedException { + if (CODEC_ID_SUBRIP.equals(track.codecId)) { + int sizeWithPrefix = SUBRIP_PREFIX.length + size; + if (subripSample.capacity() < sizeWithPrefix) { + // Initialize subripSample to contain the required prefix and have space to hold a subtitle + // twice as long as this one. + subripSample.data = Arrays.copyOf(SUBRIP_PREFIX, sizeWithPrefix + size); + } + input.readFully(subripSample.data, SUBRIP_PREFIX.length, size); + subripSample.setPosition(0); + subripSample.setLimit(sizeWithPrefix); + // Defer writing the data to the track output. We need to modify the sample data by setting + // the correct end timecode, which we might not have yet. + return; + } + + TrackOutput output = track.output; + if (!sampleEncodingHandled) { + if (track.hasContentEncryption) { + // If the sample is encrypted, read its encryption signal byte and set the IV size. + // Clear the encrypted flag. + blockFlags &= ~C.SAMPLE_FLAG_ENCRYPTED; + input.readFully(scratch.data, 0, 1); + sampleBytesRead++; + if ((scratch.data[0] & 0x80) == 0x80) { + throw new ParserException("Extension bit is set in signal byte"); + } + if ((scratch.data[0] & 0x01) == 0x01) { + scratch.data[0] = (byte) ENCRYPTION_IV_SIZE; + scratch.setPosition(0); + output.sampleData(scratch, 1); + sampleBytesWritten++; + blockFlags |= C.SAMPLE_FLAG_ENCRYPTED; + } + } else if (track.sampleStrippedBytes != null) { + // If the sample has header stripping, prepare to read/output the stripped bytes first. + sampleStrippedBytes.reset(track.sampleStrippedBytes, track.sampleStrippedBytes.length); + } + sampleEncodingHandled = true; + } + size += sampleStrippedBytes.limit(); + + if (CODEC_ID_H264.equals(track.codecId) || CODEC_ID_H265.equals(track.codecId)) { + // TODO: Deduplicate with Mp4Extractor. + + // Zero the top three bytes of the array that we'll use to parse nal unit lengths, in case + // they're only 1 or 2 bytes long. + byte[] nalLengthData = nalLength.data; + nalLengthData[0] = 0; + nalLengthData[1] = 0; + nalLengthData[2] = 0; + int nalUnitLengthFieldLength = track.nalUnitLengthFieldLength; + int nalUnitLengthFieldLengthDiff = 4 - track.nalUnitLengthFieldLength; + // NAL units are length delimited, but the decoder requires start code delimited units. + // Loop until we've written the sample to the track output, replacing length delimiters with + // start codes as we encounter them. + while (sampleBytesRead < size) { + if (sampleCurrentNalBytesRemaining == 0) { + // Read the NAL length so that we know where we find the next one. + readToTarget(input, nalLengthData, nalUnitLengthFieldLengthDiff, + nalUnitLengthFieldLength); + nalLength.setPosition(0); + sampleCurrentNalBytesRemaining = nalLength.readUnsignedIntToInt(); + // Write a start code for the current NAL unit. + nalStartCode.setPosition(0); + output.sampleData(nalStartCode, 4); + sampleBytesWritten += 4; + } else { + // Write the payload of the NAL unit. + sampleCurrentNalBytesRemaining -= + readToOutput(input, output, sampleCurrentNalBytesRemaining); + } + } + } else { + while (sampleBytesRead < size) { + readToOutput(input, output, size - sampleBytesRead); + } + } + + if (CODEC_ID_VORBIS.equals(track.codecId)) { + // Vorbis decoder in android MediaCodec [1] expects the last 4 bytes of the sample to be the + // number of samples in the current page. This definition holds good only for Ogg and + // irrelevant for WebM. So we always set this to -1 (the decoder will ignore this value if we + // set it to -1). The android platform media extractor [2] does the same. + // [1] https://android.googlesource.com/platform/frameworks/av/+/lollipop-release/media/libstagefright/codecs/vorbis/dec/SoftVorbis.cpp#314 + // [2] https://android.googlesource.com/platform/frameworks/av/+/lollipop-release/media/libstagefright/NuMediaExtractor.cpp#474 + vorbisNumPageSamples.setPosition(0); + output.sampleData(vorbisNumPageSamples, 4); + sampleBytesWritten += 4; + } + } + + private void writeSubripSample(Track track) { + setSubripSampleEndTimecode(subripSample.data, blockDurationUs); + // Note: If we ever want to support DRM protected subtitles then we'll need to output the + // appropriate encryption data here. + track.output.sampleData(subripSample, subripSample.limit()); + sampleBytesWritten += subripSample.limit(); + } + + private static void setSubripSampleEndTimecode(byte[] subripSampleData, long timeUs) { + byte[] timeCodeData; + if (timeUs == UNKNOWN) { + timeCodeData = SUBRIP_TIMECODE_EMPTY; + } else { + int hours = (int) (timeUs / 3600000000L); + timeUs -= (hours * 3600000000L); + int minutes = (int) (timeUs / 60000000); + timeUs -= (minutes * 60000000); + int seconds = (int) (timeUs / 1000000); + timeUs -= (seconds * 1000000); + int milliseconds = (int) (timeUs / 1000); + timeCodeData = String.format(Locale.US, "%02d:%02d:%02d,%03d", + hours, minutes, seconds, milliseconds).getBytes(); + } + System.arraycopy(timeCodeData, 0, subripSampleData, SUBRIP_PREFIX_END_TIMECODE_OFFSET, + SUBRIP_TIMECODE_LENGTH); + } + + /** + * Writes {@code length} bytes of sample data into {@code target} at {@code offset}, consisting of + * pending {@link #sampleStrippedBytes} and any remaining data read from {@code input}. + */ + private void readToTarget(ExtractorInput input, byte[] target, int offset, int length) + throws IOException, InterruptedException { + int pendingStrippedBytes = Math.min(length, sampleStrippedBytes.bytesLeft()); + input.readFully(target, offset + pendingStrippedBytes, length - pendingStrippedBytes); + if (pendingStrippedBytes > 0) { + sampleStrippedBytes.readBytes(target, offset, pendingStrippedBytes); + } + sampleBytesRead += length; + } + + /** + * Outputs up to {@code length} bytes of sample data to {@code output}, consisting of either + * {@link #sampleStrippedBytes} or data read from {@code input}. + */ + private int readToOutput(ExtractorInput input, TrackOutput output, int length) + throws IOException, InterruptedException { + int bytesRead; + int strippedBytesLeft = sampleStrippedBytes.bytesLeft(); + if (strippedBytesLeft > 0) { + bytesRead = Math.min(length, strippedBytesLeft); + output.sampleData(sampleStrippedBytes, bytesRead); + } else { + bytesRead = output.sampleData(input, length, false); + } + sampleBytesRead += bytesRead; + sampleBytesWritten += bytesRead; + return bytesRead; + } + + /** + * Builds a {@link SeekMap} from the recently gathered Cues information. + * + * @return The built {@link SeekMap}. May be {@link SeekMap#UNSEEKABLE} if cues information was + * missing or incomplete. + */ + private SeekMap buildSeekMap() { + if (segmentContentPosition == UNKNOWN || durationUs == C.UNKNOWN_TIME_US + || cueTimesUs == null || cueTimesUs.size() == 0 + || cueClusterPositions == null || cueClusterPositions.size() != cueTimesUs.size()) { + // Cues information is missing or incomplete. + cueTimesUs = null; + cueClusterPositions = null; + return SeekMap.UNSEEKABLE; + } + int cuePointsSize = cueTimesUs.size(); + int[] sizes = new int[cuePointsSize]; + long[] offsets = new long[cuePointsSize]; + long[] durationsUs = new long[cuePointsSize]; + long[] timesUs = new long[cuePointsSize]; + for (int i = 0; i < cuePointsSize; i++) { + timesUs[i] = cueTimesUs.get(i); + offsets[i] = segmentContentPosition + cueClusterPositions.get(i); + } + for (int i = 0; i < cuePointsSize - 1; i++) { + sizes[i] = (int) (offsets[i + 1] - offsets[i]); + durationsUs[i] = timesUs[i + 1] - timesUs[i]; + } + sizes[cuePointsSize - 1] = + (int) (segmentContentPosition + segmentContentSize - offsets[cuePointsSize - 1]); + durationsUs[cuePointsSize - 1] = durationUs - timesUs[cuePointsSize - 1]; + cueTimesUs = null; + cueClusterPositions = null; + return new ChunkIndex(sizes, offsets, durationsUs, timesUs); + } + + /** + * Updates the position of the holder to Cues element's position if the extractor configuration + * permits use of master seek entry. After building Cues sets the holder's position back to where + * it was before. + * + * @param seekPosition The holder whose position will be updated. + * @param currentPosition Current position of the input. + * @return true if the seek position was updated, false otherwise. + */ + private boolean maybeSeekForCues(PositionHolder seekPosition, long currentPosition) { + if (seekForCues) { + seekPositionAfterBuildingCues = currentPosition; + seekPosition.position = cuesContentPosition; + seekForCues = false; + return true; + } + // After parsing Cues, seek back to original position if available. We will not do this unless + // we seeked to get to the Cues in the first place. + if (sentSeekMap && seekPositionAfterBuildingCues != UNKNOWN) { + seekPosition.position = seekPositionAfterBuildingCues; + seekPositionAfterBuildingCues = UNKNOWN; + return true; + } + return false; + } + + private long scaleTimecodeToUs(long unscaledTimecode) throws ParserException { + if (timecodeScale == C.UNKNOWN_TIME_US) { + throw new ParserException("Can't scale timecode prior to timecodeScale being set."); + } + return Util.scaleLargeTimestamp(unscaledTimecode, timecodeScale, 1000); + } + + private static boolean isCodecSupported(String codecId) { + return CODEC_ID_VP8.equals(codecId) + || CODEC_ID_VP9.equals(codecId) + || CODEC_ID_MPEG2.equals(codecId) + || CODEC_ID_MPEG4_SP.equals(codecId) + || CODEC_ID_MPEG4_ASP.equals(codecId) + || CODEC_ID_MPEG4_AP.equals(codecId) + || CODEC_ID_H264.equals(codecId) + || CODEC_ID_H265.equals(codecId) + || CODEC_ID_FOURCC.equals(codecId) + || CODEC_ID_OPUS.equals(codecId) + || CODEC_ID_VORBIS.equals(codecId) + || CODEC_ID_AAC.equals(codecId) + || CODEC_ID_MP3.equals(codecId) + || CODEC_ID_AC3.equals(codecId) + || CODEC_ID_E_AC3.equals(codecId) + || CODEC_ID_TRUEHD.equals(codecId) + || CODEC_ID_DTS.equals(codecId) + || CODEC_ID_DTS_EXPRESS.equals(codecId) + || CODEC_ID_DTS_LOSSLESS.equals(codecId) + || CODEC_ID_FLAC.equals(codecId) + || CODEC_ID_ACM.equals(codecId) + || CODEC_ID_PCM_INT_LIT.equals(codecId) + || CODEC_ID_SUBRIP.equals(codecId) + || CODEC_ID_VOBSUB.equals(codecId) + || CODEC_ID_PGS.equals(codecId); + } + + /** + * Returns an array that can store (at least) {@code length} elements, which will be either a new + * array or {@code array} if it's not null and large enough. + */ + private static int[] ensureArrayCapacity(int[] array, int length) { + if (array == null) { + return new int[length]; + } else if (array.length >= length) { + return array; + } else { + // Double the size to avoid allocating constantly if the required length increases gradually. + return new int[Math.max(array.length * 2, length)]; + } + } + + /** + * Passes events through to the outer {@link WebmExtractor}. + */ + private final class InnerEbmlReaderOutput implements EbmlReaderOutput { + + @Override + public int getElementType(int id) { + return WebmExtractor.this.getElementType(id); + } + + @Override + public boolean isLevel1Element(int id) { + return WebmExtractor.this.isLevel1Element(id); + } + + @Override + public void startMasterElement(int id, long contentPosition, long contentSize) + throws ParserException { + WebmExtractor.this.startMasterElement(id, contentPosition, contentSize); + } + + @Override + public void endMasterElement(int id) throws ParserException { + WebmExtractor.this.endMasterElement(id); + } + + @Override + public void integerElement(int id, long value) throws ParserException { + WebmExtractor.this.integerElement(id, value); + } + + @Override + public void floatElement(int id, double value) throws ParserException { + WebmExtractor.this.floatElement(id, value); + } + + @Override + public void stringElement(int id, String value) throws ParserException { + WebmExtractor.this.stringElement(id, value); + } + + @Override + public void binaryElement(int id, int contentsSize, ExtractorInput input) + throws IOException, InterruptedException { + WebmExtractor.this.binaryElement(id, contentsSize, input); + } + + } + + private static final class Track { + + private static final int DISPLAY_UNIT_PIXELS = 0; + + // Common elements. + public String codecId; + public int number; + public int type; + public int defaultSampleDurationNs; + public boolean hasContentEncryption; + public byte[] sampleStrippedBytes; + public byte[] encryptionKeyId; + public byte[] codecPrivate; + + // Video elements. + public int width = MediaFormat.NO_VALUE; + public int height = MediaFormat.NO_VALUE; + public int displayWidth = MediaFormat.NO_VALUE; + public int displayHeight = MediaFormat.NO_VALUE; + public int displayUnit = DISPLAY_UNIT_PIXELS; + + // Audio elements. Initially set to their default values. + public int channelCount = 1; + public int audioBitDepth = -1; + public int sampleRate = 8000; + public long codecDelayNs = 0; + public long seekPreRollNs = 0; + + // Text elements. + private String language = "eng"; + + // Set when the output is initialized. nalUnitLengthFieldLength is only set for H264/H265. + public TrackOutput output; + public int nalUnitLengthFieldLength; + + /** + * Initializes the track with an output. + */ + public void initializeOutput(ExtractorOutput output, int trackId, long durationUs) + throws ParserException { + String mimeType; + int maxInputSize = MediaFormat.NO_VALUE; + int pcmEncoding = MediaFormat.NO_VALUE; + List initializationData = null; + switch (codecId) { + case CODEC_ID_VP8: + mimeType = MimeTypes.VIDEO_VP8; + break; + case CODEC_ID_VP9: + mimeType = MimeTypes.VIDEO_VP9; + break; + case CODEC_ID_MPEG2: + mimeType = MimeTypes.VIDEO_MPEG2; + break; + case CODEC_ID_MPEG4_SP: + case CODEC_ID_MPEG4_ASP: + case CODEC_ID_MPEG4_AP: + mimeType = MimeTypes.VIDEO_MP4V; + initializationData = + codecPrivate == null ? null : Collections.singletonList(codecPrivate); + break; + case CODEC_ID_H264: + mimeType = MimeTypes.VIDEO_H264; + Pair, Integer> h264Data = parseAvcCodecPrivate( + new ParsableByteArray(codecPrivate)); + initializationData = h264Data.first; + nalUnitLengthFieldLength = h264Data.second; + break; + case CODEC_ID_H265: + mimeType = MimeTypes.VIDEO_H265; + Pair, Integer> hevcData = parseHevcCodecPrivate( + new ParsableByteArray(codecPrivate)); + initializationData = hevcData.first; + nalUnitLengthFieldLength = hevcData.second; + break; + case CODEC_ID_FOURCC: + mimeType = MimeTypes.VIDEO_VC1; + initializationData = parseFourCcVc1Private(new ParsableByteArray(codecPrivate)); + break; + case CODEC_ID_VORBIS: + mimeType = MimeTypes.AUDIO_VORBIS; + maxInputSize = VORBIS_MAX_INPUT_SIZE; + initializationData = parseVorbisCodecPrivate(codecPrivate); + break; + case CODEC_ID_OPUS: + mimeType = MimeTypes.AUDIO_OPUS; + maxInputSize = OPUS_MAX_INPUT_SIZE; + initializationData = new ArrayList<>(3); + initializationData.add(codecPrivate); + initializationData.add( + ByteBuffer.allocate(8).order(ByteOrder.nativeOrder()).putLong(codecDelayNs).array()); + initializationData.add( + ByteBuffer.allocate(8).order(ByteOrder.nativeOrder()).putLong(seekPreRollNs).array()); + break; + case CODEC_ID_AAC: + mimeType = MimeTypes.AUDIO_AAC; + initializationData = Collections.singletonList(codecPrivate); + break; + case CODEC_ID_MP3: + mimeType = MimeTypes.AUDIO_MPEG; + maxInputSize = MP3_MAX_INPUT_SIZE; + break; + case CODEC_ID_AC3: + mimeType = MimeTypes.AUDIO_AC3; + break; + case CODEC_ID_E_AC3: + mimeType = MimeTypes.AUDIO_E_AC3; + break; + case CODEC_ID_TRUEHD: + mimeType = MimeTypes.AUDIO_TRUEHD; + break; + case CODEC_ID_DTS: + case CODEC_ID_DTS_EXPRESS: + mimeType = MimeTypes.AUDIO_DTS; + break; + case CODEC_ID_DTS_LOSSLESS: + mimeType = MimeTypes.AUDIO_DTS_HD; + break; + case CODEC_ID_FLAC: + mimeType = MimeTypes.AUDIO_FLAC; + initializationData = Collections.singletonList(codecPrivate); + break; + case CODEC_ID_ACM: + mimeType = MimeTypes.AUDIO_RAW; + if (!parseMsAcmCodecPrivate(new ParsableByteArray(codecPrivate))) { + throw new ParserException("Non-PCM MS/ACM is unsupported"); + } + pcmEncoding = Util.getPcmEncoding(audioBitDepth); + if (pcmEncoding == C.ENCODING_INVALID) { + throw new ParserException("Unsupported PCM bit depth: " + audioBitDepth); + } + break; + case CODEC_ID_PCM_INT_LIT: + mimeType = MimeTypes.AUDIO_RAW; + pcmEncoding = Util.getPcmEncoding(audioBitDepth); + if (pcmEncoding == C.ENCODING_INVALID) { + throw new ParserException("Unsupported PCM bit depth: " + audioBitDepth); + } + break; + case CODEC_ID_SUBRIP: + mimeType = MimeTypes.APPLICATION_SUBRIP; + break; + case CODEC_ID_VOBSUB: + mimeType = MimeTypes.APPLICATION_VOBSUB; + initializationData = Collections.singletonList(codecPrivate); + break; + case CODEC_ID_PGS: + mimeType = MimeTypes.APPLICATION_PGS; + break; + default: + throw new ParserException("Unrecognized codec identifier."); + } + + MediaFormat format; + // TODO: Consider reading the name elements of the tracks and, if present, incorporating them + // into the trackId passed when creating the formats. + if (MimeTypes.isAudio(mimeType)) { + format = MediaFormat.createAudioFormat(Integer.toString(trackId), mimeType, + MediaFormat.NO_VALUE, maxInputSize, durationUs, channelCount, sampleRate, + initializationData, language, pcmEncoding); + } else if (MimeTypes.isVideo(mimeType)) { + if (displayUnit == Track.DISPLAY_UNIT_PIXELS) { + displayWidth = displayWidth == MediaFormat.NO_VALUE ? width : displayWidth; + displayHeight = displayHeight == MediaFormat.NO_VALUE ? height : displayHeight; + } + float pixelWidthHeightRatio = MediaFormat.NO_VALUE; + if (displayWidth != MediaFormat.NO_VALUE && displayHeight != MediaFormat.NO_VALUE) { + pixelWidthHeightRatio = ((float) (height * displayWidth)) / (width * displayHeight); + } + format = MediaFormat.createVideoFormat(Integer.toString(trackId), mimeType, + MediaFormat.NO_VALUE, maxInputSize, durationUs, width, height, initializationData, + MediaFormat.NO_VALUE, pixelWidthHeightRatio); + } else if (MimeTypes.APPLICATION_SUBRIP.equals(mimeType)) { + format = MediaFormat.createTextFormat(Integer.toString(trackId), mimeType, + MediaFormat.NO_VALUE, durationUs, language); + } else if (MimeTypes.APPLICATION_VOBSUB.equals(mimeType) + || MimeTypes.APPLICATION_PGS.equals(mimeType)) { + format = MediaFormat.createImageFormat(Integer.toString(trackId), mimeType, + MediaFormat.NO_VALUE, durationUs, initializationData, language); + } else { + throw new ParserException("Unexpected MIME type."); + } + + this.output = output.track(number); + this.output.format(format); + } + + /** + * Builds initialization data for a {@link MediaFormat} from FourCC codec private data. + *

+ * VC1 is the only supported compression type. + * + * @return The initialization data for the {@link MediaFormat}. + * @throws ParserException If the initialization data could not be built. + */ + private static List parseFourCcVc1Private(ParsableByteArray buffer) + throws ParserException { + try { + buffer.skipBytes(16); // size(4), width(4), height(4), planes(2), bitcount(2). + long compression = buffer.readLittleEndianUnsignedInt(); + if (compression != FOURCC_COMPRESSION_VC1) { + throw new ParserException("Unsupported FourCC compression type: " + compression); + } + + // Search for the initialization data from the end of the BITMAPINFOHEADER. The last 20 + // bytes of which are: sizeImage(4), xPel/m (4), yPel/m (4), clrUsed(4), clrImportant(4). + int startOffset = buffer.getPosition() + 20; + byte[] bufferData = buffer.data; + for (int offset = startOffset; offset < bufferData.length - 4; offset++) { + if (bufferData[offset] == 0x00 && bufferData[offset + 1] == 0x00 + && bufferData[offset + 2] == 0x01 && bufferData[offset + 3] == 0x0F) { + // We've found the initialization data. + byte[] initializationData = Arrays.copyOfRange(bufferData, offset, bufferData.length); + return Collections.singletonList(initializationData); + } + } + + throw new ParserException("Failed to find FourCC VC1 initialization data"); + } catch (ArrayIndexOutOfBoundsException e) { + throw new ParserException("Error parsing FourCC VC1 codec private"); + } + } + + /** + * Builds initialization data for a {@link MediaFormat} from H.264 (AVC) codec private data. + * + * @return The initialization data for the {@link MediaFormat}. + * @throws ParserException If the initialization data could not be built. + */ + private static Pair, Integer> parseAvcCodecPrivate(ParsableByteArray buffer) + throws ParserException { + try { + // TODO: Deduplicate with AtomParsers.parseAvcCFromParent. + buffer.setPosition(4); + int nalUnitLengthFieldLength = (buffer.readUnsignedByte() & 0x03) + 1; + if (nalUnitLengthFieldLength == 3) { + throw new ParserException(); + } + List initializationData = new ArrayList<>(); + int numSequenceParameterSets = buffer.readUnsignedByte() & 0x1F; + for (int i = 0; i < numSequenceParameterSets; i++) { + initializationData.add(NalUnitUtil.parseChildNalUnit(buffer)); + } + int numPictureParameterSets = buffer.readUnsignedByte(); + for (int j = 0; j < numPictureParameterSets; j++) { + initializationData.add(NalUnitUtil.parseChildNalUnit(buffer)); + } + return Pair.create(initializationData, nalUnitLengthFieldLength); + } catch (ArrayIndexOutOfBoundsException e) { + throw new ParserException("Error parsing AVC codec private"); + } + } + + /** + * Builds initialization data for a {@link MediaFormat} from H.265 (HEVC) codec private data. + * + * @return The initialization data for the {@link MediaFormat}. + * @throws ParserException If the initialization data could not be built. + */ + private static Pair, Integer> parseHevcCodecPrivate(ParsableByteArray parent) + throws ParserException { + try { + // TODO: Deduplicate with AtomParsers.parseHvcCFromParent. + parent.setPosition(21); + int lengthSizeMinusOne = parent.readUnsignedByte() & 0x03; + + // Calculate the combined size of all VPS/SPS/PPS bitstreams. + int numberOfArrays = parent.readUnsignedByte(); + int csdLength = 0; + int csdStartPosition = parent.getPosition(); + for (int i = 0; i < numberOfArrays; i++) { + parent.skipBytes(1); // completeness (1), nal_unit_type (7) + int numberOfNalUnits = parent.readUnsignedShort(); + for (int j = 0; j < numberOfNalUnits; j++) { + int nalUnitLength = parent.readUnsignedShort(); + csdLength += 4 + nalUnitLength; // Start code and NAL unit. + parent.skipBytes(nalUnitLength); + } + } + + // Concatenate the codec-specific data into a single buffer. + parent.setPosition(csdStartPosition); + byte[] buffer = new byte[csdLength]; + int bufferPosition = 0; + for (int i = 0; i < numberOfArrays; i++) { + parent.skipBytes(1); // completeness (1), nal_unit_type (7) + int numberOfNalUnits = parent.readUnsignedShort(); + for (int j = 0; j < numberOfNalUnits; j++) { + int nalUnitLength = parent.readUnsignedShort(); + System.arraycopy(NalUnitUtil.NAL_START_CODE, 0, buffer, bufferPosition, + NalUnitUtil.NAL_START_CODE.length); + bufferPosition += NalUnitUtil.NAL_START_CODE.length; + System.arraycopy(parent.data, parent.getPosition(), buffer, bufferPosition, + nalUnitLength); + bufferPosition += nalUnitLength; + parent.skipBytes(nalUnitLength); + } + } + + List initializationData = csdLength == 0 ? null : Collections.singletonList(buffer); + return Pair.create(initializationData, lengthSizeMinusOne + 1); + } catch (ArrayIndexOutOfBoundsException e) { + throw new ParserException("Error parsing HEVC codec private"); + } + } + + /** + * Builds initialization data for a {@link MediaFormat} from Vorbis codec private data. + * + * @return The initialization data for the {@link MediaFormat}. + * @throws ParserException If the initialization data could not be built. + */ + private static List parseVorbisCodecPrivate(byte[] codecPrivate) + throws ParserException { + try { + if (codecPrivate[0] != 0x02) { + throw new ParserException("Error parsing vorbis codec private"); + } + int offset = 1; + int vorbisInfoLength = 0; + while (codecPrivate[offset] == (byte) 0xFF) { + vorbisInfoLength += 0xFF; + offset++; + } + vorbisInfoLength += codecPrivate[offset++]; + + int vorbisSkipLength = 0; + while (codecPrivate[offset] == (byte) 0xFF) { + vorbisSkipLength += 0xFF; + offset++; + } + vorbisSkipLength += codecPrivate[offset++]; + + if (codecPrivate[offset] != 0x01) { + throw new ParserException("Error parsing vorbis codec private"); + } + byte[] vorbisInfo = new byte[vorbisInfoLength]; + System.arraycopy(codecPrivate, offset, vorbisInfo, 0, vorbisInfoLength); + offset += vorbisInfoLength; + if (codecPrivate[offset] != 0x03) { + throw new ParserException("Error parsing vorbis codec private"); + } + offset += vorbisSkipLength; + if (codecPrivate[offset] != 0x05) { + throw new ParserException("Error parsing vorbis codec private"); + } + byte[] vorbisBooks = new byte[codecPrivate.length - offset]; + System.arraycopy(codecPrivate, offset, vorbisBooks, 0, codecPrivate.length - offset); + List initializationData = new ArrayList<>(2); + initializationData.add(vorbisInfo); + initializationData.add(vorbisBooks); + return initializationData; + } catch (ArrayIndexOutOfBoundsException e) { + throw new ParserException("Error parsing vorbis codec private"); + } + } + + /** + * Parses an MS/ACM codec private, returning whether it indicates PCM audio. + * + * @return True if the codec private indicates PCM audio. False otherwise. + * @throws ParserException If a parsing error occurs. + */ + private static boolean parseMsAcmCodecPrivate(ParsableByteArray buffer) throws ParserException { + try { + int formatTag = buffer.readLittleEndianUnsignedShort(); + if (formatTag == WAVE_FORMAT_PCM) { + return true; + } else if (formatTag == WAVE_FORMAT_EXTENSIBLE) { + buffer.setPosition(WAVE_FORMAT_SIZE + 6); // unionSamples(2), channelMask(4) + return buffer.readLong() == WAVE_SUBFORMAT_PCM.getMostSignificantBits() + && buffer.readLong() == WAVE_SUBFORMAT_PCM.getLeastSignificantBits(); + } else { + return false; + } + } catch (ArrayIndexOutOfBoundsException e) { + throw new ParserException("Error parsing MS/ACM codec private"); + } + } + + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/hls/Aes128DataSource.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/hls/Aes128DataSource.java new file mode 100644 index 000000000..8dbb520e6 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/hls/Aes128DataSource.java @@ -0,0 +1,108 @@ +/* + * 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.messenger.exoplayer.hls; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.upstream.DataSource; +import org.telegram.messenger.exoplayer.upstream.DataSourceInputStream; +import org.telegram.messenger.exoplayer.upstream.DataSpec; +import org.telegram.messenger.exoplayer.util.Assertions; + +import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.NoSuchAlgorithmException; +import java.security.spec.AlgorithmParameterSpec; + +import javax.crypto.Cipher; +import javax.crypto.CipherInputStream; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +/** + * A {@link DataSource} that decrypts data read from an upstream source, encrypted with AES-128 with + * a 128-bit key and PKCS7 padding. + *

+ * Note that this {@link DataSource} does not support being opened from arbitrary offsets. It is + * designed specifically for reading whole files as defined in an HLS media playlist. For this + * reason the implementation is private to the HLS package. + */ +/* package */ final class Aes128DataSource implements DataSource { + + private final DataSource upstream; + private final byte[] encryptionKey; + private final byte[] encryptionIv; + + private CipherInputStream cipherInputStream; + + /** + * @param upstream The upstream {@link DataSource}. + * @param encryptionKey The encryption key. + * @param encryptionIv The encryption initialization vector. + */ + public Aes128DataSource(DataSource upstream, byte[] encryptionKey, byte[] encryptionIv) { + this.upstream = upstream; + this.encryptionKey = encryptionKey; + this.encryptionIv = encryptionIv; + } + + @Override + public long open(DataSpec dataSpec) throws IOException { + Cipher cipher; + try { + cipher = Cipher.getInstance("AES/CBC/PKCS7Padding"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } catch (NoSuchPaddingException e) { + throw new RuntimeException(e); + } + + Key cipherKey = new SecretKeySpec(encryptionKey, "AES"); + AlgorithmParameterSpec cipherIV = new IvParameterSpec(encryptionIv); + + try { + cipher.init(Cipher.DECRYPT_MODE, cipherKey, cipherIV); + } catch (InvalidKeyException e) { + throw new RuntimeException(e); + } catch (InvalidAlgorithmParameterException e) { + throw new RuntimeException(e); + } + + cipherInputStream = new CipherInputStream( + new DataSourceInputStream(upstream, dataSpec), cipher); + + return C.LENGTH_UNBOUNDED; + } + + @Override + public void close() throws IOException { + cipherInputStream = null; + upstream.close(); + } + + @Override + public int read(byte[] buffer, int offset, int readLength) throws IOException { + Assertions.checkState(cipherInputStream != null); + int bytesRead = cipherInputStream.read(buffer, offset, readLength); + if (bytesRead < 0) { + return -1; + } + return bytesRead; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/hls/DefaultHlsTrackSelector.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/hls/DefaultHlsTrackSelector.java new file mode 100644 index 000000000..9576a6ac4 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/hls/DefaultHlsTrackSelector.java @@ -0,0 +1,141 @@ +/* + * 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.messenger.exoplayer.hls; + +import org.telegram.messenger.exoplayer.chunk.VideoFormatSelectorUtil; + +import android.content.Context; +import android.text.TextUtils; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * A default {@link HlsTrackSelector} implementation. + */ +public final class DefaultHlsTrackSelector implements HlsTrackSelector { + + private static final int TYPE_DEFAULT = 0; + private static final int TYPE_AUDIO = 1; + private static final int TYPE_SUBTITLE = 2; + + private final Context context; + private final int type; + + /** + * Creates a {@link DefaultHlsTrackSelector} that selects the streams defined in the playlist. + * + * @param context A context. + * @return The selector instance. + */ + public static DefaultHlsTrackSelector newDefaultInstance(Context context) { + return new DefaultHlsTrackSelector(context, TYPE_DEFAULT); + } + + /** + * Creates a {@link DefaultHlsTrackSelector} that selects alternate audio renditions. + * + * @return The selector instance. + */ + public static DefaultHlsTrackSelector newAudioInstance() { + return new DefaultHlsTrackSelector(null, TYPE_AUDIO); + } + + /** + * Creates a {@link DefaultHlsTrackSelector} that selects subtitle renditions. + * + * @return The selector instance. + */ + public static DefaultHlsTrackSelector newSubtitleInstance() { + return new DefaultHlsTrackSelector(null, TYPE_SUBTITLE); + } + + private DefaultHlsTrackSelector(Context context, int type) { + this.context = context; + this.type = type; + } + + @Override + public void selectTracks(HlsMasterPlaylist playlist, Output output) throws IOException { + if (type == TYPE_AUDIO || type == TYPE_SUBTITLE) { + List variants = type == TYPE_AUDIO ? playlist.audios : playlist.subtitles; + if (variants != null && !variants.isEmpty()) { + for (int i = 0; i < variants.size(); i++) { + output.fixedTrack(playlist, variants.get(i)); + } + } + return; + } + + // Type is TYPE_DEFAULT. + + ArrayList enabledVariantList = new ArrayList<>(); + int[] variantIndices = VideoFormatSelectorUtil.selectVideoFormatsForDefaultDisplay( + context, playlist.variants, null, false); + for (int i = 0; i < variantIndices.length; i++) { + enabledVariantList.add(playlist.variants.get(variantIndices[i])); + } + + ArrayList definiteVideoVariants = new ArrayList<>(); + ArrayList definiteAudioOnlyVariants = new ArrayList<>(); + for (int i = 0; i < enabledVariantList.size(); i++) { + Variant variant = enabledVariantList.get(i); + if (variant.format.height > 0 || variantHasExplicitCodecWithPrefix(variant, "avc")) { + definiteVideoVariants.add(variant); + } else if (variantHasExplicitCodecWithPrefix(variant, "mp4a")) { + definiteAudioOnlyVariants.add(variant); + } + } + + if (!definiteVideoVariants.isEmpty()) { + // We've identified some variants as definitely containing video. Assume variants within the + // master playlist are marked consistently, and hence that we have the full set. Filter out + // any other variants, which are likely to be audio only. + enabledVariantList = definiteVideoVariants; + } else if (definiteAudioOnlyVariants.size() < enabledVariantList.size()) { + // We've identified some variants, but not all, as being audio only. Filter them out to leave + // the remaining variants, which are likely to contain video. + enabledVariantList.removeAll(definiteAudioOnlyVariants); + } else { + // Leave the enabled variants unchanged. They're likely either all video or all audio. + } + + if (enabledVariantList.size() > 1) { + Variant[] enabledVariants = new Variant[enabledVariantList.size()]; + enabledVariantList.toArray(enabledVariants); + output.adaptiveTrack(playlist, enabledVariants); + } + for (int i = 0; i < enabledVariantList.size(); i++) { + output.fixedTrack(playlist, enabledVariantList.get(i)); + } + } + + private static boolean variantHasExplicitCodecWithPrefix(Variant variant, String prefix) { + String codecs = variant.format.codecs; + if (TextUtils.isEmpty(codecs)) { + return false; + } + String[] codecArray = codecs.split("(\\s*,\\s*)|(\\s*$)"); + for (int i = 0; i < codecArray.length; i++) { + if (codecArray[i].startsWith(prefix)) { + return true; + } + } + return false; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/hls/HlsChunkSource.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/hls/HlsChunkSource.java new file mode 100644 index 000000000..f17939a9b --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/hls/HlsChunkSource.java @@ -0,0 +1,956 @@ +/* + * 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.messenger.exoplayer.hls; + +import android.os.Handler; +import org.telegram.messenger.exoplayer.BehindLiveWindowException; +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.MediaFormat; +import org.telegram.messenger.exoplayer.chunk.BaseChunkSampleSourceEventListener; +import org.telegram.messenger.exoplayer.chunk.Chunk; +import org.telegram.messenger.exoplayer.chunk.ChunkOperationHolder; +import org.telegram.messenger.exoplayer.chunk.DataChunk; +import org.telegram.messenger.exoplayer.chunk.Format; +import org.telegram.messenger.exoplayer.extractor.Extractor; +import org.telegram.messenger.exoplayer.extractor.mp3.Mp3Extractor; +import org.telegram.messenger.exoplayer.extractor.ts.AdtsExtractor; +import org.telegram.messenger.exoplayer.extractor.ts.PtsTimestampAdjuster; +import org.telegram.messenger.exoplayer.extractor.ts.TsExtractor; +import org.telegram.messenger.exoplayer.upstream.BandwidthMeter; +import org.telegram.messenger.exoplayer.upstream.DataSource; +import org.telegram.messenger.exoplayer.upstream.DataSpec; +import org.telegram.messenger.exoplayer.upstream.HttpDataSource.InvalidResponseCodeException; +import org.telegram.messenger.exoplayer.util.Assertions; +import org.telegram.messenger.exoplayer.util.MimeTypes; +import org.telegram.messenger.exoplayer.util.UriUtil; +import org.telegram.messenger.exoplayer.util.Util; + +import android.net.Uri; +import android.os.SystemClock; +import android.text.TextUtils; +import android.util.Log; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; + +/** + * A temporary test source of HLS chunks. + */ +public class HlsChunkSource implements HlsTrackSelector.Output { + + /** + * Interface definition for a callback to be notified of {@link HlsChunkSource} events. + */ + public interface EventListener extends BaseChunkSampleSourceEventListener { + + /** + * Invoked when a media playlist has been loaded. + * + * @param rawResponse The raw data of the media playlist + */ + void onMediaPlaylistLoadCompleted(byte[] rawResponse); + + } + + /** + * Adaptive switching is disabled. + *

+ * The initially selected variant will be used throughout playback. + */ + public static final int ADAPTIVE_MODE_NONE = 0; + + /** + * Adaptive switches splice overlapping segments of the old and new variants. + *

+ * When performing a switch from one variant to another, overlapping segments will be requested + * from both the old and new variants. These segments will then be spliced together, allowing + * a seamless switch from one variant to another even if keyframes are misaligned or if keyframes + * are not positioned at the start of each segment. + *

+ * Note that where it can be guaranteed that the source content has keyframes positioned at the + * start of each segment, {@link #ADAPTIVE_MODE_ABRUPT} should always be used in preference to + * this mode. + */ + public static final int ADAPTIVE_MODE_SPLICE = 1; + + /** + * Adaptive switches are performed at segment boundaries. + *

+ * For this mode to perform seamless switches, the source content is required to have keyframes + * positioned at the start of each segment. If this is not the case a visual discontinuity may + * be experienced when switching from one variant to another. + *

+ * Note that where it can be guaranteed that the source content does have keyframes positioned at + * the start of each segment, this mode should always be used in preference to + * {@link #ADAPTIVE_MODE_SPLICE} because it requires fetching less data. + */ + public static final int ADAPTIVE_MODE_ABRUPT = 3; + + /** + * The default minimum duration of media that needs to be buffered for a switch to a higher + * quality variant to be considered. + */ + public static final long DEFAULT_MIN_BUFFER_TO_SWITCH_UP_MS = 5000; + + /** + * The default maximum duration of media that needs to be buffered for a switch to a lower + * quality variant to be considered. + */ + public static final long DEFAULT_MAX_BUFFER_TO_SWITCH_DOWN_MS = 20000; + + /** + * The default time for which a media playlist should be blacklisted. + */ + public static final long DEFAULT_PLAYLIST_BLACKLIST_MS = 60000; + + private static final String TAG = "HlsChunkSource"; + private static final String AAC_FILE_EXTENSION = ".aac"; + private static final String MP3_FILE_EXTENSION = ".mp3"; + private static final String VTT_FILE_EXTENSION = ".vtt"; + private static final String WEBVTT_FILE_EXTENSION = ".webvtt"; + private static final float BANDWIDTH_FRACTION = 0.8f; + + private final boolean isMaster; + private final DataSource dataSource; + private final HlsPlaylistParser playlistParser; + private final HlsMasterPlaylist masterPlaylist; + private final HlsTrackSelector trackSelector; + private final BandwidthMeter bandwidthMeter; + private final PtsTimestampAdjusterProvider timestampAdjusterProvider; + private final int adaptiveMode; + private final String baseUri; + private final long minBufferDurationToSwitchUpUs; + private final long maxBufferDurationToSwitchDownUs; + + // TODO: Expose tracks. + private final ArrayList tracks; + + private int selectedTrackIndex; + + // A list of variants considered during playback, ordered by decreasing bandwidth. The following + // three arrays are of the same length and are ordered in the same way (i.e. variantPlaylists[i], + // variantLastPlaylistLoadTimesMs[i] and variantBlacklistTimes[i] all correspond to variants[i]). + private Variant[] variants; + private HlsMediaPlaylist[] variantPlaylists; + private long[] variantLastPlaylistLoadTimesMs; + private long[] variantBlacklistTimes; + + // The index in variants of the currently selected variant. + private int selectedVariantIndex; + + private boolean prepareCalled; + private byte[] scratchSpace; + private boolean live; + private long durationUs; + private IOException fatalError; + + private Uri encryptionKeyUri; + private byte[] encryptionKey; + private String encryptionIvString; + private byte[] encryptionIv; + private final EventListener eventListener; + private final Handler eventHandler; + + /** + * @param isMaster True if this is the master source for the playback. False otherwise. Each + * playback must have exactly one master source, which should be the source providing video + * chunks (or audio chunks for audio only playbacks). + * @param dataSource A {@link DataSource} suitable for loading the media data. + * @param playlist The HLS playlist. + * @param trackSelector Selects tracks to be exposed by this source. + * @param bandwidthMeter Provides an estimate of the currently available bandwidth. + * @param timestampAdjusterProvider A provider of {@link PtsTimestampAdjuster} instances. If + * multiple {@link HlsChunkSource}s are used for a single playback, they should all share the + * same provider. + * @param adaptiveMode The mode for switching from one variant to another. One of + * {@link #ADAPTIVE_MODE_NONE}, {@link #ADAPTIVE_MODE_ABRUPT} and + * {@link #ADAPTIVE_MODE_SPLICE}. + */ + public HlsChunkSource(boolean isMaster, DataSource dataSource, HlsPlaylist playlist, + HlsTrackSelector trackSelector, BandwidthMeter bandwidthMeter, + PtsTimestampAdjusterProvider timestampAdjusterProvider, int adaptiveMode) { + this(isMaster, dataSource, playlist, trackSelector, bandwidthMeter, + timestampAdjusterProvider, adaptiveMode, DEFAULT_MIN_BUFFER_TO_SWITCH_UP_MS, + DEFAULT_MAX_BUFFER_TO_SWITCH_DOWN_MS, null, null); + } + + /** + * @param isMaster True if this is the master source for the playback. False otherwise. Each + * playback must have exactly one master source, which should be the source providing video + * chunks (or audio chunks for audio only playbacks). + * @param dataSource A {@link DataSource} suitable for loading the media data. + * @param playlist The HLS playlist. + * @param trackSelector Selects tracks to be exposed by this source. + * @param bandwidthMeter Provides an estimate of the currently available bandwidth. + * @param timestampAdjusterProvider A provider of {@link PtsTimestampAdjuster} instances. If + * multiple {@link HlsChunkSource}s are used for a single playback, they should all share the + * same provider. + * @param adaptiveMode The mode for switching from one variant to another. One of + * {@link #ADAPTIVE_MODE_NONE}, {@link #ADAPTIVE_MODE_ABRUPT} and + * {@link #ADAPTIVE_MODE_SPLICE}. + * @param minBufferDurationToSwitchUpMs The minimum duration of media that needs to be buffered + * for a switch to a higher quality variant to be considered. + * @param maxBufferDurationToSwitchDownMs The maximum duration of media that needs to be buffered + * for a switch to a lower quality variant to be considered. + */ + public HlsChunkSource(boolean isMaster, DataSource dataSource, HlsPlaylist playlist, + HlsTrackSelector trackSelector, BandwidthMeter bandwidthMeter, + PtsTimestampAdjusterProvider timestampAdjusterProvider, int adaptiveMode, + long minBufferDurationToSwitchUpMs, long maxBufferDurationToSwitchDownMs) { + this(isMaster, dataSource, playlist, trackSelector, bandwidthMeter, + timestampAdjusterProvider, adaptiveMode, minBufferDurationToSwitchUpMs, + maxBufferDurationToSwitchDownMs, null, null); + } + + /** + * @param isMaster True if this is the master source for the playback. False otherwise. Each + * playback must have exactly one master source, which should be the source providing video + * chunks (or audio chunks for audio only playbacks). + * @param dataSource A {@link DataSource} suitable for loading the media data. + * @param playlist The HLS playlist. + * @param trackSelector Selects tracks to be exposed by this source. + * @param bandwidthMeter Provides an estimate of the currently available bandwidth. + * @param timestampAdjusterProvider A provider of {@link PtsTimestampAdjuster} instances. If + * multiple {@link HlsChunkSource}s are used for a single playback, they should all share the + * same provider. + * @param adaptiveMode The mode for switching from one variant to another. One of + * {@link #ADAPTIVE_MODE_NONE}, {@link #ADAPTIVE_MODE_ABRUPT} and + * {@link #ADAPTIVE_MODE_SPLICE}. + * @param minBufferDurationToSwitchUpMs The minimum duration of media that needs to be buffered + * for a switch to a higher quality variant to be considered. + * @param maxBufferDurationToSwitchDownMs The maximum duration of media that needs to be buffered + * for a switch to a lower quality variant to be considered. + * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be + * null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + */ + public HlsChunkSource(boolean isMaster, DataSource dataSource, HlsPlaylist playlist, + HlsTrackSelector trackSelector, BandwidthMeter bandwidthMeter, + PtsTimestampAdjusterProvider timestampAdjusterProvider, int adaptiveMode, + long minBufferDurationToSwitchUpMs, long maxBufferDurationToSwitchDownMs, + Handler eventHandler, EventListener eventListener) { + this.isMaster = isMaster; + this.dataSource = dataSource; + this.trackSelector = trackSelector; + this.bandwidthMeter = bandwidthMeter; + this.timestampAdjusterProvider = timestampAdjusterProvider; + this.adaptiveMode = adaptiveMode; + this.eventListener = eventListener; + this.eventHandler = eventHandler; + minBufferDurationToSwitchUpUs = minBufferDurationToSwitchUpMs * 1000; + maxBufferDurationToSwitchDownUs = maxBufferDurationToSwitchDownMs * 1000; + baseUri = playlist.baseUri; + playlistParser = new HlsPlaylistParser(); + tracks = new ArrayList<>(); + + if (playlist.type == HlsPlaylist.TYPE_MASTER) { + masterPlaylist = (HlsMasterPlaylist) playlist; + } else { + Format format = new Format("0", MimeTypes.APPLICATION_M3U8, -1, -1, -1, -1, -1, -1, null, + null); + List variants = new ArrayList<>(); + variants.add(new Variant(baseUri, format)); + masterPlaylist = new HlsMasterPlaylist(baseUri, variants, + Collections.emptyList(), Collections.emptyList(), null, null); + } + } + + /** + * If the source is currently having difficulty providing chunks, then this method throws the + * underlying error. Otherwise does nothing. + * + * @throws IOException The underlying error. + */ + public void maybeThrowError() throws IOException { + if (fatalError != null) { + throw fatalError; + } + } + + /** + * Prepares the source. + * + * @return True if the source was prepared, false otherwise. + */ + public boolean prepare() { + if (!prepareCalled) { + prepareCalled = true; + try { + trackSelector.selectTracks(masterPlaylist, this); + selectTrack(0); + } catch (IOException e) { + fatalError = e; + } + } + return fatalError == null; + } + + /** + * Returns whether this is a live playback. + *

+ * This method should only be called after the source has been prepared. + * + * @return True if this is a live playback. False otherwise. + */ + public boolean isLive() { + return live; + } + + /** + * Returns the duration of the source, or {@link C#UNKNOWN_TIME_US} if the duration is unknown. + *

+ * This method should only be called after the source has been prepared. + * + * @return The number of tracks. + */ + public long getDurationUs() { + return durationUs; + } + + /** + * Returns the number of tracks exposed by the source. + *

+ * This method should only be called after the source has been prepared. + * + * @return The number of tracks. + */ + public int getTrackCount() { + return tracks.size(); + } + + /** + * Returns the variant corresponding to the fixed track at the specified index, or null if the + * track at the specified index is adaptive. + *

+ * This method should only be called after the source has been prepared. + * + * @param index The track index. + * @return The variant corresponding to the fixed track, or null if the track is adaptive. + */ + public Variant getFixedTrackVariant(int index) { + Variant[] variants = tracks.get(index).variants; + return variants.length == 1 ? variants[0] : null; + } + + /** + * Returns the language of the audio muxed into variants, or null if unknown. + * + * @return The language of the audio muxed into variants, or null if unknown. + */ + public String getMuxedAudioLanguage() { + return masterPlaylist.muxedAudioLanguage; + } + + /** + * Returns the language of the captions muxed into variants, or null if unknown. + * + * @return The language of the captions muxed into variants, or null if unknown. + */ + public String getMuxedCaptionLanguage() { + return masterPlaylist.muxedCaptionLanguage; + } + + /** + * Returns the currently selected track index. + *

+ * This method should only be called after the source has been prepared. + * + * @return The currently selected track index. + */ + public int getSelectedTrackIndex() { + return selectedTrackIndex; + } + + /** + * Selects a track for use. + *

+ * This method should only be called after the source has been prepared. + * + * @param index The track index. + */ + public void selectTrack(int index) { + selectedTrackIndex = index; + ExposedTrack selectedTrack = tracks.get(selectedTrackIndex); + selectedVariantIndex = selectedTrack.defaultVariantIndex; + variants = selectedTrack.variants; + variantPlaylists = new HlsMediaPlaylist[variants.length]; + variantLastPlaylistLoadTimesMs = new long[variants.length]; + variantBlacklistTimes = new long[variants.length]; + } + + /** + * Notifies the source that a seek has occurred. + *

+ * This method should only be called after the source has been prepared. + */ + public void seek() { + if (isMaster) { + timestampAdjusterProvider.reset(); + } + } + + /** + * Resets the source. + *

+ * This method should only be called after the source has been prepared. + */ + public void reset() { + fatalError = null; + } + + /** + * Updates the provided {@link ChunkOperationHolder} to contain the next operation that should + * be performed by the calling {@link HlsSampleSource}. + * + * @param previousTsChunk The previously loaded chunk that the next chunk should follow. + * @param playbackPositionUs The current playback position. If previousTsChunk is null then this + * parameter is the position from which playback is expected to start (or restart) and hence + * should be interpreted as a seek position. + * @param out The holder to populate with the result. {@link ChunkOperationHolder#queueSize} is + * unused. + */ + public void getChunkOperation(TsChunk previousTsChunk, long playbackPositionUs, + ChunkOperationHolder out) { + int nextVariantIndex; + boolean switchingVariantSpliced; + if (adaptiveMode == ADAPTIVE_MODE_NONE) { + nextVariantIndex = selectedVariantIndex; + switchingVariantSpliced = false; + } else { + nextVariantIndex = getNextVariantIndex(previousTsChunk, playbackPositionUs); + switchingVariantSpliced = previousTsChunk != null + && !variants[nextVariantIndex].format.equals(previousTsChunk.format) + && adaptiveMode == ADAPTIVE_MODE_SPLICE; + } + + HlsMediaPlaylist mediaPlaylist = variantPlaylists[nextVariantIndex]; + if (mediaPlaylist == null) { + // We don't have the media playlist for the next variant. Request it now. + out.chunk = newMediaPlaylistChunk(nextVariantIndex); + return; + } + + selectedVariantIndex = nextVariantIndex; + int chunkMediaSequence = 0; + if (live) { + if (previousTsChunk == null) { + chunkMediaSequence = getLiveStartChunkMediaSequence(nextVariantIndex); + } else { + chunkMediaSequence = switchingVariantSpliced + ? previousTsChunk.chunkIndex : previousTsChunk.chunkIndex + 1; + if (chunkMediaSequence < mediaPlaylist.mediaSequence) { + fatalError = new BehindLiveWindowException(); + return; + } + } + } else { + // Not live. + if (previousTsChunk == null) { + chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments, playbackPositionUs, + true, true) + mediaPlaylist.mediaSequence; + } else { + chunkMediaSequence = switchingVariantSpliced + ? previousTsChunk.chunkIndex : previousTsChunk.chunkIndex + 1; + } + } + + int chunkIndex = chunkMediaSequence - mediaPlaylist.mediaSequence; + if (chunkIndex >= mediaPlaylist.segments.size()) { + if (!mediaPlaylist.live) { + out.endOfStream = true; + } else if (shouldRerequestLiveMediaPlaylist(nextVariantIndex)) { + out.chunk = newMediaPlaylistChunk(nextVariantIndex); + } + return; + } + + HlsMediaPlaylist.Segment segment = mediaPlaylist.segments.get(chunkIndex); + Uri chunkUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.url); + + // Check if encryption is specified. + if (segment.isEncrypted) { + Uri keyUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.encryptionKeyUri); + if (!keyUri.equals(encryptionKeyUri)) { + // Encryption is specified and the key has changed. + out.chunk = newEncryptionKeyChunk(keyUri, segment.encryptionIV, selectedVariantIndex); + return; + } + if (!Util.areEqual(segment.encryptionIV, encryptionIvString)) { + setEncryptionData(keyUri, segment.encryptionIV, encryptionKey); + } + } else { + clearEncryptionData(); + } + + // Configure the data source and spec for the chunk. + DataSpec dataSpec = new DataSpec(chunkUri, segment.byterangeOffset, segment.byterangeLength, + null); + + // Compute start and end times, and the sequence number of the next chunk. + long startTimeUs; + if (live) { + if (previousTsChunk == null) { + startTimeUs = 0; + } else if (switchingVariantSpliced) { + startTimeUs = previousTsChunk.startTimeUs; + } else { + startTimeUs = previousTsChunk.endTimeUs; + } + } else /* Not live */ { + startTimeUs = segment.startTimeUs; + } + long endTimeUs = startTimeUs + (long) (segment.durationSecs * C.MICROS_PER_SECOND); + int trigger = Chunk.TRIGGER_UNSPECIFIED; + Format format = variants[selectedVariantIndex].format; + + // Configure the extractor that will read the chunk. + HlsExtractorWrapper extractorWrapper; + String lastPathSegment = chunkUri.getLastPathSegment(); + if (lastPathSegment.endsWith(AAC_FILE_EXTENSION)) { + // TODO: Inject a timestamp adjuster and use it along with ID3 PRIV tag values with owner + // identifier com.apple.streaming.transportStreamTimestamp. This may also apply to the MP3 + // case below. + Extractor extractor = new AdtsExtractor(startTimeUs); + extractorWrapper = new HlsExtractorWrapper(trigger, format, startTimeUs, extractor, + switchingVariantSpliced, MediaFormat.NO_VALUE, MediaFormat.NO_VALUE); + } else if (lastPathSegment.endsWith(MP3_FILE_EXTENSION)) { + Extractor extractor = new Mp3Extractor(startTimeUs); + extractorWrapper = new HlsExtractorWrapper(trigger, format, startTimeUs, extractor, + switchingVariantSpliced, MediaFormat.NO_VALUE, MediaFormat.NO_VALUE); + } else if (lastPathSegment.endsWith(WEBVTT_FILE_EXTENSION) + || lastPathSegment.endsWith(VTT_FILE_EXTENSION)) { + PtsTimestampAdjuster timestampAdjuster = timestampAdjusterProvider.getAdjuster(isMaster, + segment.discontinuitySequenceNumber, startTimeUs); + if (timestampAdjuster == null) { + // The master source has yet to instantiate an adjuster for the discontinuity sequence. + // TODO: There's probably an edge case if the master starts playback at a chunk belonging to + // a discontinuity sequence greater than the one that this source is trying to start at. + return; + } + Extractor extractor = new WebvttExtractor(timestampAdjuster); + extractorWrapper = new HlsExtractorWrapper(trigger, format, startTimeUs, extractor, + switchingVariantSpliced, MediaFormat.NO_VALUE, MediaFormat.NO_VALUE); + } else if (previousTsChunk == null + || previousTsChunk.discontinuitySequenceNumber != segment.discontinuitySequenceNumber + || !format.equals(previousTsChunk.format)) { + // MPEG-2 TS segments, but we need a new extractor. + PtsTimestampAdjuster timestampAdjuster = timestampAdjusterProvider.getAdjuster(isMaster, + segment.discontinuitySequenceNumber, startTimeUs); + if (timestampAdjuster == null) { + // The master source has yet to instantiate an adjuster for the discontinuity sequence. + return; + } + int workaroundFlags = 0; + String codecs = format.codecs; + if (!TextUtils.isEmpty(codecs)) { + // Sometimes AAC and H264 streams are declared in TS chunks even though they don't really + // exist. If we know from the codec attribute that they don't exist, then we can explicitly + // ignore them even if they're declared. + if (MimeTypes.getAudioMediaMimeType(codecs) != MimeTypes.AUDIO_AAC) { + workaroundFlags |= TsExtractor.WORKAROUND_IGNORE_AAC_STREAM; + } + if (MimeTypes.getVideoMediaMimeType(codecs) != MimeTypes.VIDEO_H264) { + workaroundFlags |= TsExtractor.WORKAROUND_IGNORE_H264_STREAM; + } + } + Extractor extractor = new TsExtractor(timestampAdjuster, workaroundFlags); + ExposedTrack selectedTrack = tracks.get(selectedTrackIndex); + extractorWrapper = new HlsExtractorWrapper(trigger, format, startTimeUs, extractor, + switchingVariantSpliced, selectedTrack.adaptiveMaxWidth, selectedTrack.adaptiveMaxHeight); + } else { + // MPEG-2 TS segments, and we need to continue using the same extractor. + extractorWrapper = previousTsChunk.extractorWrapper; + } + + out.chunk = new TsChunk(dataSource, dataSpec, trigger, format, startTimeUs, endTimeUs, + chunkMediaSequence, segment.discontinuitySequenceNumber, extractorWrapper, encryptionKey, + encryptionIv); + } + + /** + * Invoked when the {@link HlsSampleSource} has finished loading a chunk obtained from this + * source. + * + * @param chunk The chunk whose load has been completed. + */ + public void onChunkLoadCompleted(Chunk chunk) { + if (chunk instanceof MediaPlaylistChunk) { + MediaPlaylistChunk mediaPlaylistChunk = (MediaPlaylistChunk) chunk; + scratchSpace = mediaPlaylistChunk.getDataHolder(); + setMediaPlaylist(mediaPlaylistChunk.variantIndex, mediaPlaylistChunk.getResult()); + if (eventHandler != null && eventListener != null) { + final byte[] rawResponse = mediaPlaylistChunk.getRawResponse(); + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onMediaPlaylistLoadCompleted(rawResponse); + } + }); + } + } else if (chunk instanceof EncryptionKeyChunk) { + EncryptionKeyChunk encryptionKeyChunk = (EncryptionKeyChunk) chunk; + scratchSpace = encryptionKeyChunk.getDataHolder(); + setEncryptionData(encryptionKeyChunk.dataSpec.uri, encryptionKeyChunk.iv, + encryptionKeyChunk.getResult()); + } + } + + /** + * Invoked when the {@link HlsSampleSource} encounters an error loading a chunk obtained from + * this source. + * + * @param chunk The chunk whose load encountered the error. + * @param e The error. + * @return True if the error was handled by the source. False otherwise. + */ + public boolean onChunkLoadError(Chunk chunk, IOException e) { + if (chunk.bytesLoaded() == 0 + && (chunk instanceof TsChunk || chunk instanceof MediaPlaylistChunk + || chunk instanceof EncryptionKeyChunk) + && (e instanceof InvalidResponseCodeException)) { + InvalidResponseCodeException responseCodeException = (InvalidResponseCodeException) e; + int responseCode = responseCodeException.responseCode; + if (responseCode == 404 || responseCode == 410) { + int variantIndex; + if (chunk instanceof TsChunk) { + TsChunk tsChunk = (TsChunk) chunk; + variantIndex = getVariantIndex(tsChunk.format); + } else if (chunk instanceof MediaPlaylistChunk) { + MediaPlaylistChunk playlistChunk = (MediaPlaylistChunk) chunk; + variantIndex = playlistChunk.variantIndex; + } else { + EncryptionKeyChunk encryptionChunk = (EncryptionKeyChunk) chunk; + variantIndex = encryptionChunk.variantIndex; + } + boolean alreadyBlacklisted = variantBlacklistTimes[variantIndex] != 0; + variantBlacklistTimes[variantIndex] = SystemClock.elapsedRealtime(); + if (alreadyBlacklisted) { + // The playlist was already blacklisted. + Log.w(TAG, "Already blacklisted variant (" + responseCode + "): " + + chunk.dataSpec.uri); + return false; + } else if (!allVariantsBlacklisted()) { + // We've handled the 404/410 by blacklisting the variant. + Log.w(TAG, "Blacklisted variant (" + responseCode + "): " + + chunk.dataSpec.uri); + return true; + } else { + // This was the last non-blacklisted playlist. Don't blacklist it. + Log.w(TAG, "Final variant not blacklisted (" + responseCode + "): " + + chunk.dataSpec.uri); + variantBlacklistTimes[variantIndex] = 0; + return false; + } + } + } + return false; + } + + // HlsTrackSelector.Output implementation. + + @Override + public void adaptiveTrack(HlsMasterPlaylist playlist, Variant[] variants) { + Arrays.sort(variants, new Comparator() { + private final Comparator formatComparator = + new Format.DecreasingBandwidthComparator(); + @Override + public int compare(Variant first, Variant second) { + return formatComparator.compare(first.format, second.format); + } + }); + + int defaultVariantIndex = computeDefaultVariantIndex(playlist, variants, bandwidthMeter); + int maxWidth = -1; + int maxHeight = -1; + + for (int i = 0; i < variants.length; i++) { + Format variantFormat = variants[i].format; + maxWidth = Math.max(variantFormat.width, maxWidth); + maxHeight = Math.max(variantFormat.height, maxHeight); + } + // TODO: We should allow the default values to be passed through the constructor. + // TODO: Print a warning if resolution tags are omitted. + maxWidth = maxWidth > 0 ? maxWidth : 1920; + maxHeight = maxHeight > 0 ? maxHeight : 1080; + tracks.add(new ExposedTrack(variants, defaultVariantIndex, maxWidth, maxHeight)); + } + + @Override + public void fixedTrack(HlsMasterPlaylist playlist, Variant variant) { + tracks.add(new ExposedTrack(variant)); + } + + protected int computeDefaultVariantIndex(HlsMasterPlaylist playlist, Variant[] variants, + BandwidthMeter bandwidthMeter) { + int defaultVariantIndex = 0; + int minOriginalVariantIndex = Integer.MAX_VALUE; + + for (int i = 0; i < variants.length; i++) { + int originalVariantIndex = playlist.variants.indexOf(variants[i]); + if (originalVariantIndex < minOriginalVariantIndex) { + minOriginalVariantIndex = originalVariantIndex; + defaultVariantIndex = i; + } + } + + return defaultVariantIndex; + } + + // Private methods. + + private int getNextVariantIndex(TsChunk previousTsChunk, long playbackPositionUs) { + clearStaleBlacklistedVariants(); + long bitrateEstimate = bandwidthMeter.getBitrateEstimate(); + if (variantBlacklistTimes[selectedVariantIndex] != 0) { + // The current variant has been blacklisted, so we have no choice but to re-evaluate. + return getVariantIndexForBandwidth(bitrateEstimate); + } + if (previousTsChunk == null) { + // Don't consider switching if we don't have a previous chunk. + return selectedVariantIndex; + } + if (bitrateEstimate == BandwidthMeter.NO_ESTIMATE) { + // Don't consider switching if we don't have a bandwidth estimate. + return selectedVariantIndex; + } + int idealIndex = getVariantIndexForBandwidth(bitrateEstimate); + if (idealIndex == selectedVariantIndex) { + // We're already using the ideal variant. + return selectedVariantIndex; + } + // We're not using the ideal variant for the available bandwidth, but only switch if the + // conditions are appropriate. + long bufferedPositionUs = adaptiveMode == ADAPTIVE_MODE_SPLICE ? previousTsChunk.startTimeUs + : previousTsChunk.endTimeUs; + long bufferedUs = bufferedPositionUs - playbackPositionUs; + if (variantBlacklistTimes[selectedVariantIndex] != 0 + || (idealIndex > selectedVariantIndex && bufferedUs < maxBufferDurationToSwitchDownUs) + || (idealIndex < selectedVariantIndex && bufferedUs > minBufferDurationToSwitchUpUs)) { + // Switch variant. + return idealIndex; + } + // Stick with the current variant for now. + return selectedVariantIndex; + } + + private int getVariantIndexForBandwidth(long bitrateEstimate) { + if (bitrateEstimate == BandwidthMeter.NO_ESTIMATE) { + // Select the lowest quality. + bitrateEstimate = 0; + } + int effectiveBitrate = (int) (bitrateEstimate * BANDWIDTH_FRACTION); + int lowestQualityEnabledVariantIndex = -1; + for (int i = 0; i < variants.length; i++) { + if (variantBlacklistTimes[i] == 0) { + if (variants[i].format.bitrate <= effectiveBitrate) { + return i; + } + lowestQualityEnabledVariantIndex = i; + } + } + // At least one variant should always be enabled. + Assertions.checkState(lowestQualityEnabledVariantIndex != -1); + return lowestQualityEnabledVariantIndex; + } + + private boolean shouldRerequestLiveMediaPlaylist(int nextVariantIndex) { + // Don't re-request media playlist more often than one-half of the target duration. + HlsMediaPlaylist mediaPlaylist = variantPlaylists[nextVariantIndex]; + long timeSinceLastMediaPlaylistLoadMs = + SystemClock.elapsedRealtime() - variantLastPlaylistLoadTimesMs[nextVariantIndex]; + return timeSinceLastMediaPlaylistLoadMs >= (mediaPlaylist.targetDurationSecs * 1000) / 2; + } + + private int getLiveStartChunkMediaSequence(int variantIndex) { + // For live start playback from the third chunk from the end. + HlsMediaPlaylist mediaPlaylist = variantPlaylists[variantIndex]; + int chunkIndex = mediaPlaylist.segments.size() > 3 ? mediaPlaylist.segments.size() - 3 : 0; + return chunkIndex + mediaPlaylist.mediaSequence; + } + + private MediaPlaylistChunk newMediaPlaylistChunk(int variantIndex) { + Uri mediaPlaylistUri = UriUtil.resolveToUri(baseUri, variants[variantIndex].url); + DataSpec dataSpec = new DataSpec(mediaPlaylistUri, 0, C.LENGTH_UNBOUNDED, null, + DataSpec.FLAG_ALLOW_GZIP); + return new MediaPlaylistChunk(dataSource, dataSpec, scratchSpace, playlistParser, variantIndex, + mediaPlaylistUri.toString()); + } + + private EncryptionKeyChunk newEncryptionKeyChunk(Uri keyUri, String iv, int variantIndex) { + DataSpec dataSpec = new DataSpec(keyUri, 0, C.LENGTH_UNBOUNDED, null, DataSpec.FLAG_ALLOW_GZIP); + return new EncryptionKeyChunk(dataSource, dataSpec, scratchSpace, iv, variantIndex); + } + + private void setEncryptionData(Uri keyUri, String iv, byte[] secretKey) { + String trimmedIv; + if (iv.toLowerCase(Locale.getDefault()).startsWith("0x")) { + trimmedIv = iv.substring(2); + } else { + trimmedIv = iv; + } + + byte[] ivData = new BigInteger(trimmedIv, 16).toByteArray(); + byte[] ivDataWithPadding = new byte[16]; + int offset = ivData.length > 16 ? ivData.length - 16 : 0; + System.arraycopy(ivData, offset, ivDataWithPadding, ivDataWithPadding.length - ivData.length + + offset, ivData.length - offset); + + encryptionKeyUri = keyUri; + encryptionKey = secretKey; + encryptionIvString = iv; + encryptionIv = ivDataWithPadding; + } + + private void clearEncryptionData() { + encryptionKeyUri = null; + encryptionKey = null; + encryptionIvString = null; + encryptionIv = null; + } + + private void setMediaPlaylist(int variantIndex, HlsMediaPlaylist mediaPlaylist) { + variantLastPlaylistLoadTimesMs[variantIndex] = SystemClock.elapsedRealtime(); + variantPlaylists[variantIndex] = mediaPlaylist; + live |= mediaPlaylist.live; + durationUs = live ? C.UNKNOWN_TIME_US : mediaPlaylist.durationUs; + } + + private boolean allVariantsBlacklisted() { + for (int i = 0; i < variantBlacklistTimes.length; i++) { + if (variantBlacklistTimes[i] == 0) { + return false; + } + } + return true; + } + + private void clearStaleBlacklistedVariants() { + long currentTime = SystemClock.elapsedRealtime(); + for (int i = 0; i < variantBlacklistTimes.length; i++) { + if (variantBlacklistTimes[i] != 0 + && currentTime - variantBlacklistTimes[i] > DEFAULT_PLAYLIST_BLACKLIST_MS) { + variantBlacklistTimes[i] = 0; + } + } + } + + private int getVariantIndex(Format format) { + for (int i = 0; i < variants.length; i++) { + if (variants[i].format.equals(format)) { + return i; + } + } + // Should never happen. + throw new IllegalStateException("Invalid format: " + format); + } + + // Private classes. + + private static final class ExposedTrack { + + private final Variant[] variants; + private final int defaultVariantIndex; + + private final int adaptiveMaxWidth; + private final int adaptiveMaxHeight; + + public ExposedTrack(Variant fixedVariant) { + this.variants = new Variant[] {fixedVariant}; + this.defaultVariantIndex = 0; + this.adaptiveMaxWidth = MediaFormat.NO_VALUE; + this.adaptiveMaxHeight = MediaFormat.NO_VALUE; + } + + public ExposedTrack(Variant[] adaptiveVariants, int defaultVariantIndex, int maxWidth, + int maxHeight) { + this.variants = adaptiveVariants; + this.defaultVariantIndex = defaultVariantIndex; + this.adaptiveMaxWidth = maxWidth; + this.adaptiveMaxHeight = maxHeight; + } + + } + + private static final class MediaPlaylistChunk extends DataChunk { + + public final int variantIndex; + + private final HlsPlaylistParser playlistParser; + private final String playlistUrl; + + private byte[] rawResponse; + private HlsMediaPlaylist result; + + public MediaPlaylistChunk(DataSource dataSource, DataSpec dataSpec, byte[] scratchSpace, + HlsPlaylistParser playlistParser, int variantIndex, String playlistUrl) { + super(dataSource, dataSpec, Chunk.TYPE_MANIFEST, Chunk.TRIGGER_UNSPECIFIED, null, + Chunk.NO_PARENT_ID, scratchSpace); + this.variantIndex = variantIndex; + this.playlistParser = playlistParser; + this.playlistUrl = playlistUrl; + } + + @Override + protected void consume(byte[] data, int limit) throws IOException { + rawResponse = Arrays.copyOf(data, limit); + result = (HlsMediaPlaylist) playlistParser.parse(playlistUrl, + new ByteArrayInputStream(rawResponse)); + } + + public byte[] getRawResponse() { + return rawResponse; + } + + public HlsMediaPlaylist getResult() { + return result; + } + + } + + private static final class EncryptionKeyChunk extends DataChunk { + + public final String iv; + public final int variantIndex; + + private byte[] result; + + public EncryptionKeyChunk(DataSource dataSource, DataSpec dataSpec, byte[] scratchSpace, + String iv, int variantIndex) { + super(dataSource, dataSpec, Chunk.TYPE_DRM, Chunk.TRIGGER_UNSPECIFIED, null, + Chunk.NO_PARENT_ID, scratchSpace); + this.iv = iv; + this.variantIndex = variantIndex; + } + + @Override + protected void consume(byte[] data, int limit) throws IOException { + result = Arrays.copyOf(data, limit); + } + + public byte[] getResult() { + return result; + } + + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/hls/HlsExtractorWrapper.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/hls/HlsExtractorWrapper.java new file mode 100644 index 000000000..96cfc26e9 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/hls/HlsExtractorWrapper.java @@ -0,0 +1,269 @@ +/* + * 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.messenger.exoplayer.hls; + +import org.telegram.messenger.exoplayer.MediaFormat; +import org.telegram.messenger.exoplayer.SampleHolder; +import org.telegram.messenger.exoplayer.chunk.Format; +import org.telegram.messenger.exoplayer.drm.DrmInitData; +import org.telegram.messenger.exoplayer.extractor.DefaultTrackOutput; +import org.telegram.messenger.exoplayer.extractor.Extractor; +import org.telegram.messenger.exoplayer.extractor.ExtractorInput; +import org.telegram.messenger.exoplayer.extractor.ExtractorOutput; +import org.telegram.messenger.exoplayer.extractor.SeekMap; +import org.telegram.messenger.exoplayer.extractor.TrackOutput; +import org.telegram.messenger.exoplayer.upstream.Allocator; +import org.telegram.messenger.exoplayer.util.Assertions; +import org.telegram.messenger.exoplayer.util.MimeTypes; + +import android.util.SparseArray; + +import java.io.IOException; + +/** + * Wraps a {@link Extractor}, adding functionality to enable reading of the extracted samples. + */ +public final class HlsExtractorWrapper implements ExtractorOutput { + + public final int trigger; + public final Format format; + public final long startTimeUs; + + private final Extractor extractor; + private final SparseArray sampleQueues; + private final boolean shouldSpliceIn; + private final int adaptiveMaxWidth; + private final int adaptiveMaxHeight; + + private MediaFormat[] sampleQueueFormats; + private Allocator allocator; + + private volatile boolean tracksBuilt; + + // Accessed only by the consuming thread. + private boolean prepared; + private boolean spliceConfigured; + + public HlsExtractorWrapper(int trigger, Format format, long startTimeUs, Extractor extractor, + boolean shouldSpliceIn, int adaptiveMaxWidth, int adaptiveMaxHeight) { + this.trigger = trigger; + this.format = format; + this.startTimeUs = startTimeUs; + this.extractor = extractor; + this.shouldSpliceIn = shouldSpliceIn; + this.adaptiveMaxWidth = adaptiveMaxWidth; + this.adaptiveMaxHeight = adaptiveMaxHeight; + sampleQueues = new SparseArray<>(); + } + + /** + * Initializes the wrapper for use. + * + * @param allocator An allocator for obtaining allocations into which extracted data is written. + */ + public void init(Allocator allocator) { + this.allocator = allocator; + extractor.init(this); + } + + /** + * Whether the extractor is prepared. + * + * @return True if the extractor is prepared. False otherwise. + */ + public boolean isPrepared() { + if (!prepared && tracksBuilt) { + for (int i = 0; i < sampleQueues.size(); i++) { + if (!sampleQueues.valueAt(i).hasFormat()) { + return false; + } + } + prepared = true; + sampleQueueFormats = new MediaFormat[sampleQueues.size()]; + for (int i = 0; i < sampleQueueFormats.length; i++) { + MediaFormat format = sampleQueues.valueAt(i).getFormat(); + if (MimeTypes.isVideo(format.mimeType) && (adaptiveMaxWidth != MediaFormat.NO_VALUE + || adaptiveMaxHeight != MediaFormat.NO_VALUE)) { + format = format.copyWithMaxVideoDimensions(adaptiveMaxWidth, adaptiveMaxHeight); + } + sampleQueueFormats[i] = format; + } + } + return prepared; + } + + /** + * Clears queues for all tracks, returning all allocations to the allocator. + */ + public void clear() { + for (int i = 0; i < sampleQueues.size(); i++) { + sampleQueues.valueAt(i).clear(); + } + } + + /** + * Gets the largest timestamp of any sample parsed by the extractor. + * + * @return The largest timestamp, or {@link Long#MIN_VALUE} if no samples have been parsed. + */ + public long getLargestParsedTimestampUs() { + long largestParsedTimestampUs = Long.MIN_VALUE; + for (int i = 0; i < sampleQueues.size(); i++) { + largestParsedTimestampUs = Math.max(largestParsedTimestampUs, + sampleQueues.valueAt(i).getLargestParsedTimestampUs()); + } + return largestParsedTimestampUs; + } + + /** + * Attempts to configure a splice from this extractor to the next. + *

+ * The splice is performed such that for each track the samples read from the next extractor + * start with a keyframe, and continue from where the samples read from this extractor finish. + * A successful splice may discard samples from either or both extractors. + *

+ * Splice configuration may fail if the next extractor is not yet in a state that allows the + * splice to be performed. Calling this method is a noop if the splice has already been + * configured. Hence this method should be called repeatedly during the window within which a + * splice can be performed. + *

+ * This method must only be called after the extractor has been prepared. + * + * @param nextExtractor The extractor being spliced to. + */ + public final void configureSpliceTo(HlsExtractorWrapper nextExtractor) { + Assertions.checkState(isPrepared()); + if (spliceConfigured || !nextExtractor.shouldSpliceIn || !nextExtractor.isPrepared()) { + // The splice is already configured, or the next extractor doesn't want to be spliced in, or + // the next extractor isn't ready to be spliced in. + return; + } + boolean spliceConfigured = true; + int trackCount = getTrackCount(); + for (int i = 0; i < trackCount; i++) { + DefaultTrackOutput currentSampleQueue = sampleQueues.valueAt(i); + DefaultTrackOutput nextSampleQueue = nextExtractor.sampleQueues.valueAt(i); + spliceConfigured &= currentSampleQueue.configureSpliceTo(nextSampleQueue); + } + this.spliceConfigured = spliceConfigured; + return; + } + + /** + * Gets the number of available tracks. + *

+ * This method must only be called after the extractor has been prepared. + * + * @return The number of available tracks. + */ + public int getTrackCount() { + Assertions.checkState(isPrepared()); + return sampleQueues.size(); + } + + /** + * Gets the {@link MediaFormat} of the specified track. + *

+ * This method must only be called after the extractor has been prepared. + * + * @param track The track index. + * @return The corresponding format. + */ + public MediaFormat getMediaFormat(int track) { + Assertions.checkState(isPrepared()); + return sampleQueueFormats[track]; + } + + /** + * Gets the next sample for the specified track. + *

+ * This method must only be called after the extractor has been prepared. + * + * @param track The track from which to read. + * @param holder A {@link SampleHolder} into which the sample should be read. + * @return True if a sample was read. False otherwise. + */ + public boolean getSample(int track, SampleHolder holder) { + Assertions.checkState(isPrepared()); + return sampleQueues.valueAt(track).getSample(holder); + } + + /** + * Discards samples for the specified track up to the specified time. + *

+ * This method must only be called after the extractor has been prepared. + * + * @param track The track from which samples should be discarded. + * @param timeUs The time up to which samples should be discarded, in microseconds. + */ + public void discardUntil(int track, long timeUs) { + Assertions.checkState(isPrepared()); + sampleQueues.valueAt(track).discardUntil(timeUs); + } + + /** + * Whether samples are available for reading from {@link #getSample(int, SampleHolder)} for the + * specified track. + *

+ * This method must only be called after the extractor has been prepared. + * + * @return True if samples are available for reading from {@link #getSample(int, SampleHolder)} + * for the specified track. False otherwise. + */ + public boolean hasSamples(int track) { + Assertions.checkState(isPrepared()); + return !sampleQueues.valueAt(track).isEmpty(); + } + + /** + * Reads from the provided {@link ExtractorInput}. + * + * @param input The {@link ExtractorInput} from which to read. + * @return One of {@link Extractor#RESULT_CONTINUE} and {@link Extractor#RESULT_END_OF_INPUT}. + * @throws IOException If an error occurred reading from the source. + * @throws InterruptedException If the thread was interrupted. + */ + public int read(ExtractorInput input) throws IOException, InterruptedException { + int result = extractor.read(input, null); + Assertions.checkState(result != Extractor.RESULT_SEEK); + return result; + } + + // ExtractorOutput implementation. + + @Override + public TrackOutput track(int id) { + DefaultTrackOutput sampleQueue = new DefaultTrackOutput(allocator); + sampleQueues.put(id, sampleQueue); + return sampleQueue; + } + + @Override + public void endTracks() { + this.tracksBuilt = true; + } + + @Override + public void seekMap(SeekMap seekMap) { + // Do nothing. + } + + @Override + public void drmInitData(DrmInitData drmInit) { + // Do nothing. + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/hls/HlsMasterPlaylist.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/hls/HlsMasterPlaylist.java new file mode 100644 index 000000000..b337e65fa --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/hls/HlsMasterPlaylist.java @@ -0,0 +1,44 @@ +/* + * 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.messenger.exoplayer.hls; + +import java.util.Collections; +import java.util.List; + +/** + * Represents an HLS master playlist. + */ +public final class HlsMasterPlaylist extends HlsPlaylist { + + public final List variants; + public final List audios; + public final List subtitles; + + public final String muxedAudioLanguage; + public final String muxedCaptionLanguage; + + public HlsMasterPlaylist(String baseUri, List variants, + List audios, List subtitles, String muxedAudioLanguage, + String muxedCaptionLanguage) { + super(baseUri, HlsPlaylist.TYPE_MASTER); + this.variants = Collections.unmodifiableList(variants); + this.audios = Collections.unmodifiableList(audios); + this.subtitles = Collections.unmodifiableList(subtitles); + this.muxedAudioLanguage = muxedAudioLanguage; + this.muxedCaptionLanguage = muxedCaptionLanguage; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/hls/HlsMediaPlaylist.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/hls/HlsMediaPlaylist.java new file mode 100644 index 000000000..9a8f01e49 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/hls/HlsMediaPlaylist.java @@ -0,0 +1,89 @@ +/* + * 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.messenger.exoplayer.hls; + +import org.telegram.messenger.exoplayer.C; + +import java.util.List; + +/** + * Represents an HLS media playlist. + */ +public final class HlsMediaPlaylist extends HlsPlaylist { + + /** + * Media segment reference. + */ + public static final class Segment implements Comparable { + + public final String url; + public final double durationSecs; + public final int discontinuitySequenceNumber; + public final long startTimeUs; + public final boolean isEncrypted; + public final String encryptionKeyUri; + public final String encryptionIV; + public final long byterangeOffset; + public final long byterangeLength; + + public Segment(String uri, double durationSecs, int discontinuitySequenceNumber, + long startTimeUs, boolean isEncrypted, String encryptionKeyUri, String encryptionIV, + long byterangeOffset, long byterangeLength) { + this.url = uri; + this.durationSecs = durationSecs; + this.discontinuitySequenceNumber = discontinuitySequenceNumber; + this.startTimeUs = startTimeUs; + this.isEncrypted = isEncrypted; + this.encryptionKeyUri = encryptionKeyUri; + this.encryptionIV = encryptionIV; + this.byterangeOffset = byterangeOffset; + this.byterangeLength = byterangeLength; + } + + @Override + public int compareTo(Long startTimeUs) { + return this.startTimeUs > startTimeUs ? 1 : (this.startTimeUs < startTimeUs ? -1 : 0); + } + } + + public static final String ENCRYPTION_METHOD_NONE = "NONE"; + public static final String ENCRYPTION_METHOD_AES_128 = "AES-128"; + + public final int mediaSequence; + public final int targetDurationSecs; + public final int version; + public final List segments; + public final boolean live; + public final long durationUs; + + public HlsMediaPlaylist(String baseUri, int mediaSequence, int targetDurationSecs, int version, + boolean live, List segments) { + super(baseUri, HlsPlaylist.TYPE_MEDIA); + this.mediaSequence = mediaSequence; + this.targetDurationSecs = targetDurationSecs; + this.version = version; + this.live = live; + this.segments = segments; + + if (!segments.isEmpty()) { + Segment last = segments.get(segments.size() - 1); + durationUs = last.startTimeUs + (long) (last.durationSecs * C.MICROS_PER_SECOND); + } else { + durationUs = 0; + } + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/hls/HlsParserUtil.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/hls/HlsParserUtil.java new file mode 100644 index 000000000..ba7a23df6 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/hls/HlsParserUtil.java @@ -0,0 +1,72 @@ +/* + * 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.messenger.exoplayer.hls; + +import org.telegram.messenger.exoplayer.ParserException; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Utility methods for HLS manifest parsing. + */ +/* package */ final class HlsParserUtil { + + private static final String BOOLEAN_YES = "YES"; + private static final String BOOLEAN_NO = "NO"; + + private HlsParserUtil() {} + + public static String parseStringAttr(String line, Pattern pattern, String tag) + throws ParserException { + Matcher matcher = pattern.matcher(line); + if (matcher.find() && matcher.groupCount() == 1) { + return matcher.group(1); + } + throw new ParserException("Couldn't match " + tag + " tag in " + line); + } + + public static int parseIntAttr(String line, Pattern pattern, String tag) + throws ParserException { + return Integer.parseInt(parseStringAttr(line, pattern, tag)); + } + + public static double parseDoubleAttr(String line, Pattern pattern, String tag) + throws ParserException { + return Double.parseDouble(parseStringAttr(line, pattern, tag)); + } + + public static String parseOptionalStringAttr(String line, Pattern pattern) { + Matcher matcher = pattern.matcher(line); + if (matcher.find()) { + return matcher.group(1); + } + return null; + } + + public static boolean parseOptionalBooleanAttr(String line, Pattern pattern) { + Matcher matcher = pattern.matcher(line); + if (matcher.find()) { + return BOOLEAN_YES.equals(matcher.group(1)); + } + return false; + } + + public static Pattern compileBooleanAttrPattern(String attrName) { + return Pattern.compile(attrName + "=(" + BOOLEAN_YES + "|" + BOOLEAN_NO + ")"); + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/FloatProperty10.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/hls/HlsPlaylist.java old mode 100755 new mode 100644 similarity index 57% rename from TMessagesProj/src/main/java/org/telegram/messenger/Animation/FloatProperty10.java rename to TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/hls/HlsPlaylist.java index 1a3b6d798..16e1fafbe --- a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/FloatProperty10.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/hls/HlsPlaylist.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011 The Android Open Source Project + * 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. @@ -13,18 +13,22 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.telegram.messenger.Animation; +package org.telegram.messenger.exoplayer.hls; -public abstract class FloatProperty10 extends Property { +/** + * Represents an HLS playlist. + */ +public abstract class HlsPlaylist { - public FloatProperty10(String name) { - super(Float.class, name); - } + public final static int TYPE_MASTER = 0; + public final static int TYPE_MEDIA = 1; - public abstract void setValue(T object, float value); + public final String baseUri; + public final int type; - @Override - final public void set(T object, Float value) { - setValue(object, value.floatValue()); - } -} \ No newline at end of file + protected HlsPlaylist(String baseUri, int type) { + this.baseUri = baseUri; + this.type = type; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/hls/HlsPlaylistParser.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/hls/HlsPlaylistParser.java new file mode 100644 index 000000000..5d5c553db --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/hls/HlsPlaylistParser.java @@ -0,0 +1,355 @@ +/* + * 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.messenger.exoplayer.hls; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.ParserException; +import org.telegram.messenger.exoplayer.chunk.Format; +import org.telegram.messenger.exoplayer.hls.HlsMediaPlaylist.Segment; +import org.telegram.messenger.exoplayer.upstream.UriLoadable; +import org.telegram.messenger.exoplayer.util.MimeTypes; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; +import java.util.regex.Pattern; + +/** + * HLS playlists parsing logic. + */ +public final class HlsPlaylistParser implements UriLoadable.Parser { + + private static final String VERSION_TAG = "#EXT-X-VERSION"; + private static final String STREAM_INF_TAG = "#EXT-X-STREAM-INF"; + private static final String MEDIA_TAG = "#EXT-X-MEDIA"; + private static final String DISCONTINUITY_TAG = "#EXT-X-DISCONTINUITY"; + private static final String DISCONTINUITY_SEQUENCE_TAG = "#EXT-X-DISCONTINUITY-SEQUENCE"; + private static final String MEDIA_DURATION_TAG = "#EXTINF"; + private static final String MEDIA_SEQUENCE_TAG = "#EXT-X-MEDIA-SEQUENCE"; + private static final String TARGET_DURATION_TAG = "#EXT-X-TARGETDURATION"; + private static final String ENDLIST_TAG = "#EXT-X-ENDLIST"; + private static final String KEY_TAG = "#EXT-X-KEY"; + private static final String BYTERANGE_TAG = "#EXT-X-BYTERANGE"; + + private static final String BANDWIDTH_ATTR = "BANDWIDTH"; + private static final String CODECS_ATTR = "CODECS"; + private static final String RESOLUTION_ATTR = "RESOLUTION"; + private static final String LANGUAGE_ATTR = "LANGUAGE"; + private static final String NAME_ATTR = "NAME"; + private static final String TYPE_ATTR = "TYPE"; + private static final String METHOD_ATTR = "METHOD"; + private static final String URI_ATTR = "URI"; + private static final String IV_ATTR = "IV"; + private static final String INSTREAM_ID_ATTR = "INSTREAM-ID"; + + private static final String AUDIO_TYPE = "AUDIO"; + private static final String VIDEO_TYPE = "VIDEO"; + private static final String SUBTITLES_TYPE = "SUBTITLES"; + private static final String CLOSED_CAPTIONS_TYPE = "CLOSED-CAPTIONS"; + + private static final String METHOD_NONE = "NONE"; + private static final String METHOD_AES128 = "AES-128"; + + private static final Pattern BANDWIDTH_ATTR_REGEX = + Pattern.compile(BANDWIDTH_ATTR + "=(\\d+)\\b"); + private static final Pattern CODECS_ATTR_REGEX = + Pattern.compile(CODECS_ATTR + "=\"(.+?)\""); + private static final Pattern RESOLUTION_ATTR_REGEX = + Pattern.compile(RESOLUTION_ATTR + "=(\\d+x\\d+)"); + private static final Pattern MEDIA_DURATION_REGEX = + Pattern.compile(MEDIA_DURATION_TAG + ":([\\d.]+)\\b"); + private static final Pattern MEDIA_SEQUENCE_REGEX = + Pattern.compile(MEDIA_SEQUENCE_TAG + ":(\\d+)\\b"); + private static final Pattern TARGET_DURATION_REGEX = + Pattern.compile(TARGET_DURATION_TAG + ":(\\d+)\\b"); + private static final Pattern VERSION_REGEX = + Pattern.compile(VERSION_TAG + ":(\\d+)\\b"); + private static final Pattern BYTERANGE_REGEX = + Pattern.compile(BYTERANGE_TAG + ":(\\d+(?:@\\d+)?)\\b"); + + private static final Pattern METHOD_ATTR_REGEX = + Pattern.compile(METHOD_ATTR + "=(" + METHOD_NONE + "|" + METHOD_AES128 + ")"); + private static final Pattern URI_ATTR_REGEX = + Pattern.compile(URI_ATTR + "=\"(.+?)\""); + private static final Pattern IV_ATTR_REGEX = + Pattern.compile(IV_ATTR + "=([^,.*]+)"); + private static final Pattern TYPE_ATTR_REGEX = + Pattern.compile(TYPE_ATTR + "=(" + AUDIO_TYPE + "|" + VIDEO_TYPE + "|" + SUBTITLES_TYPE + "|" + + CLOSED_CAPTIONS_TYPE + ")"); + private static final Pattern LANGUAGE_ATTR_REGEX = + Pattern.compile(LANGUAGE_ATTR + "=\"(.+?)\""); + private static final Pattern NAME_ATTR_REGEX = + Pattern.compile(NAME_ATTR + "=\"(.+?)\""); + private static final Pattern INSTREAM_ID_ATTR_REGEX = + Pattern.compile(INSTREAM_ID_ATTR + "=\"(.+?)\""); + // private static final Pattern AUTOSELECT_ATTR_REGEX = + // HlsParserUtil.compileBooleanAttrPattern(AUTOSELECT_ATTR); + // private static final Pattern DEFAULT_ATTR_REGEX = + // HlsParserUtil.compileBooleanAttrPattern(DEFAULT_ATTR); + + @Override + public HlsPlaylist parse(String connectionUrl, InputStream inputStream) + throws IOException, ParserException { + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); + Queue extraLines = new LinkedList<>(); + String line; + try { + while ((line = reader.readLine()) != null) { + line = line.trim(); + if (line.isEmpty()) { + // Do nothing. + } else if (line.startsWith(STREAM_INF_TAG)) { + extraLines.add(line); + return parseMasterPlaylist(new LineIterator(extraLines, reader), connectionUrl); + } else if (line.startsWith(TARGET_DURATION_TAG) + || line.startsWith(MEDIA_SEQUENCE_TAG) + || line.startsWith(MEDIA_DURATION_TAG) + || line.startsWith(KEY_TAG) + || line.startsWith(BYTERANGE_TAG) + || line.equals(DISCONTINUITY_TAG) + || line.equals(DISCONTINUITY_SEQUENCE_TAG) + || line.equals(ENDLIST_TAG)) { + extraLines.add(line); + return parseMediaPlaylist(new LineIterator(extraLines, reader), connectionUrl); + } else { + extraLines.add(line); + } + } + } finally { + reader.close(); + } + throw new ParserException("Failed to parse the playlist, could not identify any tags."); + } + + private static HlsMasterPlaylist parseMasterPlaylist(LineIterator iterator, String baseUri) + throws IOException { + ArrayList variants = new ArrayList<>(); + ArrayList audios = new ArrayList<>(); + ArrayList subtitles = new ArrayList<>(); + int bitrate = 0; + String codecs = null; + int width = -1; + int height = -1; + String name = null; + String muxedAudioLanguage = null; + String muxedCaptionLanguage = null; + + boolean expectingStreamInfUrl = false; + String line; + while (iterator.hasNext()) { + line = iterator.next(); + if (line.startsWith(MEDIA_TAG)) { + String type = HlsParserUtil.parseStringAttr(line, TYPE_ATTR_REGEX, TYPE_ATTR); + if (CLOSED_CAPTIONS_TYPE.equals(type)) { + String instreamId = HlsParserUtil.parseStringAttr(line, INSTREAM_ID_ATTR_REGEX, + INSTREAM_ID_ATTR); + if ("CC1".equals(instreamId)) { + muxedCaptionLanguage = HlsParserUtil.parseOptionalStringAttr(line, LANGUAGE_ATTR_REGEX); + } + } else if (SUBTITLES_TYPE.equals(type)) { + // We assume all subtitles belong to the same group. + String subtitleName = HlsParserUtil.parseStringAttr(line, NAME_ATTR_REGEX, NAME_ATTR); + String uri = HlsParserUtil.parseStringAttr(line, URI_ATTR_REGEX, URI_ATTR); + String language = HlsParserUtil.parseOptionalStringAttr(line, LANGUAGE_ATTR_REGEX); + Format format = new Format(subtitleName, MimeTypes.APPLICATION_M3U8, -1, -1, -1, -1, -1, + -1, language, codecs); + subtitles.add(new Variant(uri, format)); + } else if (AUDIO_TYPE.equals(type)) { + // We assume all audios belong to the same group. + String language = HlsParserUtil.parseOptionalStringAttr(line, LANGUAGE_ATTR_REGEX); + String uri = HlsParserUtil.parseOptionalStringAttr(line, URI_ATTR_REGEX); + if (uri != null) { + String audioName = HlsParserUtil.parseStringAttr(line, NAME_ATTR_REGEX, NAME_ATTR); + Format format = new Format(audioName, MimeTypes.APPLICATION_M3U8, -1, -1, -1, -1, -1, + -1, language, codecs); + audios.add(new Variant(uri, format)); + } else { + muxedAudioLanguage = language; + } + } + } else if (line.startsWith(STREAM_INF_TAG)) { + bitrate = HlsParserUtil.parseIntAttr(line, BANDWIDTH_ATTR_REGEX, BANDWIDTH_ATTR); + codecs = HlsParserUtil.parseOptionalStringAttr(line, CODECS_ATTR_REGEX); + name = HlsParserUtil.parseOptionalStringAttr(line, NAME_ATTR_REGEX); + String resolutionString = HlsParserUtil.parseOptionalStringAttr(line, + RESOLUTION_ATTR_REGEX); + if (resolutionString != null) { + String[] widthAndHeight = resolutionString.split("x"); + width = Integer.parseInt(widthAndHeight[0]); + if (width <= 0) { + // Width was invalid. + width = -1; + } + height = Integer.parseInt(widthAndHeight[1]); + if (height <= 0) { + // Height was invalid. + height = -1; + } + } else { + width = -1; + height = -1; + } + expectingStreamInfUrl = true; + } else if (!line.startsWith("#") && expectingStreamInfUrl) { + if (name == null) { + name = Integer.toString(variants.size()); + } + Format format = new Format(name, MimeTypes.APPLICATION_M3U8, width, height, -1, -1, -1, + bitrate, null, codecs); + variants.add(new Variant(line, format)); + bitrate = 0; + codecs = null; + name = null; + width = -1; + height = -1; + expectingStreamInfUrl = false; + } + } + return new HlsMasterPlaylist(baseUri, variants, audios, subtitles, muxedAudioLanguage, + muxedCaptionLanguage); + } + + private static HlsMediaPlaylist parseMediaPlaylist(LineIterator iterator, String baseUri) + throws IOException { + int mediaSequence = 0; + int targetDurationSecs = 0; + int version = 1; // Default version == 1. + boolean live = true; + List segments = new ArrayList<>(); + + double segmentDurationSecs = 0.0; + int discontinuitySequenceNumber = 0; + long segmentStartTimeUs = 0; + long segmentByterangeOffset = 0; + long segmentByterangeLength = C.LENGTH_UNBOUNDED; + int segmentMediaSequence = 0; + + boolean isEncrypted = false; + String encryptionKeyUri = null; + String encryptionIV = null; + + String line; + while (iterator.hasNext()) { + line = iterator.next(); + if (line.startsWith(TARGET_DURATION_TAG)) { + targetDurationSecs = HlsParserUtil.parseIntAttr(line, TARGET_DURATION_REGEX, + TARGET_DURATION_TAG); + } else if (line.startsWith(MEDIA_SEQUENCE_TAG)) { + mediaSequence = HlsParserUtil.parseIntAttr(line, MEDIA_SEQUENCE_REGEX, MEDIA_SEQUENCE_TAG); + segmentMediaSequence = mediaSequence; + } else if (line.startsWith(VERSION_TAG)) { + version = HlsParserUtil.parseIntAttr(line, VERSION_REGEX, VERSION_TAG); + } else if (line.startsWith(MEDIA_DURATION_TAG)) { + segmentDurationSecs = HlsParserUtil.parseDoubleAttr(line, MEDIA_DURATION_REGEX, + MEDIA_DURATION_TAG); + } else if (line.startsWith(KEY_TAG)) { + String method = HlsParserUtil.parseStringAttr(line, METHOD_ATTR_REGEX, METHOD_ATTR); + isEncrypted = METHOD_AES128.equals(method); + if (isEncrypted) { + encryptionKeyUri = HlsParserUtil.parseStringAttr(line, URI_ATTR_REGEX, URI_ATTR); + encryptionIV = HlsParserUtil.parseOptionalStringAttr(line, IV_ATTR_REGEX); + } else { + encryptionKeyUri = null; + encryptionIV = null; + } + } else if (line.startsWith(BYTERANGE_TAG)) { + String byteRange = HlsParserUtil.parseStringAttr(line, BYTERANGE_REGEX, BYTERANGE_TAG); + String[] splitByteRange = byteRange.split("@"); + segmentByterangeLength = Long.parseLong(splitByteRange[0]); + if (splitByteRange.length > 1) { + segmentByterangeOffset = Long.parseLong(splitByteRange[1]); + } + } else if (line.startsWith(DISCONTINUITY_SEQUENCE_TAG)) { + discontinuitySequenceNumber = Integer.parseInt(line.substring(line.indexOf(':') + 1)); + } else if (line.equals(DISCONTINUITY_TAG)) { + discontinuitySequenceNumber++; + } else if (!line.startsWith("#")) { + String segmentEncryptionIV; + if (!isEncrypted) { + segmentEncryptionIV = null; + } else if (encryptionIV != null) { + segmentEncryptionIV = encryptionIV; + } else { + segmentEncryptionIV = Integer.toHexString(segmentMediaSequence); + } + segmentMediaSequence++; + if (segmentByterangeLength == C.LENGTH_UNBOUNDED) { + segmentByterangeOffset = 0; + } + segments.add(new Segment(line, segmentDurationSecs, discontinuitySequenceNumber, + segmentStartTimeUs, isEncrypted, encryptionKeyUri, segmentEncryptionIV, + segmentByterangeOffset, segmentByterangeLength)); + segmentStartTimeUs += (long) (segmentDurationSecs * C.MICROS_PER_SECOND); + segmentDurationSecs = 0.0; + if (segmentByterangeLength != C.LENGTH_UNBOUNDED) { + segmentByterangeOffset += segmentByterangeLength; + } + segmentByterangeLength = C.LENGTH_UNBOUNDED; + } else if (line.equals(ENDLIST_TAG)) { + live = false; + } + } + return new HlsMediaPlaylist(baseUri, mediaSequence, targetDurationSecs, version, live, + Collections.unmodifiableList(segments)); + } + + private static class LineIterator { + + private final BufferedReader reader; + private final Queue extraLines; + + private String next; + + public LineIterator(Queue extraLines, BufferedReader reader) { + this.extraLines = extraLines; + this.reader = reader; + } + + public boolean hasNext() throws IOException { + if (next != null) { + return true; + } + if (!extraLines.isEmpty()) { + next = extraLines.poll(); + return true; + } + while ((next = reader.readLine()) != null) { + next = next.trim(); + if (!next.isEmpty()) { + return true; + } + } + return false; + } + + public String next() throws IOException { + String result = null; + if (hasNext()) { + result = next; + next = null; + } + return result; + } + + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/hls/HlsSampleSource.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/hls/HlsSampleSource.java new file mode 100644 index 000000000..c71b6e396 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/hls/HlsSampleSource.java @@ -0,0 +1,841 @@ +/* + * 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.messenger.exoplayer.hls; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.LoadControl; +import org.telegram.messenger.exoplayer.MediaFormat; +import org.telegram.messenger.exoplayer.MediaFormatHolder; +import org.telegram.messenger.exoplayer.SampleHolder; +import org.telegram.messenger.exoplayer.SampleSource; +import org.telegram.messenger.exoplayer.SampleSource.SampleSourceReader; +import org.telegram.messenger.exoplayer.TrackRenderer; +import org.telegram.messenger.exoplayer.chunk.BaseChunkSampleSourceEventListener; +import org.telegram.messenger.exoplayer.chunk.Chunk; +import org.telegram.messenger.exoplayer.chunk.ChunkOperationHolder; +import org.telegram.messenger.exoplayer.chunk.Format; +import org.telegram.messenger.exoplayer.upstream.Loader; +import org.telegram.messenger.exoplayer.upstream.Loader.Loadable; +import org.telegram.messenger.exoplayer.util.Assertions; +import org.telegram.messenger.exoplayer.util.MimeTypes; + +import android.os.Handler; +import android.os.SystemClock; + +import java.io.IOException; +import java.util.Arrays; +import java.util.LinkedList; + +/** + * A {@link SampleSource} for HLS streams. + */ +public final class HlsSampleSource implements SampleSource, SampleSourceReader, Loader.Callback { + + /** + * Interface definition for a callback to be notified of {@link HlsSampleSource} events. + */ + public interface EventListener extends BaseChunkSampleSourceEventListener {} + + /** + * The default minimum number of times to retry loading data prior to failing. + */ + public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 3; + + private static final long NO_RESET_PENDING = Long.MIN_VALUE; + + private static final int PRIMARY_TYPE_NONE = 0; + private static final int PRIMARY_TYPE_TEXT = 1; + private static final int PRIMARY_TYPE_AUDIO = 2; + private static final int PRIMARY_TYPE_VIDEO = 3; + + private final HlsChunkSource chunkSource; + private final LinkedList extractors; + private final int minLoadableRetryCount; + private final int bufferSizeContribution; + private final ChunkOperationHolder chunkOperationHolder; + + private final int eventSourceId; + private final LoadControl loadControl; + private final Handler eventHandler; + private final EventListener eventListener; + + private int remainingReleaseCount; + private boolean prepared; + private boolean loadControlRegistered; + private int trackCount; + private int enabledTrackCount; + + private Format downstreamFormat; + + // Tracks are complicated in HLS. See documentation of buildTracks for details. + // Indexed by track (as exposed by this source). + private MediaFormat[] trackFormats; + private boolean[] trackEnabledStates; + private boolean[] pendingDiscontinuities; + private MediaFormat[] downstreamMediaFormats; + // Maps track index (as exposed by this source) to the corresponding chunk source track index for + // primary tracks, or to -1 otherwise. + private int[] chunkSourceTrackIndices; + // Maps track index (as exposed by this source) to the corresponding extractor track index. + private int[] extractorTrackIndices; + // Indexed by extractor track index. + private boolean[] extractorTrackEnabledStates; + + private long downstreamPositionUs; + private long lastSeekPositionUs; + private long pendingResetPositionUs; + + private boolean loadingFinished; + private Chunk currentLoadable; + private TsChunk currentTsLoadable; + private TsChunk previousTsLoadable; + + private Loader loader; + private IOException currentLoadableException; + private int currentLoadableExceptionCount; + private long currentLoadableExceptionTimestamp; + private long currentLoadStartTimeMs; + + public HlsSampleSource(HlsChunkSource chunkSource, LoadControl loadControl, + int bufferSizeContribution) { + this(chunkSource, loadControl, bufferSizeContribution, null, null, 0); + } + + public HlsSampleSource(HlsChunkSource chunkSource, LoadControl loadControl, + int bufferSizeContribution, Handler eventHandler, EventListener eventListener, + int eventSourceId) { + this(chunkSource, loadControl, bufferSizeContribution, eventHandler, eventListener, + eventSourceId, DEFAULT_MIN_LOADABLE_RETRY_COUNT); + } + + public HlsSampleSource(HlsChunkSource chunkSource, LoadControl loadControl, + int bufferSizeContribution, Handler eventHandler, EventListener eventListener, + int eventSourceId, int minLoadableRetryCount) { + this.chunkSource = chunkSource; + this.loadControl = loadControl; + this.bufferSizeContribution = bufferSizeContribution; + this.minLoadableRetryCount = minLoadableRetryCount; + this.eventHandler = eventHandler; + this.eventListener = eventListener; + this.eventSourceId = eventSourceId; + this.pendingResetPositionUs = NO_RESET_PENDING; + extractors = new LinkedList<>(); + chunkOperationHolder = new ChunkOperationHolder(); + } + + @Override + public SampleSourceReader register() { + remainingReleaseCount++; + return this; + } + + @Override + public boolean prepare(long positionUs) { + if (prepared) { + return true; + } else if (!chunkSource.prepare()) { + return false; + } + if (!extractors.isEmpty()) { + while (true) { + // We're not prepared, but we might have loaded what we need. + HlsExtractorWrapper extractor = extractors.getFirst(); + if (extractor.isPrepared()) { + buildTracks(extractor); + prepared = true; + maybeStartLoading(); // Update the load control. + return true; + } else if (extractors.size() > 1) { + extractors.removeFirst().clear(); + } else { + break; + } + } + } + // We're not prepared and we haven't loaded what we need. + if (loader == null) { + loader = new Loader("Loader:HLS"); + loadControl.register(this, bufferSizeContribution); + loadControlRegistered = true; + } + if (!loader.isLoading()) { + // We're going to have to start loading a chunk to get what we need for preparation. We should + // attempt to load the chunk at positionUs, so that we'll already be loading the correct chunk + // in the common case where the renderer is subsequently enabled at this position. + pendingResetPositionUs = positionUs; + downstreamPositionUs = positionUs; + } + maybeStartLoading(); + return false; + } + + @Override + public int getTrackCount() { + Assertions.checkState(prepared); + return trackCount; + } + + @Override + public MediaFormat getFormat(int track) { + Assertions.checkState(prepared); + return trackFormats[track]; + } + + @Override + public void enable(int track, long positionUs) { + Assertions.checkState(prepared); + setTrackEnabledState(track, true); + downstreamMediaFormats[track] = null; + pendingDiscontinuities[track] = false; + downstreamFormat = null; + boolean wasLoadControlRegistered = loadControlRegistered; + if (!loadControlRegistered) { + loadControl.register(this, bufferSizeContribution); + loadControlRegistered = true; + } + // Treat enabling of a live stream as occurring at t=0 in both of the blocks below. + positionUs = chunkSource.isLive() ? 0 : positionUs; + int chunkSourceTrack = chunkSourceTrackIndices[track]; + if (chunkSourceTrack != -1 && chunkSourceTrack != chunkSource.getSelectedTrackIndex()) { + // This is a primary track whose corresponding chunk source track is different to the one + // currently selected. We need to change the selection and restart. Since other exposed tracks + // may be enabled too, we need to implement the restart as a seek so that all downstream + // renderers receive a discontinuity event. + chunkSource.selectTrack(chunkSourceTrack); + seekToInternal(positionUs); + return; + } + if (enabledTrackCount == 1) { + lastSeekPositionUs = positionUs; + if (wasLoadControlRegistered && downstreamPositionUs == positionUs) { + // TODO: Address [Internal: b/21743989] to remove the need for this kind of hack. + // This is the first track to be enabled after preparation and the position is the same as + // was passed to prepare. In this case we can avoid restarting, which would reload the same + // chunks as were loaded during preparation. + maybeStartLoading(); + } else { + downstreamPositionUs = positionUs; + restartFrom(positionUs); + } + } + } + + @Override + public void disable(int track) { + Assertions.checkState(prepared); + setTrackEnabledState(track, false); + if (enabledTrackCount == 0) { + chunkSource.reset(); + downstreamPositionUs = Long.MIN_VALUE; + if (loadControlRegistered) { + loadControl.unregister(this); + loadControlRegistered = false; + } + if (loader.isLoading()) { + loader.cancelLoading(); + } else { + clearState(); + loadControl.trimAllocator(); + } + } + } + + @Override + public boolean continueBuffering(int track, long playbackPositionUs) { + Assertions.checkState(prepared); + Assertions.checkState(trackEnabledStates[track]); + downstreamPositionUs = playbackPositionUs; + if (!extractors.isEmpty()) { + discardSamplesForDisabledTracks(getCurrentExtractor(), downstreamPositionUs); + } + maybeStartLoading(); + if (loadingFinished) { + return true; + } + if (isPendingReset() || extractors.isEmpty()) { + return false; + } + for (int extractorIndex = 0; extractorIndex < extractors.size(); extractorIndex++) { + HlsExtractorWrapper extractor = extractors.get(extractorIndex); + if (!extractor.isPrepared()) { + break; + } + int extractorTrack = extractorTrackIndices[track]; + if (extractor.hasSamples(extractorTrack)) { + return true; + } + } + return false; + } + + @Override + public long readDiscontinuity(int track) { + if (pendingDiscontinuities[track]) { + pendingDiscontinuities[track] = false; + return lastSeekPositionUs; + } + return NO_DISCONTINUITY; + } + + @Override + public int readData(int track, long playbackPositionUs, MediaFormatHolder formatHolder, + SampleHolder sampleHolder) { + Assertions.checkState(prepared); + downstreamPositionUs = playbackPositionUs; + + if (pendingDiscontinuities[track] || isPendingReset()) { + return NOTHING_READ; + } + + HlsExtractorWrapper extractor = getCurrentExtractor(); + if (!extractor.isPrepared()) { + return NOTHING_READ; + } + + Format format = extractor.format; + if (!format.equals(downstreamFormat)) { + notifyDownstreamFormatChanged(format, extractor.trigger, extractor.startTimeUs); + } + downstreamFormat = format; + + if (extractors.size() > 1) { + // If there's more than one extractor, attempt to configure a seamless splice from the + // current one to the next one. + extractor.configureSpliceTo(extractors.get(1)); + } + + int extractorTrack = extractorTrackIndices[track]; + int extractorIndex = 0; + while (extractors.size() > extractorIndex + 1 && !extractor.hasSamples(extractorTrack)) { + // We're finished reading from the extractor for this particular track, so advance to the + // next one for the current read. + extractor = extractors.get(++extractorIndex); + if (!extractor.isPrepared()) { + return NOTHING_READ; + } + } + + MediaFormat mediaFormat = extractor.getMediaFormat(extractorTrack); + if (mediaFormat != null) { + if (!mediaFormat.equals(downstreamMediaFormats[track])) { + formatHolder.format = mediaFormat; + downstreamMediaFormats[track] = mediaFormat; + return FORMAT_READ; + } + // If mediaFormat and downstreamMediaFormat[track] are equal but different objects then the + // equality check above will have been expensive, comparing the fields in each format. We + // update downstreamMediaFormat here so that referential equality can be cheaply established + // during subsequent calls. + downstreamMediaFormats[track] = mediaFormat; + } + + if (extractor.getSample(extractorTrack, sampleHolder)) { + boolean decodeOnly = sampleHolder.timeUs < lastSeekPositionUs; + sampleHolder.flags |= decodeOnly ? C.SAMPLE_FLAG_DECODE_ONLY : 0; + return SAMPLE_READ; + } + + if (loadingFinished) { + return END_OF_STREAM; + } + + return NOTHING_READ; + } + + @Override + public void maybeThrowError() throws IOException { + if (currentLoadableException != null && currentLoadableExceptionCount > minLoadableRetryCount) { + throw currentLoadableException; + } else if (currentLoadable == null) { + chunkSource.maybeThrowError(); + } + } + + @Override + public void seekToUs(long positionUs) { + Assertions.checkState(prepared); + Assertions.checkState(enabledTrackCount > 0); + // Treat all seeks into live streams as being to t=0. + positionUs = chunkSource.isLive() ? 0 : positionUs; + + // Ignore seeks to the current position. + long currentPositionUs = isPendingReset() ? pendingResetPositionUs : downstreamPositionUs; + downstreamPositionUs = positionUs; + lastSeekPositionUs = positionUs; + if (currentPositionUs == positionUs) { + return; + } + + seekToInternal(positionUs); + } + + @Override + public long getBufferedPositionUs() { + Assertions.checkState(prepared); + Assertions.checkState(enabledTrackCount > 0); + if (isPendingReset()) { + return pendingResetPositionUs; + } else if (loadingFinished) { + return TrackRenderer.END_OF_TRACK_US; + } else { + long largestParsedTimestampUs = extractors.getLast().getLargestParsedTimestampUs(); + if (extractors.size() > 1) { + // When adapting from one format to the next, the penultimate extractor may have the largest + // parsed timestamp (e.g. if the last extractor hasn't parsed any timestamps yet). + largestParsedTimestampUs = Math.max(largestParsedTimestampUs, + extractors.get(extractors.size() - 2).getLargestParsedTimestampUs()); + } + return largestParsedTimestampUs == Long.MIN_VALUE ? downstreamPositionUs + : largestParsedTimestampUs; + } + } + + @Override + public void release() { + Assertions.checkState(remainingReleaseCount > 0); + if (--remainingReleaseCount == 0 && loader != null) { + if (loadControlRegistered) { + loadControl.unregister(this); + loadControlRegistered = false; + } + loader.release(); + loader = null; + } + } + + // Loader.Callback implementation. + + @Override + public void onLoadCompleted(Loadable loadable) { + Assertions.checkState(loadable == currentLoadable); + long now = SystemClock.elapsedRealtime(); + long loadDurationMs = now - currentLoadStartTimeMs; + chunkSource.onChunkLoadCompleted(currentLoadable); + if (isTsChunk(currentLoadable)) { + Assertions.checkState(currentLoadable == currentTsLoadable); + previousTsLoadable = currentTsLoadable; + notifyLoadCompleted(currentLoadable.bytesLoaded(), currentTsLoadable.type, + currentTsLoadable.trigger, currentTsLoadable.format, currentTsLoadable.startTimeUs, + currentTsLoadable.endTimeUs, now, loadDurationMs); + } else { + notifyLoadCompleted(currentLoadable.bytesLoaded(), currentLoadable.type, + currentLoadable.trigger, currentLoadable.format, -1, -1, now, loadDurationMs); + } + clearCurrentLoadable(); + maybeStartLoading(); + } + + @Override + public void onLoadCanceled(Loadable loadable) { + notifyLoadCanceled(currentLoadable.bytesLoaded()); + if (enabledTrackCount > 0) { + restartFrom(pendingResetPositionUs); + } else { + clearState(); + loadControl.trimAllocator(); + } + } + + @Override + public void onLoadError(Loadable loadable, IOException e) { + if (chunkSource.onChunkLoadError(currentLoadable, e)) { + // Error handled by source. + if (previousTsLoadable == null && !isPendingReset()) { + pendingResetPositionUs = lastSeekPositionUs; + } + clearCurrentLoadable(); + } else { + currentLoadableException = e; + currentLoadableExceptionCount++; + currentLoadableExceptionTimestamp = SystemClock.elapsedRealtime(); + } + notifyLoadError(e); + maybeStartLoading(); + } + + // Internal stuff. + + /** + * Builds tracks that are exposed by this {@link HlsSampleSource} instance, as well as internal + * data-structures required for operation. + *

+ * Tracks in HLS are complicated. A HLS master playlist contains a number of "variants". Each + * variant stream typically contains muxed video, audio and (possibly) additional audio, metadata + * and caption tracks. We wish to allow the user to select between an adaptive track that spans + * all variants, as well as each individual variant. If multiple audio tracks are present within + * each variant then we wish to allow the user to select between those also. + *

+ * To do this, tracks are constructed as follows. The {@link HlsChunkSource} exposes (N+1) tracks, + * where N is the number of variants defined in the HLS master playlist. These consist of one + * adaptive track defined to span all variants and a track for each individual variant. The + * adaptive track is initially selected. The extractor is then prepared to discover the tracks + * inside of each variant stream. The two sets of tracks are then combined by this method to + * create a third set, which is the set exposed by this {@link HlsSampleSource}: + *

    + *
  • The extractor tracks are inspected to infer a "primary" track type. If a video track is + * present then it is always the primary type. If not, audio is the primary type if present. + * Else text is the primary type if present. Else there is no primary type.
  • + *
  • If there is exactly one extractor track of the primary type, it's expanded into (N+1) + * exposed tracks, all of which correspond to the primary extractor track and each of which + * corresponds to a different chunk source track. Selecting one of these tracks has the effect + * of switching the selected track on the chunk source.
  • + *
  • All other extractor tracks are exposed directly. Selecting one of these tracks has the + * effect of selecting an extractor track, leaving the selected track on the chunk source + * unchanged.
  • + *
+ * + * @param extractor The prepared extractor. + */ + private void buildTracks(HlsExtractorWrapper extractor) { + // Iterate through the extractor tracks to discover the "primary" track type, and the index + // of the single track of this type. + int primaryExtractorTrackType = PRIMARY_TYPE_NONE; + int primaryExtractorTrackIndex = -1; + int extractorTrackCount = extractor.getTrackCount(); + for (int i = 0; i < extractorTrackCount; i++) { + String mimeType = extractor.getMediaFormat(i).mimeType; + int trackType; + if (MimeTypes.isVideo(mimeType)) { + trackType = PRIMARY_TYPE_VIDEO; + } else if (MimeTypes.isAudio(mimeType)) { + trackType = PRIMARY_TYPE_AUDIO; + } else if (MimeTypes.isText(mimeType)) { + trackType = PRIMARY_TYPE_TEXT; + } else { + trackType = PRIMARY_TYPE_NONE; + } + if (trackType > primaryExtractorTrackType) { + primaryExtractorTrackType = trackType; + primaryExtractorTrackIndex = i; + } else if (trackType == primaryExtractorTrackType && primaryExtractorTrackIndex != -1) { + // We have multiple tracks of the primary type. We only want an index if there only + // exists a single track of the primary type, so set the index back to -1. + primaryExtractorTrackIndex = -1; + } + } + + // Calculate the number of tracks that will be exposed. + int chunkSourceTrackCount = chunkSource.getTrackCount(); + boolean expandPrimaryExtractorTrack = primaryExtractorTrackIndex != -1; + trackCount = extractorTrackCount; + if (expandPrimaryExtractorTrack) { + trackCount += chunkSourceTrackCount - 1; + } + + // Instantiate the necessary internal data-structures. + trackFormats = new MediaFormat[trackCount]; + trackEnabledStates = new boolean[trackCount]; + pendingDiscontinuities = new boolean[trackCount]; + downstreamMediaFormats = new MediaFormat[trackCount]; + chunkSourceTrackIndices = new int[trackCount]; + extractorTrackIndices = new int[trackCount]; + extractorTrackEnabledStates = new boolean[extractorTrackCount]; + + // Construct the set of exposed tracks. + long durationUs = chunkSource.getDurationUs(); + int trackIndex = 0; + for (int i = 0; i < extractorTrackCount; i++) { + MediaFormat format = extractor.getMediaFormat(i).copyWithDurationUs(durationUs); + String language = null; + if (MimeTypes.isAudio(format.mimeType)) { + language = chunkSource.getMuxedAudioLanguage(); + } else if (MimeTypes.APPLICATION_EIA608.equals(format.mimeType)) { + language = chunkSource.getMuxedCaptionLanguage(); + } + if (i == primaryExtractorTrackIndex) { + for (int j = 0; j < chunkSourceTrackCount; j++) { + extractorTrackIndices[trackIndex] = i; + chunkSourceTrackIndices[trackIndex] = j; + Variant fixedTrackVariant = chunkSource.getFixedTrackVariant(j); + trackFormats[trackIndex++] = fixedTrackVariant == null ? format.copyAsAdaptive(null) + : copyWithFixedTrackInfo(format, fixedTrackVariant.format, language); + } + } else { + extractorTrackIndices[trackIndex] = i; + chunkSourceTrackIndices[trackIndex] = -1; + trackFormats[trackIndex++] = format.copyWithLanguage(language); + } + } + } + + /** + * Enables or disables the track at a given index. + * + * @param track The index of the track. + * @param enabledState True if the track is being enabled, or false if it's being disabled. + */ + private void setTrackEnabledState(int track, boolean enabledState) { + Assertions.checkState(trackEnabledStates[track] != enabledState); + int extractorTrack = extractorTrackIndices[track]; + Assertions.checkState(extractorTrackEnabledStates[extractorTrack] != enabledState); + trackEnabledStates[track] = enabledState; + extractorTrackEnabledStates[extractorTrack] = enabledState; + enabledTrackCount = enabledTrackCount + (enabledState ? 1 : -1); + } + + /** + * Copies a provided {@link MediaFormat}, incorporating information from the {@link Format} of + * a fixed (i.e. non-adaptive) track, as well as a language. + * + * @param format The {@link MediaFormat} to copy. + * @param fixedTrackFormat The {@link Format} to incorporate into the copy. + * @param language The language to incorporate into the copy. + * @return The copied {@link MediaFormat}. + */ + private static MediaFormat copyWithFixedTrackInfo(MediaFormat format, Format fixedTrackFormat, + String language) { + int width = fixedTrackFormat.width == -1 ? MediaFormat.NO_VALUE : fixedTrackFormat.width; + int height = fixedTrackFormat.height == -1 ? MediaFormat.NO_VALUE : fixedTrackFormat.height; + return format.copyWithFixedTrackInfo(fixedTrackFormat.id, fixedTrackFormat.bitrate, width, + height, language); + } + + /** + * Performs a seek. The operation is performed even if the seek is to the current position. + * + * @param positionUs The position to seek to. + */ + private void seekToInternal(long positionUs) { + lastSeekPositionUs = positionUs; + downstreamPositionUs = positionUs; + Arrays.fill(pendingDiscontinuities, true); + chunkSource.seek(); + restartFrom(positionUs); + } + + /** + * Gets the current extractor from which samples should be read. + *

+ * Calling this method discards extractors without any samples from the front of the queue. The + * last extractor is retained even if it doesn't have any samples. + *

+ * This method must not be called unless {@link #extractors} is non-empty. + * + * @return The current extractor from which samples should be read. Guaranteed to be non-null. + */ + private HlsExtractorWrapper getCurrentExtractor() { + HlsExtractorWrapper extractor = extractors.getFirst(); + while (extractors.size() > 1 && !haveSamplesForEnabledTracks(extractor)) { + // We're finished reading from the extractor for all tracks, and so can discard it. + extractors.removeFirst().clear(); + extractor = extractors.getFirst(); + } + return extractor; + } + + private void discardSamplesForDisabledTracks(HlsExtractorWrapper extractor, long timeUs) { + if (!extractor.isPrepared()) { + return; + } + for (int i = 0; i < extractorTrackEnabledStates.length; i++) { + if (!extractorTrackEnabledStates[i]) { + extractor.discardUntil(i, timeUs); + } + } + } + + private boolean haveSamplesForEnabledTracks(HlsExtractorWrapper extractor) { + if (!extractor.isPrepared()) { + return false; + } + for (int i = 0; i < extractorTrackEnabledStates.length; i++) { + if (extractorTrackEnabledStates[i] && extractor.hasSamples(i)) { + return true; + } + } + return false; + } + + private void restartFrom(long positionUs) { + pendingResetPositionUs = positionUs; + loadingFinished = false; + if (loader.isLoading()) { + loader.cancelLoading(); + } else { + clearState(); + maybeStartLoading(); + } + } + + private void clearState() { + for (int i = 0; i < extractors.size(); i++) { + extractors.get(i).clear(); + } + extractors.clear(); + clearCurrentLoadable(); + previousTsLoadable = null; + } + + private void clearCurrentLoadable() { + currentTsLoadable = null; + currentLoadable = null; + currentLoadableException = null; + currentLoadableExceptionCount = 0; + } + + private void maybeStartLoading() { + long now = SystemClock.elapsedRealtime(); + long nextLoadPositionUs = getNextLoadPositionUs(); + boolean isBackedOff = currentLoadableException != null; + boolean loadingOrBackedOff = loader.isLoading() || isBackedOff; + + // Update the control with our current state, and determine whether we're the next loader. + boolean nextLoader = loadControl.update(this, downstreamPositionUs, nextLoadPositionUs, + loadingOrBackedOff); + + if (isBackedOff) { + long elapsedMillis = now - currentLoadableExceptionTimestamp; + if (elapsedMillis >= getRetryDelayMillis(currentLoadableExceptionCount)) { + currentLoadableException = null; + loader.startLoading(currentLoadable, this); + } + return; + } + + if (loader.isLoading() || !nextLoader || (prepared && enabledTrackCount == 0)) { + return; + } + + chunkSource.getChunkOperation(previousTsLoadable, + pendingResetPositionUs != NO_RESET_PENDING ? pendingResetPositionUs : downstreamPositionUs, + chunkOperationHolder); + boolean endOfStream = chunkOperationHolder.endOfStream; + Chunk nextLoadable = chunkOperationHolder.chunk; + chunkOperationHolder.clear(); + + if (endOfStream) { + loadingFinished = true; + loadControl.update(this, downstreamPositionUs, -1, false); + return; + } + if (nextLoadable == null) { + return; + } + + currentLoadStartTimeMs = now; + currentLoadable = nextLoadable; + if (isTsChunk(currentLoadable)) { + TsChunk tsChunk = (TsChunk) currentLoadable; + if (isPendingReset()) { + pendingResetPositionUs = NO_RESET_PENDING; + } + HlsExtractorWrapper extractorWrapper = tsChunk.extractorWrapper; + if (extractors.isEmpty() || extractors.getLast() != extractorWrapper) { + extractorWrapper.init(loadControl.getAllocator()); + extractors.addLast(extractorWrapper); + } + notifyLoadStarted(tsChunk.dataSpec.length, tsChunk.type, tsChunk.trigger, tsChunk.format, + tsChunk.startTimeUs, tsChunk.endTimeUs); + currentTsLoadable = tsChunk; + } else { + notifyLoadStarted(currentLoadable.dataSpec.length, currentLoadable.type, + currentLoadable.trigger, currentLoadable.format, -1, -1); + } + loader.startLoading(currentLoadable, this); + } + + /** + * Gets the next load time, assuming that the next load starts where the previous chunk ended (or + * from the pending reset time, if there is one). + */ + private long getNextLoadPositionUs() { + if (isPendingReset()) { + return pendingResetPositionUs; + } else { + return loadingFinished || (prepared && enabledTrackCount == 0) ? -1 + : currentTsLoadable != null ? currentTsLoadable.endTimeUs : previousTsLoadable.endTimeUs; + } + } + + private boolean isTsChunk(Chunk chunk) { + return chunk instanceof TsChunk; + } + + private boolean isPendingReset() { + return pendingResetPositionUs != NO_RESET_PENDING; + } + + private long getRetryDelayMillis(long errorCount) { + return Math.min((errorCount - 1) * 1000, 5000); + } + + /* package */ long usToMs(long timeUs) { + return timeUs / 1000; + } + + private void notifyLoadStarted(final long length, final int type, final int trigger, + final Format format, final long mediaStartTimeUs, final long mediaEndTimeUs) { + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onLoadStarted(eventSourceId, length, type, trigger, format, + usToMs(mediaStartTimeUs), usToMs(mediaEndTimeUs)); + } + }); + } + } + + private void notifyLoadCompleted(final long bytesLoaded, final int type, final int trigger, + final Format format, final long mediaStartTimeUs, final long mediaEndTimeUs, + final long elapsedRealtimeMs, final long loadDurationMs) { + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onLoadCompleted(eventSourceId, bytesLoaded, type, trigger, format, + usToMs(mediaStartTimeUs), usToMs(mediaEndTimeUs), elapsedRealtimeMs, loadDurationMs); + } + }); + } + } + + private void notifyLoadCanceled(final long bytesLoaded) { + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onLoadCanceled(eventSourceId, bytesLoaded); + } + }); + } + } + + private void notifyLoadError(final IOException e) { + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onLoadError(eventSourceId, e); + } + }); + } + } + + private void notifyDownstreamFormatChanged(final Format format, final int trigger, + final long positionUs) { + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onDownstreamFormatChanged(eventSourceId, format, trigger, + usToMs(positionUs)); + } + }); + } + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/hls/HlsTrackSelector.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/hls/HlsTrackSelector.java new file mode 100644 index 000000000..361ec38d4 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/hls/HlsTrackSelector.java @@ -0,0 +1,59 @@ +/* + * 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.messenger.exoplayer.hls; + +import java.io.IOException; + +/** + * Specifies a track selection from an {@link HlsMasterPlaylist}. + */ +public interface HlsTrackSelector { + + /** + * Defines a selector output. + */ + interface Output { + + /** + * Outputs an adaptive track, covering the specified representations in the specified + * adaptation set. + * + * @param playlist The master playlist being processed. + * @param variants The variants to use for the adaptive track. + */ + void adaptiveTrack(HlsMasterPlaylist playlist, Variant[] variants); + + /** + * Outputs an fixed track corresponding to the specified representation in the specified + * adaptation set. + * + * @param playlist The master playlist being processed. + * @param variant The variant to use for the track. + */ + void fixedTrack(HlsMasterPlaylist playlist, Variant variant); + + } + + /** + * Outputs a track selection for a given period. + * + * @param playlist The master playlist to process. + * @param output The output to receive tracks. + * @throws IOException If an error occurs processing the period. + */ + void selectTracks(HlsMasterPlaylist playlist, Output output) throws IOException; + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/hls/PtsTimestampAdjusterProvider.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/hls/PtsTimestampAdjusterProvider.java new file mode 100644 index 000000000..6ff34feb4 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/hls/PtsTimestampAdjusterProvider.java @@ -0,0 +1,63 @@ +/* + * 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.messenger.exoplayer.hls; + +import org.telegram.messenger.exoplayer.extractor.ts.PtsTimestampAdjuster; + +import android.util.SparseArray; + +/** + * Provides {@link PtsTimestampAdjuster} instances for use during HLS playbacks. + */ +public final class PtsTimestampAdjusterProvider { + + // TODO: Prevent this array from growing indefinitely large by removing adjusters that are no + // longer required. + private final SparseArray ptsTimestampAdjusters; + + public PtsTimestampAdjusterProvider() { + ptsTimestampAdjusters = new SparseArray<>(); + } + + /** + * Gets a {@link PtsTimestampAdjuster} suitable for adjusting the pts timestamps contained in + * a chunk with a given discontinuity sequence. + *

+ * This method may return null if the master source has yet to initialize a suitable adjuster. + * + * @param isMasterSource True if the calling chunk source is the master. + * @param discontinuitySequence The chunk's discontinuity sequence. + * @param startTimeUs The chunk's start time. + * @return A {@link PtsTimestampAdjuster}. + */ + public PtsTimestampAdjuster getAdjuster(boolean isMasterSource, int discontinuitySequence, + long startTimeUs) { + PtsTimestampAdjuster adjuster = ptsTimestampAdjusters.get(discontinuitySequence); + if (isMasterSource && adjuster == null) { + adjuster = new PtsTimestampAdjuster(startTimeUs); + ptsTimestampAdjusters.put(discontinuitySequence, adjuster); + } + return isMasterSource || (adjuster != null && adjuster.isInitialized()) ? adjuster : null; + } + + /** + * Resets the provider. + */ + public void reset() { + ptsTimestampAdjusters.clear(); + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/hls/TsChunk.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/hls/TsChunk.java new file mode 100644 index 000000000..10a47ce1c --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/hls/TsChunk.java @@ -0,0 +1,139 @@ +/* + * 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.messenger.exoplayer.hls; + +import org.telegram.messenger.exoplayer.chunk.Format; +import org.telegram.messenger.exoplayer.chunk.MediaChunk; +import org.telegram.messenger.exoplayer.extractor.DefaultExtractorInput; +import org.telegram.messenger.exoplayer.extractor.Extractor; +import org.telegram.messenger.exoplayer.extractor.ExtractorInput; +import org.telegram.messenger.exoplayer.upstream.DataSource; +import org.telegram.messenger.exoplayer.upstream.DataSpec; +import org.telegram.messenger.exoplayer.util.Util; + +import java.io.IOException; + +/** + * An MPEG2TS chunk. + */ +public final class TsChunk extends MediaChunk { + + /** + * The discontinuity sequence number of the chunk. + */ + public final int discontinuitySequenceNumber; + + /** + * The wrapped extractor into which this chunk is being consumed. + */ + public final HlsExtractorWrapper extractorWrapper; + + private final boolean isEncrypted; + + private int bytesLoaded; + private volatile boolean loadCanceled; + + /** + * @param dataSource A {@link DataSource} for loading the data. + * @param dataSpec Defines the data to be loaded. + * @param trigger The reason for this chunk being selected. + * @param format The format of the stream to which this chunk belongs. + * @param startTimeUs The start time of the media contained by the chunk, in microseconds. + * @param endTimeUs The end time of the media contained by the chunk, in microseconds. + * @param discontinuitySequenceNumber The discontinuity sequence number of the chunk. + * @param chunkIndex The index of the chunk. + * @param extractorWrapper A wrapped extractor to parse samples from the data. + * @param encryptionKey For AES encryption chunks, the encryption key. + * @param encryptionIv For AES encryption chunks, the encryption initialization vector. + */ + public TsChunk(DataSource dataSource, DataSpec dataSpec, int trigger, Format format, + long startTimeUs, long endTimeUs, int chunkIndex, int discontinuitySequenceNumber, + HlsExtractorWrapper extractorWrapper, byte[] encryptionKey, byte[] encryptionIv) { + super(buildDataSource(dataSource, encryptionKey, encryptionIv), dataSpec, trigger, format, + startTimeUs, endTimeUs, chunkIndex); + this.discontinuitySequenceNumber = discontinuitySequenceNumber; + this.extractorWrapper = extractorWrapper; + // Note: this.dataSource and dataSource may be different. + this.isEncrypted = this.dataSource instanceof Aes128DataSource; + } + + @Override + public long bytesLoaded() { + return bytesLoaded; + } + + // Loadable implementation + + @Override + public void cancelLoad() { + loadCanceled = true; + } + + @Override + public boolean isLoadCanceled() { + return loadCanceled; + } + + @Override + public void load() throws IOException, InterruptedException { + // If we previously fed part of this chunk to the extractor, we need to skip it this time. For + // encrypted content we need to skip the data by reading it through the source, so as to ensure + // correct decryption of the remainder of the chunk. For clear content, we can request the + // remainder of the chunk directly. + DataSpec loadDataSpec; + boolean skipLoadedBytes; + if (isEncrypted) { + loadDataSpec = dataSpec; + skipLoadedBytes = bytesLoaded != 0; + } else { + loadDataSpec = Util.getRemainderDataSpec(dataSpec, bytesLoaded); + skipLoadedBytes = false; + } + + try { + ExtractorInput input = new DefaultExtractorInput(dataSource, + loadDataSpec.absoluteStreamPosition, dataSource.open(loadDataSpec)); + if (skipLoadedBytes) { + input.skipFully(bytesLoaded); + } + try { + int result = Extractor.RESULT_CONTINUE; + while (result == Extractor.RESULT_CONTINUE && !loadCanceled) { + result = extractorWrapper.read(input); + } + } finally { + bytesLoaded = (int) (input.getPosition() - dataSpec.absoluteStreamPosition); + } + } finally { + dataSource.close(); + } + } + + // Private methods + + /** + * If the content is encrypted, returns an {@link Aes128DataSource} that wraps the original in + * order to decrypt the loaded data. Else returns the original. + */ + private static DataSource buildDataSource(DataSource dataSource, byte[] encryptionKey, + byte[] encryptionIv) { + if (encryptionKey == null || encryptionIv == null) { + return dataSource; + } + return new Aes128DataSource(dataSource, encryptionKey, encryptionIv); + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/hls/Variant.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/hls/Variant.java new file mode 100644 index 000000000..828e942cd --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/hls/Variant.java @@ -0,0 +1,39 @@ +/* + * 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.messenger.exoplayer.hls; + +import org.telegram.messenger.exoplayer.chunk.Format; +import org.telegram.messenger.exoplayer.chunk.FormatWrapper; + +/** + * Variant stream reference. + */ +public final class Variant implements FormatWrapper { + + public final String url; + public final Format format; + + public Variant(String url, Format format) { + this.url = url; + this.format = format; + } + + @Override + public Format getFormat() { + return format; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/hls/WebvttExtractor.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/hls/WebvttExtractor.java new file mode 100644 index 000000000..6e5d86522 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/hls/WebvttExtractor.java @@ -0,0 +1,173 @@ +/* + * 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.messenger.exoplayer.hls; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.MediaFormat; +import org.telegram.messenger.exoplayer.ParserException; +import org.telegram.messenger.exoplayer.extractor.Extractor; +import org.telegram.messenger.exoplayer.extractor.ExtractorInput; +import org.telegram.messenger.exoplayer.extractor.ExtractorOutput; +import org.telegram.messenger.exoplayer.extractor.PositionHolder; +import org.telegram.messenger.exoplayer.extractor.SeekMap; +import org.telegram.messenger.exoplayer.extractor.TrackOutput; +import org.telegram.messenger.exoplayer.extractor.ts.PtsTimestampAdjuster; +import org.telegram.messenger.exoplayer.text.webvtt.WebvttCueParser; +import org.telegram.messenger.exoplayer.text.webvtt.WebvttParserUtil; +import org.telegram.messenger.exoplayer.util.MimeTypes; +import org.telegram.messenger.exoplayer.util.ParsableByteArray; + +import android.text.TextUtils; + +import java.io.IOException; +import java.util.Arrays; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A special purpose extractor for WebVTT content in HLS. + *

+ * This extractor passes through non-empty WebVTT files untouched, however derives the correct + * sample timestamp for each by sniffing the X-TIMESTAMP-MAP header along with the start timestamp + * of the first cue header. Empty WebVTT files are not passed through, since it's not possible to + * derive a sample timestamp in this case. + */ +/* package */ final class WebvttExtractor implements Extractor { + + private static final Pattern LOCAL_TIMESTAMP = Pattern.compile("LOCAL:([^,]+)"); + private static final Pattern MEDIA_TIMESTAMP = Pattern.compile("MPEGTS:(\\d+)"); + + private final PtsTimestampAdjuster ptsTimestampAdjuster; + private final ParsableByteArray sampleDataWrapper; + + private ExtractorOutput output; + + private byte[] sampleData; + private int sampleSize; + + public WebvttExtractor(PtsTimestampAdjuster ptsTimestampAdjuster) { + this.ptsTimestampAdjuster = ptsTimestampAdjuster; + this.sampleDataWrapper = new ParsableByteArray(); + sampleData = new byte[1024]; + } + + // Extractor implementation. + + @Override + public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { + // This extractor is only used for the HLS use case, which should not call this method. + throw new IllegalStateException(); + } + + @Override + public void init(ExtractorOutput output) { + this.output = output; + output.seekMap(SeekMap.UNSEEKABLE); + } + + @Override + public void seek() { + // This extractor is only used for the HLS use case, which should not call this method. + throw new IllegalStateException(); + } + + @Override + public void release() { + // Do nothing + } + + @Override + public int read(ExtractorInput input, PositionHolder seekPosition) + throws IOException, InterruptedException { + int currentFileSize = (int) input.getLength(); + + // Increase the size of sampleData if necessary. + if (sampleSize == sampleData.length) { + sampleData = Arrays.copyOf(sampleData, + (currentFileSize != C.LENGTH_UNBOUNDED ? currentFileSize : sampleData.length) * 3 / 2); + } + + // Consume to the input. + int bytesRead = input.read(sampleData, sampleSize, sampleData.length - sampleSize); + if (bytesRead != C.RESULT_END_OF_INPUT) { + sampleSize += bytesRead; + if (currentFileSize == C.LENGTH_UNBOUNDED || sampleSize != currentFileSize) { + return Extractor.RESULT_CONTINUE; + } + } + + // We've reached the end of the input, which corresponds to the end of the current file. + processSample(); + return Extractor.RESULT_END_OF_INPUT; + } + + private void processSample() throws ParserException { + ParsableByteArray webvttData = new ParsableByteArray(sampleData); + + // Validate the first line of the header. + WebvttParserUtil.validateWebvttHeaderLine(webvttData); + + // Defaults to use if the header doesn't contain an X-TIMESTAMP-MAP header. + long vttTimestampUs = 0; + long tsTimestampUs = 0; + + // Parse the remainder of the header looking for X-TIMESTAMP-MAP. + String line; + while (!TextUtils.isEmpty(line = webvttData.readLine())) { + if (line.startsWith("X-TIMESTAMP-MAP")) { + Matcher localTimestampMatcher = LOCAL_TIMESTAMP.matcher(line); + if (!localTimestampMatcher.find()) { + throw new ParserException("X-TIMESTAMP-MAP doesn't contain local timestamp: " + line); + } + Matcher mediaTimestampMatcher = MEDIA_TIMESTAMP.matcher(line); + if (!mediaTimestampMatcher.find()) { + throw new ParserException("X-TIMESTAMP-MAP doesn't contain media timestamp: " + line); + } + vttTimestampUs = WebvttParserUtil.parseTimestampUs(localTimestampMatcher.group(1)); + tsTimestampUs = PtsTimestampAdjuster.ptsToUs( + Long.parseLong(mediaTimestampMatcher.group(1))); + } + } + + // Find the first cue header and parse the start time. + Matcher cueHeaderMatcher = WebvttCueParser.findNextCueHeader(webvttData); + if (cueHeaderMatcher == null) { + // No cues found. Don't output a sample, but still output a corresponding track. + buildTrackOutput(0); + return; + } + + long firstCueTimeUs = WebvttParserUtil.parseTimestampUs(cueHeaderMatcher.group(1)); + long sampleTimeUs = ptsTimestampAdjuster.adjustTimestamp( + PtsTimestampAdjuster.usToPts(firstCueTimeUs + tsTimestampUs - vttTimestampUs)); + long subsampleOffsetUs = sampleTimeUs - firstCueTimeUs; + // Output the track. + TrackOutput trackOutput = buildTrackOutput(subsampleOffsetUs); + // Output the sample. + sampleDataWrapper.reset(sampleData, sampleSize); + trackOutput.sampleData(sampleDataWrapper, sampleSize); + trackOutput.sampleMetadata(sampleTimeUs, C.SAMPLE_FLAG_SYNC, sampleSize, 0, null); + } + + private TrackOutput buildTrackOutput(long subsampleOffsetUs) { + TrackOutput trackOutput = output.track(0); + trackOutput.format(MediaFormat.createTextFormat("id", MimeTypes.TEXT_VTT, MediaFormat.NO_VALUE, + C.UNKNOWN_TIME_US, "en", subsampleOffsetUs)); + output.endTracks(); + return trackOutput; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/metadata/MetadataParser.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/metadata/MetadataParser.java new file mode 100755 index 000000000..0320a4574 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/metadata/MetadataParser.java @@ -0,0 +1,45 @@ +/* + * 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.messenger.exoplayer.metadata; + +import org.telegram.messenger.exoplayer.ParserException; + +/** + * Parses objects of type from binary data. + * + * @param The type of the metadata. + */ +public interface MetadataParser { + + /** + * Checks whether the parser supports a given mime type. + * + * @param mimeType A metadata mime type. + * @return Whether the mime type is supported. + */ + public boolean canParse(String mimeType); + + /** + * Parses metadata objects of type from the provided binary data. + * + * @param data The raw binary data from which to parse the metadata. + * @param size The size of the input data. + * @return @return A parsed metadata object of type . + * @throws ParserException If a problem occurred parsing the data. + */ + public T parse(byte[] data, int size) throws ParserException; + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/metadata/MetadataTrackRenderer.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/metadata/MetadataTrackRenderer.java new file mode 100755 index 000000000..b4ff6810f --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/metadata/MetadataTrackRenderer.java @@ -0,0 +1,169 @@ +/* + * 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.messenger.exoplayer.metadata; + +import org.telegram.messenger.exoplayer.ExoPlaybackException; +import org.telegram.messenger.exoplayer.MediaFormat; +import org.telegram.messenger.exoplayer.MediaFormatHolder; +import org.telegram.messenger.exoplayer.SampleHolder; +import org.telegram.messenger.exoplayer.SampleSource; +import org.telegram.messenger.exoplayer.SampleSourceTrackRenderer; +import org.telegram.messenger.exoplayer.TrackRenderer; +import org.telegram.messenger.exoplayer.util.Assertions; + +import android.os.Handler; +import android.os.Handler.Callback; +import android.os.Looper; +import android.os.Message; + +import java.io.IOException; + +/** + * A {@link TrackRenderer} for metadata embedded in a media stream. + * + * @param The type of the metadata. + */ +public final class MetadataTrackRenderer extends SampleSourceTrackRenderer implements Callback { + + /** + * An interface for components that process metadata. + * + * @param The type of the metadata. + */ + public interface MetadataRenderer { + + /** + * Invoked each time there is a metadata associated with current playback time. + * + * @param metadata The metadata to process. + */ + void onMetadata(T metadata); + + } + + private static final int MSG_INVOKE_RENDERER = 0; + + private final MetadataParser metadataParser; + private final MetadataRenderer metadataRenderer; + private final Handler metadataHandler; + private final MediaFormatHolder formatHolder; + private final SampleHolder sampleHolder; + + private boolean inputStreamEnded; + private long pendingMetadataTimestamp; + private T pendingMetadata; + + /** + * @param source A source from which samples containing metadata can be read. + * @param metadataParser A parser for parsing the metadata. + * @param metadataRenderer The metadata renderer to receive the parsed metadata. + * @param metadataRendererLooper The looper associated with the thread on which metadataRenderer + * should be invoked. If the renderer makes use of standard Android UI components, then this + * should normally be the looper associated with the applications' main thread, which can be + * obtained using {@link android.app.Activity#getMainLooper()}. Null may be passed if the + * renderer should be invoked directly on the player's internal rendering thread. + */ + public MetadataTrackRenderer(SampleSource source, MetadataParser metadataParser, + MetadataRenderer metadataRenderer, Looper metadataRendererLooper) { + super(source); + this.metadataParser = Assertions.checkNotNull(metadataParser); + this.metadataRenderer = Assertions.checkNotNull(metadataRenderer); + this.metadataHandler = metadataRendererLooper == null ? null + : new Handler(metadataRendererLooper, this); + formatHolder = new MediaFormatHolder(); + sampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL); + } + + @Override + protected boolean handlesTrack(MediaFormat mediaFormat) { + return metadataParser.canParse(mediaFormat.mimeType); + } + + @Override + protected void onDiscontinuity(long positionUs) { + pendingMetadata = null; + inputStreamEnded = false; + } + + @Override + protected void doSomeWork(long positionUs, long elapsedRealtimeUs, boolean sourceIsReady) + throws ExoPlaybackException { + if (!inputStreamEnded && pendingMetadata == null) { + sampleHolder.clearData(); + int result = readSource(positionUs, formatHolder, sampleHolder); + if (result == SampleSource.SAMPLE_READ) { + pendingMetadataTimestamp = sampleHolder.timeUs; + try { + pendingMetadata = metadataParser.parse(sampleHolder.data.array(), sampleHolder.size); + } catch (IOException e) { + throw new ExoPlaybackException(e); + } + } else if (result == SampleSource.END_OF_STREAM) { + inputStreamEnded = true; + } + } + + if (pendingMetadata != null && pendingMetadataTimestamp <= positionUs) { + invokeRenderer(pendingMetadata); + pendingMetadata = null; + } + } + + @Override + protected void onDisabled() throws ExoPlaybackException { + pendingMetadata = null; + super.onDisabled(); + } + + @Override + protected long getBufferedPositionUs() { + return TrackRenderer.END_OF_TRACK_US; + } + + @Override + protected boolean isEnded() { + return inputStreamEnded; + } + + @Override + protected boolean isReady() { + return true; + } + + private void invokeRenderer(T metadata) { + if (metadataHandler != null) { + metadataHandler.obtainMessage(MSG_INVOKE_RENDERER, metadata).sendToTarget(); + } else { + invokeRendererInternal(metadata); + } + } + + @SuppressWarnings("unchecked") + @Override + public boolean handleMessage(Message msg) { + switch (msg.what) { + case MSG_INVOKE_RENDERER: + invokeRendererInternal((T) msg.obj); + return true; + } + return false; + } + + private void invokeRendererInternal(T metadata) { + metadataRenderer.onMetadata(metadata); + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/metadata/id3/ApicFrame.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/metadata/id3/ApicFrame.java new file mode 100755 index 000000000..0508dd72b --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/metadata/id3/ApicFrame.java @@ -0,0 +1,38 @@ +/* + * 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.messenger.exoplayer.metadata.id3; + +/** + * APIC (Attached Picture) ID3 frame. + */ +public final class ApicFrame extends Id3Frame { + + public static final String ID = "APIC"; + + public final String mimeType; + public final String description; + public final int pictureType; + public final byte[] pictureData; + + public ApicFrame(String mimeType, String description, int pictureType, byte[] pictureData) { + super(ID); + this.mimeType = mimeType; + this.description = description; + this.pictureType = pictureType; + this.pictureData = pictureData; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/metadata/id3/BinaryFrame.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/metadata/id3/BinaryFrame.java new file mode 100755 index 000000000..0cb6bc2fc --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/metadata/id3/BinaryFrame.java @@ -0,0 +1,30 @@ +/* + * 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.messenger.exoplayer.metadata.id3; + +/** + * Binary ID3 frame. + */ +public final class BinaryFrame extends Id3Frame { + + public final byte[] data; + + public BinaryFrame(String type, byte[] data) { + super(type); + this.data = data; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/metadata/id3/GeobFrame.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/metadata/id3/GeobFrame.java new file mode 100755 index 000000000..a8709cfd2 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/metadata/id3/GeobFrame.java @@ -0,0 +1,38 @@ +/* + * 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.messenger.exoplayer.metadata.id3; + +/** + * GEOB (General Encapsulated Object) ID3 frame. + */ +public final class GeobFrame extends Id3Frame { + + public static final String ID = "GEOB"; + + public final String mimeType; + public final String filename; + public final String description; + public final byte[] data; + + public GeobFrame(String mimeType, String filename, String description, byte[] data) { + super(ID); + this.mimeType = mimeType; + this.filename = filename; + this.description = description; + this.data = data; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/metadata/id3/Id3Frame.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/metadata/id3/Id3Frame.java new file mode 100755 index 000000000..22756ea3e --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/metadata/id3/Id3Frame.java @@ -0,0 +1,32 @@ +/* + * 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.messenger.exoplayer.metadata.id3; + +/** + * Base class for ID3 frames. + */ +public abstract class Id3Frame { + + /** + * The frame ID. + */ + public final String id; + + public Id3Frame(String id) { + this.id = id; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/metadata/id3/Id3Parser.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/metadata/id3/Id3Parser.java new file mode 100755 index 000000000..aceea5fb7 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/metadata/id3/Id3Parser.java @@ -0,0 +1,285 @@ +/* + * 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.messenger.exoplayer.metadata.id3; + +import org.telegram.messenger.exoplayer.ParserException; +import org.telegram.messenger.exoplayer.metadata.MetadataParser; +import org.telegram.messenger.exoplayer.util.MimeTypes; +import org.telegram.messenger.exoplayer.util.ParsableByteArray; + +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +/** + * Extracts individual TXXX text frames from raw ID3 data. + */ +public final class Id3Parser implements MetadataParser> { + + private static final int ID3_TEXT_ENCODING_ISO_8859_1 = 0; + private static final int ID3_TEXT_ENCODING_UTF_16 = 1; + private static final int ID3_TEXT_ENCODING_UTF_16BE = 2; + private static final int ID3_TEXT_ENCODING_UTF_8 = 3; + + @Override + public boolean canParse(String mimeType) { + return mimeType.equals(MimeTypes.APPLICATION_ID3); + } + + @Override + public List parse(byte[] data, int size) throws ParserException { + List id3Frames = new ArrayList<>(); + ParsableByteArray id3Data = new ParsableByteArray(data, size); + int id3Size = parseId3Header(id3Data); + + while (id3Size > 0) { + int frameId0 = id3Data.readUnsignedByte(); + int frameId1 = id3Data.readUnsignedByte(); + int frameId2 = id3Data.readUnsignedByte(); + int frameId3 = id3Data.readUnsignedByte(); + int frameSize = id3Data.readSynchSafeInt(); + if (frameSize <= 1) { + break; + } + + // Skip frame flags. + id3Data.skipBytes(2); + + try { + Id3Frame frame; + if (frameId0 == 'T' && frameId1 == 'X' && frameId2 == 'X' && frameId3 == 'X') { + frame = parseTxxxFrame(id3Data, frameSize); + } else if (frameId0 == 'P' && frameId1 == 'R' && frameId2 == 'I' && frameId3 == 'V') { + frame = parsePrivFrame(id3Data, frameSize); + } else if (frameId0 == 'G' && frameId1 == 'E' && frameId2 == 'O' && frameId3 == 'B') { + frame = parseGeobFrame(id3Data, frameSize); + } else if (frameId0 == 'A' && frameId1 == 'P' && frameId2 == 'I' && frameId3 == 'C') { + frame = parseApicFrame(id3Data, frameSize); + } else if (frameId0 == 'T') { + String id = String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3); + frame = parseTextInformationFrame(id3Data, frameSize, id); + } else { + String id = String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3); + frame = parseBinaryFrame(id3Data, frameSize, id); + } + id3Frames.add(frame); + id3Size -= frameSize + 10 /* header size */; + } catch (UnsupportedEncodingException e) { + throw new ParserException(e); + } + } + + return Collections.unmodifiableList(id3Frames); + } + + private static int indexOfEos(byte[] data, int fromIndex, int encoding) { + int terminationPos = indexOfZeroByte(data, fromIndex); + + // For single byte encoding charsets, we're done. + if (encoding == ID3_TEXT_ENCODING_ISO_8859_1 || encoding == ID3_TEXT_ENCODING_UTF_8) { + return terminationPos; + } + + // Otherwise look for a second zero byte. + while (terminationPos < data.length - 1) { + if (data[terminationPos + 1] == (byte) 0) { + return terminationPos; + } + terminationPos = indexOfZeroByte(data, terminationPos + 1); + } + + return data.length; + } + + private static int indexOfZeroByte(byte[] data, int fromIndex) { + for (int i = fromIndex; i < data.length; i++) { + if (data[i] == (byte) 0) { + return i; + } + } + return data.length; + } + + private static int delimiterLength(int encodingByte) { + return (encodingByte == ID3_TEXT_ENCODING_ISO_8859_1 || encodingByte == ID3_TEXT_ENCODING_UTF_8) + ? 1 : 2; + } + + /** + * Parses an ID3 header. + * + * @param id3Buffer A {@link ParsableByteArray} from which data should be read. + * @return The size of ID3 frames in bytes, excluding the header and footer. + * @throws ParserException If ID3 file identifier != "ID3". + */ + private static int parseId3Header(ParsableByteArray id3Buffer) throws ParserException { + int id1 = id3Buffer.readUnsignedByte(); + int id2 = id3Buffer.readUnsignedByte(); + int id3 = id3Buffer.readUnsignedByte(); + if (id1 != 'I' || id2 != 'D' || id3 != '3') { + throw new ParserException(String.format(Locale.US, + "Unexpected ID3 file identifier, expected \"ID3\", actual \"%c%c%c\".", id1, id2, id3)); + } + id3Buffer.skipBytes(2); // Skip version. + + int flags = id3Buffer.readUnsignedByte(); + int id3Size = id3Buffer.readSynchSafeInt(); + + // Check if extended header presents. + if ((flags & 0x2) != 0) { + int extendedHeaderSize = id3Buffer.readSynchSafeInt(); + if (extendedHeaderSize > 4) { + id3Buffer.skipBytes(extendedHeaderSize - 4); + } + id3Size -= extendedHeaderSize; + } + + // Check if footer presents. + if ((flags & 0x8) != 0) { + id3Size -= 10; + } + + return id3Size; + } + + private static TxxxFrame parseTxxxFrame(ParsableByteArray id3Data, int frameSize) + throws UnsupportedEncodingException { + int encoding = id3Data.readUnsignedByte(); + String charset = getCharsetName(encoding); + + byte[] data = new byte[frameSize - 1]; + id3Data.readBytes(data, 0, frameSize - 1); + + int descriptionEndIndex = indexOfEos(data, 0, encoding); + String description = new String(data, 0, descriptionEndIndex, charset); + + int valueStartIndex = descriptionEndIndex + delimiterLength(encoding); + int valueEndIndex = indexOfEos(data, valueStartIndex, encoding); + String value = new String(data, valueStartIndex, valueEndIndex - valueStartIndex, charset); + + return new TxxxFrame(description, value); + } + + private static PrivFrame parsePrivFrame(ParsableByteArray id3Data, int frameSize) + throws UnsupportedEncodingException { + byte[] data = new byte[frameSize]; + id3Data.readBytes(data, 0, frameSize); + + int ownerEndIndex = indexOfZeroByte(data, 0); + String owner = new String(data, 0, ownerEndIndex, "ISO-8859-1"); + + int privateDataStartIndex = ownerEndIndex + 1; + byte[] privateData = Arrays.copyOfRange(data, privateDataStartIndex, data.length); + + return new PrivFrame(owner, privateData); + } + + private static GeobFrame parseGeobFrame(ParsableByteArray id3Data, int frameSize) + throws UnsupportedEncodingException { + int encoding = id3Data.readUnsignedByte(); + String charset = getCharsetName(encoding); + + byte[] data = new byte[frameSize - 1]; + id3Data.readBytes(data, 0, frameSize - 1); + + int mimeTypeEndIndex = indexOfZeroByte(data, 0); + String mimeType = new String(data, 0, mimeTypeEndIndex, "ISO-8859-1"); + + int filenameStartIndex = mimeTypeEndIndex + 1; + int filenameEndIndex = indexOfEos(data, filenameStartIndex, encoding); + String filename = new String(data, filenameStartIndex, filenameEndIndex - filenameStartIndex, + charset); + + int descriptionStartIndex = filenameEndIndex + delimiterLength(encoding); + int descriptionEndIndex = indexOfEos(data, descriptionStartIndex, encoding); + String description = new String(data, descriptionStartIndex, + descriptionEndIndex - descriptionStartIndex, charset); + + int objectDataStartIndex = descriptionEndIndex + delimiterLength(encoding); + byte[] objectData = Arrays.copyOfRange(data, objectDataStartIndex, data.length); + + return new GeobFrame(mimeType, filename, description, objectData); + } + + private static ApicFrame parseApicFrame(ParsableByteArray id3Data, int frameSize) + throws UnsupportedEncodingException { + int encoding = id3Data.readUnsignedByte(); + String charset = getCharsetName(encoding); + + byte[] data = new byte[frameSize - 1]; + id3Data.readBytes(data, 0, frameSize - 1); + + int mimeTypeEndIndex = indexOfZeroByte(data, 0); + String mimeType = new String(data, 0, mimeTypeEndIndex, "ISO-8859-1"); + + int pictureType = data[mimeTypeEndIndex + 1] & 0xFF; + + int descriptionStartIndex = mimeTypeEndIndex + 2; + int descriptionEndIndex = indexOfEos(data, descriptionStartIndex, encoding); + String description = new String(data, descriptionStartIndex, + descriptionEndIndex - descriptionStartIndex, charset); + + int pictureDataStartIndex = descriptionEndIndex + delimiterLength(encoding); + byte[] pictureData = Arrays.copyOfRange(data, pictureDataStartIndex, data.length); + + return new ApicFrame(mimeType, description, pictureType, pictureData); + } + + private static TextInformationFrame parseTextInformationFrame(ParsableByteArray id3Data, + int frameSize, String id) throws UnsupportedEncodingException { + int encoding = id3Data.readUnsignedByte(); + String charset = getCharsetName(encoding); + + byte[] data = new byte[frameSize - 1]; + id3Data.readBytes(data, 0, frameSize - 1); + + int descriptionEndIndex = indexOfEos(data, 0, encoding); + String description = new String(data, 0, descriptionEndIndex, charset); + + return new TextInformationFrame(id, description); + } + + private static BinaryFrame parseBinaryFrame(ParsableByteArray id3Data, int frameSize, String id) { + byte[] frame = new byte[frameSize]; + id3Data.readBytes(frame, 0, frameSize); + + return new BinaryFrame(id, frame); + } + + /** + * Maps encoding byte from ID3v2 frame to a Charset. + * @param encodingByte The value of encoding byte from ID3v2 frame. + * @return Charset name. + */ + private static String getCharsetName(int encodingByte) { + switch (encodingByte) { + case ID3_TEXT_ENCODING_ISO_8859_1: + return "ISO-8859-1"; + case ID3_TEXT_ENCODING_UTF_16: + return "UTF-16"; + case ID3_TEXT_ENCODING_UTF_16BE: + return "UTF-16BE"; + case ID3_TEXT_ENCODING_UTF_8: + return "UTF-8"; + default: + return "ISO-8859-1"; + } + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/metadata/id3/PrivFrame.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/metadata/id3/PrivFrame.java new file mode 100755 index 000000000..89d05e5a5 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/metadata/id3/PrivFrame.java @@ -0,0 +1,34 @@ +/* + * 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.messenger.exoplayer.metadata.id3; + +/** + * PRIV (Private) ID3 frame. + */ +public final class PrivFrame extends Id3Frame { + + public static final String ID = "PRIV"; + + public final String owner; + public final byte[] privateData; + + public PrivFrame(String owner, byte[] privateData) { + super(ID); + this.owner = owner; + this.privateData = privateData; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/metadata/id3/TextInformationFrame.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/metadata/id3/TextInformationFrame.java new file mode 100755 index 000000000..1694d240a --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/metadata/id3/TextInformationFrame.java @@ -0,0 +1,30 @@ +/* + * 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.messenger.exoplayer.metadata.id3; + +/** + * Text information ("T000" - "TZZZ", excluding "TXXX") ID3 frame. + */ +public final class TextInformationFrame extends Id3Frame { + + public final String description; + + public TextInformationFrame(String id, String description) { + super(id); + this.description = description; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/metadata/id3/TxxxFrame.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/metadata/id3/TxxxFrame.java new file mode 100755 index 000000000..9bbaef3d1 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/metadata/id3/TxxxFrame.java @@ -0,0 +1,34 @@ +/* + * 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.messenger.exoplayer.metadata.id3; + +/** + * TXXX (User defined text information) ID3 frame. + */ +public final class TxxxFrame extends Id3Frame { + + public static final String ID = "TXXX"; + + public final String description; + public final String value; + + public TxxxFrame(String description, String value) { + super(ID); + this.description = description; + this.value = value; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/smoothstreaming/DefaultSmoothStreamingTrackSelector.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/smoothstreaming/DefaultSmoothStreamingTrackSelector.java new file mode 100755 index 000000000..d8bcca371 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/smoothstreaming/DefaultSmoothStreamingTrackSelector.java @@ -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.messenger.exoplayer.smoothstreaming; + +import org.telegram.messenger.exoplayer.chunk.VideoFormatSelectorUtil; +import org.telegram.messenger.exoplayer.smoothstreaming.SmoothStreamingManifest.StreamElement; +import org.telegram.messenger.exoplayer.smoothstreaming.SmoothStreamingManifest.TrackElement; +import org.telegram.messenger.exoplayer.util.Util; + +import android.content.Context; + +import java.io.IOException; +import java.util.Arrays; + +/** + * A default {@link SmoothStreamingTrackSelector} implementation. + */ +// TODO: Add more configuration options (e.g. ability to disable adaptive track output). +public final class DefaultSmoothStreamingTrackSelector implements SmoothStreamingTrackSelector { + + private final int streamElementType; + + private final Context context; + private final boolean filterVideoRepresentations; + private final boolean filterProtectedHdContent; + + /** + * @param context A context. May be null if {@code filterVideoRepresentations == false}. + * @param filterVideoRepresentations Whether video representations should be filtered according to + * the capabilities of the device. It is strongly recommended to set this to {@code true}, + * unless the application has already verified that all representations are playable. + * @param filterProtectedHdContent Whether video representations that are both drm protected and + * high definition should be filtered when tracks are built. If + * {@code filterVideoRepresentations == false} then this parameter is ignored. + */ + public static DefaultSmoothStreamingTrackSelector newVideoInstance(Context context, + boolean filterVideoRepresentations, boolean filterProtectedHdContent) { + return new DefaultSmoothStreamingTrackSelector(StreamElement.TYPE_VIDEO, context, + filterVideoRepresentations, filterProtectedHdContent); + } + + public static DefaultSmoothStreamingTrackSelector newAudioInstance() { + return new DefaultSmoothStreamingTrackSelector(StreamElement.TYPE_AUDIO, null, false, false); + } + + public static DefaultSmoothStreamingTrackSelector newTextInstance() { + return new DefaultSmoothStreamingTrackSelector(StreamElement.TYPE_TEXT, null, false, false); + } + + private DefaultSmoothStreamingTrackSelector(int streamElementType, Context context, + boolean filterVideoRepresentations, boolean filterProtectedHdContent) { + this.context = context; + this.streamElementType = streamElementType; + this.filterVideoRepresentations = filterVideoRepresentations; + this.filterProtectedHdContent = filterProtectedHdContent; + } + + @Override + public void selectTracks(SmoothStreamingManifest manifest, Output output) throws IOException { + for (int i = 0; i < manifest.streamElements.length; i++) { + TrackElement[] tracks = manifest.streamElements[i].tracks; + if (manifest.streamElements[i].type == streamElementType) { + if (streamElementType == StreamElement.TYPE_VIDEO) { + int[] trackIndices; + if (filterVideoRepresentations) { + trackIndices = VideoFormatSelectorUtil.selectVideoFormatsForDefaultDisplay( + context, Arrays.asList(tracks), null, + filterProtectedHdContent && manifest.protectionElement != null); + } else { + trackIndices = Util.firstIntegersArray(tracks.length); + } + int trackCount = trackIndices.length; + if (trackCount > 1) { + output.adaptiveTrack(manifest, i, trackIndices); + } + for (int j = 0; j < trackCount; j++) { + output.fixedTrack(manifest, i, trackIndices[j]); + } + } else { + for (int j = 0; j < tracks.length; j++) { + output.fixedTrack(manifest, i, j); + } + } + } + } + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java new file mode 100755 index 000000000..5feaf8d6a --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/smoothstreaming/SmoothStreamingChunkSource.java @@ -0,0 +1,558 @@ +/* + * 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.messenger.exoplayer.smoothstreaming; + +import org.telegram.messenger.exoplayer.BehindLiveWindowException; +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.MediaFormat; +import org.telegram.messenger.exoplayer.chunk.Chunk; +import org.telegram.messenger.exoplayer.chunk.ChunkExtractorWrapper; +import org.telegram.messenger.exoplayer.chunk.ChunkOperationHolder; +import org.telegram.messenger.exoplayer.chunk.ChunkSource; +import org.telegram.messenger.exoplayer.chunk.ContainerMediaChunk; +import org.telegram.messenger.exoplayer.chunk.Format; +import org.telegram.messenger.exoplayer.chunk.Format.DecreasingBandwidthComparator; +import org.telegram.messenger.exoplayer.chunk.FormatEvaluator; +import org.telegram.messenger.exoplayer.chunk.FormatEvaluator.Evaluation; +import org.telegram.messenger.exoplayer.chunk.MediaChunk; +import org.telegram.messenger.exoplayer.drm.DrmInitData; +import org.telegram.messenger.exoplayer.drm.DrmInitData.SchemeInitData; +import org.telegram.messenger.exoplayer.extractor.mp4.FragmentedMp4Extractor; +import org.telegram.messenger.exoplayer.extractor.mp4.Track; +import org.telegram.messenger.exoplayer.extractor.mp4.TrackEncryptionBox; +import org.telegram.messenger.exoplayer.smoothstreaming.SmoothStreamingManifest.ProtectionElement; +import org.telegram.messenger.exoplayer.smoothstreaming.SmoothStreamingManifest.StreamElement; +import org.telegram.messenger.exoplayer.smoothstreaming.SmoothStreamingManifest.TrackElement; +import org.telegram.messenger.exoplayer.upstream.DataSource; +import org.telegram.messenger.exoplayer.upstream.DataSpec; +import org.telegram.messenger.exoplayer.util.Assertions; +import org.telegram.messenger.exoplayer.util.CodecSpecificDataUtil; +import org.telegram.messenger.exoplayer.util.ManifestFetcher; +import org.telegram.messenger.exoplayer.util.MimeTypes; + +import android.net.Uri; +import android.os.SystemClock; +import android.util.Base64; +import android.util.SparseArray; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * An {@link ChunkSource} for SmoothStreaming. + */ +public class SmoothStreamingChunkSource implements ChunkSource, + SmoothStreamingTrackSelector.Output { + + private static final int MINIMUM_MANIFEST_REFRESH_PERIOD_MS = 5000; + private static final int INITIALIZATION_VECTOR_SIZE = 8; + + private final SmoothStreamingTrackSelector trackSelector; + private final DataSource dataSource; + private final Evaluation evaluation; + private final long liveEdgeLatencyUs; + private final TrackEncryptionBox[] trackEncryptionBoxes; + private final ManifestFetcher manifestFetcher; + private final DrmInitData.Mapped drmInitData; + private final FormatEvaluator adaptiveFormatEvaluator; + private final boolean live; + + // The tracks exposed by this source. + private final ArrayList tracks; + + // Mappings from manifest track key. + private final SparseArray extractorWrappers; + private final SparseArray mediaFormats; + + private boolean prepareCalled; + private SmoothStreamingManifest currentManifest; + private int currentManifestChunkOffset; + private boolean needManifestRefresh; + private ExposedTrack enabledTrack; + private IOException fatalError; + + /** + * Constructor to use for live streaming. + *

+ * May also be used for fixed duration content, in which case the call is equivalent to calling + * the other constructor, passing {@code manifestFetcher.getManifest()} is the first argument. + * + * @param manifestFetcher A fetcher for the manifest, which must have already successfully + * completed an initial load. + * @param trackSelector Selects tracks from the manifest to be exposed by this source. + * @param dataSource A {@link DataSource} suitable for loading the media data. + * @param adaptiveFormatEvaluator For adaptive tracks, selects from the available formats. + * @param liveEdgeLatencyMs For live streams, the number of milliseconds that the playback should + * lag behind the "live edge" (i.e. the end of the most recently defined media in the + * manifest). Choosing a small value will minimize latency introduced by the player, however + * note that the value sets an upper bound on the length of media that the player can buffer. + * Hence a small value may increase the probability of rebuffering and playback failures. + */ + public SmoothStreamingChunkSource(ManifestFetcher manifestFetcher, + SmoothStreamingTrackSelector trackSelector, DataSource dataSource, + FormatEvaluator adaptiveFormatEvaluator, long liveEdgeLatencyMs) { + this(manifestFetcher, manifestFetcher.getManifest(), trackSelector, dataSource, + adaptiveFormatEvaluator, liveEdgeLatencyMs); + } + + /** + * Constructor to use for fixed duration content. + * + * @param manifest The manifest parsed from {@code baseUrl + "/Manifest"}. + * @param trackSelector Selects tracks from the manifest to be exposed by this source. + * @param dataSource A {@link DataSource} suitable for loading the media data. + * @param adaptiveFormatEvaluator For adaptive tracks, selects from the available formats. + */ + public SmoothStreamingChunkSource(SmoothStreamingManifest manifest, + SmoothStreamingTrackSelector trackSelector, DataSource dataSource, + FormatEvaluator adaptiveFormatEvaluator) { + this(null, manifest, trackSelector, dataSource, adaptiveFormatEvaluator, 0); + } + + private SmoothStreamingChunkSource(ManifestFetcher manifestFetcher, + SmoothStreamingManifest initialManifest, SmoothStreamingTrackSelector trackSelector, + DataSource dataSource, FormatEvaluator adaptiveFormatEvaluator, long liveEdgeLatencyMs) { + this.manifestFetcher = manifestFetcher; + this.currentManifest = initialManifest; + this.trackSelector = trackSelector; + this.dataSource = dataSource; + this.adaptiveFormatEvaluator = adaptiveFormatEvaluator; + this.liveEdgeLatencyUs = liveEdgeLatencyMs * 1000; + evaluation = new Evaluation(); + tracks = new ArrayList<>(); + extractorWrappers = new SparseArray<>(); + mediaFormats = new SparseArray<>(); + live = initialManifest.isLive; + + ProtectionElement protectionElement = initialManifest.protectionElement; + if (protectionElement != null) { + byte[] keyId = getProtectionElementKeyId(protectionElement.data); + trackEncryptionBoxes = new TrackEncryptionBox[1]; + trackEncryptionBoxes[0] = new TrackEncryptionBox(true, INITIALIZATION_VECTOR_SIZE, keyId); + drmInitData = new DrmInitData.Mapped(); + drmInitData.put(protectionElement.uuid, + new SchemeInitData(MimeTypes.VIDEO_MP4, protectionElement.data)); + } else { + trackEncryptionBoxes = null; + drmInitData = null; + } + } + + // ChunkSource implementation. + + @Override + public void maybeThrowError() throws IOException { + if (fatalError != null) { + throw fatalError; + } else { + manifestFetcher.maybeThrowError(); + } + } + + @Override + public boolean prepare() { + if (!prepareCalled) { + prepareCalled = true; + try { + trackSelector.selectTracks(currentManifest, this); + } catch (IOException e) { + fatalError = e; + } + } + return fatalError == null; + } + + @Override + public int getTrackCount() { + return tracks.size(); + } + + @Override + public final MediaFormat getFormat(int track) { + return tracks.get(track).trackFormat; + } + + @Override + public void enable(int track) { + enabledTrack = tracks.get(track); + if (enabledTrack.isAdaptive()) { + adaptiveFormatEvaluator.enable(); + } + if (manifestFetcher != null) { + manifestFetcher.enable(); + } + } + + @Override + public void continueBuffering(long playbackPositionUs) { + if (manifestFetcher == null || !currentManifest.isLive || fatalError != null) { + return; + } + + SmoothStreamingManifest newManifest = manifestFetcher.getManifest(); + if (currentManifest != newManifest && newManifest != null) { + StreamElement currentElement = currentManifest.streamElements[enabledTrack.elementIndex]; + int currentElementChunkCount = currentElement.chunkCount; + StreamElement newElement = newManifest.streamElements[enabledTrack.elementIndex]; + if (currentElementChunkCount == 0 || newElement.chunkCount == 0) { + // There's no overlap between the old and new elements because at least one is empty. + currentManifestChunkOffset += currentElementChunkCount; + } else { + long currentElementEndTimeUs = currentElement.getStartTimeUs(currentElementChunkCount - 1) + + currentElement.getChunkDurationUs(currentElementChunkCount - 1); + long newElementStartTimeUs = newElement.getStartTimeUs(0); + if (currentElementEndTimeUs <= newElementStartTimeUs) { + // There's no overlap between the old and new elements. + currentManifestChunkOffset += currentElementChunkCount; + } else { + // The new element overlaps with the old one. + currentManifestChunkOffset += currentElement.getChunkIndex(newElementStartTimeUs); + } + } + currentManifest = newManifest; + needManifestRefresh = false; + } + + if (needManifestRefresh && (SystemClock.elapsedRealtime() + > manifestFetcher.getManifestLoadStartTimestamp() + MINIMUM_MANIFEST_REFRESH_PERIOD_MS)) { + manifestFetcher.requestRefresh(); + } + } + + @Override + public final void getChunkOperation(List queue, long playbackPositionUs, + ChunkOperationHolder out) { + if (fatalError != null) { + out.chunk = null; + return; + } + + evaluation.queueSize = queue.size(); + if (enabledTrack.isAdaptive()) { + adaptiveFormatEvaluator.evaluate(queue, playbackPositionUs, enabledTrack.adaptiveFormats, + evaluation); + } else { + evaluation.format = enabledTrack.fixedFormat; + evaluation.trigger = Chunk.TRIGGER_MANUAL; + } + + Format selectedFormat = evaluation.format; + out.queueSize = evaluation.queueSize; + + if (selectedFormat == null) { + out.chunk = null; + return; + } else if (out.queueSize == queue.size() && out.chunk != null + && out.chunk.format.equals(selectedFormat)) { + // We already have a chunk, and the evaluation hasn't changed either the format or the size + // of the queue. Leave unchanged. + return; + } + + // In all cases where we return before instantiating a new chunk, we want out.chunk to be null. + out.chunk = null; + + StreamElement streamElement = currentManifest.streamElements[enabledTrack.elementIndex]; + if (streamElement.chunkCount == 0) { + if (currentManifest.isLive) { + needManifestRefresh = true; + } else { + out.endOfStream = true; + } + return; + } + + int chunkIndex; + if (queue.isEmpty()) { + if (live) { + playbackPositionUs = getLiveSeekPosition(currentManifest, liveEdgeLatencyUs); + } + chunkIndex = streamElement.getChunkIndex(playbackPositionUs); + } else { + MediaChunk previous = queue.get(out.queueSize - 1); + chunkIndex = previous.chunkIndex + 1 - currentManifestChunkOffset; + } + + if (live && chunkIndex < 0) { + // This is before the first chunk in the current manifest. + fatalError = new BehindLiveWindowException(); + return; + } else if (currentManifest.isLive) { + if (chunkIndex >= streamElement.chunkCount) { + // This is beyond the last chunk in the current manifest. + needManifestRefresh = true; + return; + } else if (chunkIndex == streamElement.chunkCount - 1) { + // This is the last chunk in the current manifest. Mark the manifest as being finished, + // but continue to return the final chunk. + needManifestRefresh = true; + } + } else if (chunkIndex >= streamElement.chunkCount) { + out.endOfStream = true; + return; + } + + boolean isLastChunk = !currentManifest.isLive && chunkIndex == streamElement.chunkCount - 1; + long chunkStartTimeUs = streamElement.getStartTimeUs(chunkIndex); + long chunkEndTimeUs = isLastChunk ? -1 + : chunkStartTimeUs + streamElement.getChunkDurationUs(chunkIndex); + int currentAbsoluteChunkIndex = chunkIndex + currentManifestChunkOffset; + + int manifestTrackIndex = getManifestTrackIndex(streamElement, selectedFormat); + int manifestTrackKey = getManifestTrackKey(enabledTrack.elementIndex, manifestTrackIndex); + Uri uri = streamElement.buildRequestUri(manifestTrackIndex, chunkIndex); + Chunk mediaChunk = newMediaChunk(selectedFormat, uri, null, + extractorWrappers.get(manifestTrackKey), drmInitData, dataSource, currentAbsoluteChunkIndex, + chunkStartTimeUs, chunkEndTimeUs, evaluation.trigger, mediaFormats.get(manifestTrackKey), + enabledTrack.adaptiveMaxWidth, enabledTrack.adaptiveMaxHeight); + out.chunk = mediaChunk; + } + + @Override + public void onChunkLoadCompleted(Chunk chunk) { + // Do nothing. + } + + @Override + public void onChunkLoadError(Chunk chunk, Exception e) { + // Do nothing. + } + + @Override + public void disable(List queue) { + if (enabledTrack.isAdaptive()) { + adaptiveFormatEvaluator.disable(); + } + if (manifestFetcher != null) { + manifestFetcher.disable(); + } + evaluation.format = null; + fatalError = null; + } + + // SmoothStreamingTrackSelector.Output implementation. + + @Override + public void adaptiveTrack(SmoothStreamingManifest manifest, int element, int[] trackIndices) { + if (adaptiveFormatEvaluator == null) { + // Do nothing. + return; + } + MediaFormat maxHeightMediaFormat = null; + StreamElement streamElement = manifest.streamElements[element]; + int maxWidth = -1; + int maxHeight = -1; + Format[] formats = new Format[trackIndices.length]; + for (int i = 0; i < formats.length; i++) { + int manifestTrackIndex = trackIndices[i]; + formats[i] = streamElement.tracks[manifestTrackIndex].format; + MediaFormat mediaFormat = initManifestTrack(manifest, element, manifestTrackIndex); + if (maxHeightMediaFormat == null || mediaFormat.height > maxHeight) { + maxHeightMediaFormat = mediaFormat; + } + maxWidth = Math.max(maxWidth, mediaFormat.width); + maxHeight = Math.max(maxHeight, mediaFormat.height); + } + Arrays.sort(formats, new DecreasingBandwidthComparator()); + MediaFormat adaptiveMediaFormat = maxHeightMediaFormat.copyAsAdaptive(null); + tracks.add(new ExposedTrack(adaptiveMediaFormat, element, formats, maxWidth, maxHeight)); + } + + @Override + public void fixedTrack(SmoothStreamingManifest manifest, int element, int trackIndex) { + MediaFormat mediaFormat = initManifestTrack(manifest, element, trackIndex); + Format format = manifest.streamElements[element].tracks[trackIndex].format; + tracks.add(new ExposedTrack(mediaFormat, element, format)); + } + + // Private methods. + + private MediaFormat initManifestTrack(SmoothStreamingManifest manifest, int elementIndex, + int trackIndex) { + int manifestTrackKey = getManifestTrackKey(elementIndex, trackIndex); + MediaFormat mediaFormat = mediaFormats.get(manifestTrackKey); + if (mediaFormat != null) { + // Already initialized. + return mediaFormat; + } + + // Build the media format. + long durationUs = live ? C.UNKNOWN_TIME_US : manifest.durationUs; + StreamElement element = manifest.streamElements[elementIndex]; + Format format = element.tracks[trackIndex].format; + byte[][] csdArray = element.tracks[trackIndex].csd; + int mp4TrackType; + switch (element.type) { + case StreamElement.TYPE_VIDEO: + mediaFormat = MediaFormat.createVideoFormat(format.id, format.mimeType, format.bitrate, + MediaFormat.NO_VALUE, durationUs, format.width, format.height, Arrays.asList(csdArray)); + mp4TrackType = Track.TYPE_vide; + break; + case StreamElement.TYPE_AUDIO: + List csd; + if (csdArray != null) { + csd = Arrays.asList(csdArray); + } else { + csd = Collections.singletonList(CodecSpecificDataUtil.buildAacAudioSpecificConfig( + format.audioSamplingRate, format.audioChannels)); + } + mediaFormat = MediaFormat.createAudioFormat(format.id, format.mimeType, format.bitrate, + MediaFormat.NO_VALUE, durationUs, format.audioChannels, format.audioSamplingRate, csd, + format.language); + mp4TrackType = Track.TYPE_soun; + break; + case StreamElement.TYPE_TEXT: + mediaFormat = MediaFormat.createTextFormat(format.id, format.mimeType, format.bitrate, + durationUs, format.language); + mp4TrackType = Track.TYPE_text; + break; + default: + throw new IllegalStateException("Invalid type: " + element.type); + } + + Track mp4Track = new Track(trackIndex, mp4TrackType, element.timescale, C.UNKNOWN_TIME_US, + durationUs, mediaFormat, trackEncryptionBoxes, mp4TrackType == Track.TYPE_vide ? 4 : -1, + null, null); + // Build the extractor. + FragmentedMp4Extractor mp4Extractor = new FragmentedMp4Extractor( + FragmentedMp4Extractor.FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME + | FragmentedMp4Extractor.FLAG_WORKAROUND_IGNORE_TFDT_BOX, mp4Track); + + // Store the format and a wrapper around the extractor. + mediaFormats.put(manifestTrackKey, mediaFormat); + extractorWrappers.put(manifestTrackKey, new ChunkExtractorWrapper(mp4Extractor)); + return mediaFormat; + } + + /** + * For live playbacks, determines the seek position that snaps playback to be + * {@code liveEdgeLatencyUs} behind the live edge of the provided manifest. + * + * @param manifest The manifest. + * @param liveEdgeLatencyUs The live edge latency, in microseconds. + * @return The seek position in microseconds. + */ + private static long getLiveSeekPosition(SmoothStreamingManifest manifest, + long liveEdgeLatencyUs) { + long liveEdgeTimestampUs = Long.MIN_VALUE; + for (int i = 0; i < manifest.streamElements.length; i++) { + StreamElement streamElement = manifest.streamElements[i]; + if (streamElement.chunkCount > 0) { + long elementLiveEdgeTimestampUs = + streamElement.getStartTimeUs(streamElement.chunkCount - 1) + + streamElement.getChunkDurationUs(streamElement.chunkCount - 1); + liveEdgeTimestampUs = Math.max(liveEdgeTimestampUs, elementLiveEdgeTimestampUs); + } + } + return liveEdgeTimestampUs - liveEdgeLatencyUs; + } + + private static int getManifestTrackIndex(StreamElement element, Format format) { + TrackElement[] tracks = element.tracks; + for (int i = 0; i < tracks.length; i++) { + if (tracks[i].format.equals(format)) { + return i; + } + } + // Should never happen. + throw new IllegalStateException("Invalid format: " + format); + } + + private static MediaChunk newMediaChunk(Format formatInfo, Uri uri, String cacheKey, + ChunkExtractorWrapper extractorWrapper, DrmInitData drmInitData, DataSource dataSource, + int chunkIndex, long chunkStartTimeUs, long chunkEndTimeUs, int trigger, + MediaFormat mediaFormat, int adaptiveMaxWidth, int adaptiveMaxHeight) { + long offset = 0; + DataSpec dataSpec = new DataSpec(uri, offset, -1, cacheKey); + // In SmoothStreaming each chunk contains sample timestamps relative to the start of the chunk. + // To convert them the absolute timestamps, we need to set sampleOffsetUs to -chunkStartTimeUs. + return new ContainerMediaChunk(dataSource, dataSpec, trigger, formatInfo, chunkStartTimeUs, + chunkEndTimeUs, chunkIndex, chunkStartTimeUs, extractorWrapper, mediaFormat, + adaptiveMaxWidth, adaptiveMaxHeight, drmInitData, true, Chunk.NO_PARENT_ID); + } + + private static int getManifestTrackKey(int elementIndex, int trackIndex) { + Assertions.checkState(elementIndex <= 65536 && trackIndex <= 65536); + return (elementIndex << 16) | trackIndex; + } + + private static byte[] getProtectionElementKeyId(byte[] initData) { + StringBuilder initDataStringBuilder = new StringBuilder(); + for (int i = 0; i < initData.length; i += 2) { + initDataStringBuilder.append((char) initData[i]); + } + String initDataString = initDataStringBuilder.toString(); + String keyIdString = initDataString.substring( + initDataString.indexOf("") + 5, initDataString.indexOf("")); + byte[] keyId = Base64.decode(keyIdString, Base64.DEFAULT); + swap(keyId, 0, 3); + swap(keyId, 1, 2); + swap(keyId, 4, 5); + swap(keyId, 6, 7); + return keyId; + } + + private static void swap(byte[] data, int firstPosition, int secondPosition) { + byte temp = data[firstPosition]; + data[firstPosition] = data[secondPosition]; + data[secondPosition] = temp; + } + + // Private classes. + + private static final class ExposedTrack { + + public final MediaFormat trackFormat; + + private final int elementIndex; + + // Non-adaptive track variables. + private final Format fixedFormat; + + // Adaptive track variables. + private final Format[] adaptiveFormats; + private final int adaptiveMaxWidth; + private final int adaptiveMaxHeight; + + public ExposedTrack(MediaFormat trackFormat, int elementIndex, Format fixedFormat) { + this.trackFormat = trackFormat; + this.elementIndex = elementIndex; + this.fixedFormat = fixedFormat; + this.adaptiveFormats = null; + this.adaptiveMaxWidth = MediaFormat.NO_VALUE; + this.adaptiveMaxHeight = MediaFormat.NO_VALUE; + } + + public ExposedTrack(MediaFormat trackFormat, int elementIndex, Format[] adaptiveFormats, + int adaptiveMaxWidth, int adaptiveMaxHeight) { + this.trackFormat = trackFormat; + this.elementIndex = elementIndex; + this.adaptiveFormats = adaptiveFormats; + this.adaptiveMaxWidth = adaptiveMaxWidth; + this.adaptiveMaxHeight = adaptiveMaxHeight; + this.fixedFormat = null; + } + + public boolean isAdaptive() { + return adaptiveFormats != null; + } + + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/smoothstreaming/SmoothStreamingManifest.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/smoothstreaming/SmoothStreamingManifest.java new file mode 100755 index 000000000..20cb49120 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/smoothstreaming/SmoothStreamingManifest.java @@ -0,0 +1,256 @@ +/* + * 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.messenger.exoplayer.smoothstreaming; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.chunk.Format; +import org.telegram.messenger.exoplayer.chunk.FormatWrapper; +import org.telegram.messenger.exoplayer.util.Assertions; +import org.telegram.messenger.exoplayer.util.UriUtil; +import org.telegram.messenger.exoplayer.util.Util; + +import android.net.Uri; + +import java.util.List; +import java.util.UUID; + +/** + * Represents a SmoothStreaming manifest. + * + * @see + * IIS Smooth Streaming Client Manifest Format + */ +public class SmoothStreamingManifest { + + /** + * The client manifest major version. + */ + public final int majorVersion; + + /** + * The client manifest minor version. + */ + public final int minorVersion; + + /** + * The number of fragments in a lookahead, or -1 if the lookahead is unspecified. + */ + public final int lookAheadCount; + + /** + * True if the manifest describes a live presentation still in progress. False otherwise. + */ + public final boolean isLive; + + /** + * Content protection information, or null if the content is not protected. + */ + public final ProtectionElement protectionElement; + + /** + * The contained stream elements. + */ + public final StreamElement[] streamElements; + + /** + * The overall presentation duration of the media in microseconds, or {@link C#UNKNOWN_TIME_US} + * if the duration is unknown. + */ + public final long durationUs; + + /** + * The length of the trailing window for a live broadcast in microseconds, or + * {@link C#UNKNOWN_TIME_US} if the stream is not live or if the window length is unspecified. + */ + public final long dvrWindowLengthUs; + + /** + * @param majorVersion The client manifest major version. + * @param minorVersion The client manifest minor version. + * @param timescale The timescale of the media as the number of units that pass in one second. + * @param duration The overall presentation duration in units of the timescale attribute, or 0 + * if the duration is unknown. + * @param dvrWindowLength The length of the trailing window in units of the timescale attribute, + * or 0 if this attribute is unspecified or not applicable. + * @param lookAheadCount The number of fragments in a lookahead, or -1 if this attribute is + * unspecified or not applicable. + * @param isLive True if the manifest describes a live presentation still in progress. False + * otherwise. + * @param protectionElement Content protection information, or null if the content is not + * protected. + * @param streamElements The contained stream elements. + */ + public SmoothStreamingManifest(int majorVersion, int minorVersion, long timescale, long duration, + long dvrWindowLength, int lookAheadCount, boolean isLive, ProtectionElement protectionElement, + StreamElement[] streamElements) { + this.majorVersion = majorVersion; + this.minorVersion = minorVersion; + this.lookAheadCount = lookAheadCount; + this.isLive = isLive; + this.protectionElement = protectionElement; + this.streamElements = streamElements; + dvrWindowLengthUs = dvrWindowLength == 0 ? C.UNKNOWN_TIME_US + : Util.scaleLargeTimestamp(dvrWindowLength, C.MICROS_PER_SECOND, timescale); + durationUs = duration == 0 ? C.UNKNOWN_TIME_US + : Util.scaleLargeTimestamp(duration, C.MICROS_PER_SECOND, timescale); + } + + /** + * Represents a protection element containing a single header. + */ + public static class ProtectionElement { + + public final UUID uuid; + public final byte[] data; + + public ProtectionElement(UUID uuid, byte[] data) { + this.uuid = uuid; + this.data = data; + } + + } + + /** + * Represents a QualityLevel element. + */ + public static class TrackElement implements FormatWrapper { + + public final Format format; + public final byte[][] csd; + + public TrackElement(int index, int bitrate, String mimeType, byte[][] csd, int maxWidth, + int maxHeight, int sampleRate, int numChannels, String language) { + this.csd = csd; + format = new Format(String.valueOf(index), mimeType, maxWidth, maxHeight, -1, numChannels, + sampleRate, bitrate, language); + } + + @Override + public Format getFormat() { + return format; + } + + } + + /** + * Represents a StreamIndex element. + */ + public static class StreamElement { + + public static final int TYPE_UNKNOWN = -1; + public static final int TYPE_AUDIO = 0; + public static final int TYPE_VIDEO = 1; + public static final int TYPE_TEXT = 2; + + private static final String URL_PLACEHOLDER_START_TIME = "{start time}"; + private static final String URL_PLACEHOLDER_BITRATE = "{bitrate}"; + + public final int type; + public final String subType; + public final long timescale; + public final String name; + public final int qualityLevels; + public final int maxWidth; + public final int maxHeight; + public final int displayWidth; + public final int displayHeight; + public final String language; + public final TrackElement[] tracks; + public final int chunkCount; + + private final String baseUri; + private final String chunkTemplate; + + private final List chunkStartTimes; + private final long[] chunkStartTimesUs; + private final long lastChunkDurationUs; + + public StreamElement(String baseUri, String chunkTemplate, int type, String subType, + long timescale, String name, int qualityLevels, int maxWidth, int maxHeight, + int displayWidth, int displayHeight, String language, TrackElement[] tracks, + List chunkStartTimes, long lastChunkDuration) { + this.baseUri = baseUri; + this.chunkTemplate = chunkTemplate; + this.type = type; + this.subType = subType; + this.timescale = timescale; + this.name = name; + this.qualityLevels = qualityLevels; + this.maxWidth = maxWidth; + this.maxHeight = maxHeight; + this.displayWidth = displayWidth; + this.displayHeight = displayHeight; + this.language = language; + this.tracks = tracks; + this.chunkCount = chunkStartTimes.size(); + this.chunkStartTimes = chunkStartTimes; + lastChunkDurationUs = + Util.scaleLargeTimestamp(lastChunkDuration, C.MICROS_PER_SECOND, timescale); + chunkStartTimesUs = + Util.scaleLargeTimestamps(chunkStartTimes, C.MICROS_PER_SECOND, timescale); + } + + /** + * Gets the index of the chunk that contains the specified time. + * + * @param timeUs The time in microseconds. + * @return The index of the corresponding chunk. + */ + public int getChunkIndex(long timeUs) { + return Util.binarySearchFloor(chunkStartTimesUs, timeUs, true, true); + } + + /** + * Gets the start time of the specified chunk. + * + * @param chunkIndex The index of the chunk. + * @return The start time of the chunk, in microseconds. + */ + public long getStartTimeUs(int chunkIndex) { + return chunkStartTimesUs[chunkIndex]; + } + + /** + * Gets the duration of the specified chunk. + * + * @param chunkIndex The index of the chunk. + * @return The duration of the chunk, in microseconds. + */ + public long getChunkDurationUs(int chunkIndex) { + return (chunkIndex == chunkCount - 1) ? lastChunkDurationUs + : chunkStartTimesUs[chunkIndex + 1] - chunkStartTimesUs[chunkIndex]; + } + + /** + * Builds a uri for requesting the specified chunk of the specified track. + * + * @param track The index of the track for which to build the URL. + * @param chunkIndex The index of the chunk for which to build the URL. + * @return The request uri. + */ + public Uri buildRequestUri(int track, int chunkIndex) { + Assertions.checkState(tracks != null); + Assertions.checkState(chunkStartTimes != null); + Assertions.checkState(chunkIndex < chunkStartTimes.size()); + String chunkUrl = chunkTemplate + .replace(URL_PLACEHOLDER_BITRATE, Integer.toString(tracks[track].format.bitrate)) + .replace(URL_PLACEHOLDER_START_TIME, chunkStartTimes.get(chunkIndex).toString()); + return UriUtil.resolveToUri(baseUri, chunkUrl); + } + + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/smoothstreaming/SmoothStreamingManifestParser.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/smoothstreaming/SmoothStreamingManifestParser.java new file mode 100755 index 000000000..5acb9f3e2 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/smoothstreaming/SmoothStreamingManifestParser.java @@ -0,0 +1,702 @@ +/* + * 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.messenger.exoplayer.smoothstreaming; + +import org.telegram.messenger.exoplayer.ParserException; +import org.telegram.messenger.exoplayer.extractor.mp4.PsshAtomUtil; +import org.telegram.messenger.exoplayer.smoothstreaming.SmoothStreamingManifest.ProtectionElement; +import org.telegram.messenger.exoplayer.smoothstreaming.SmoothStreamingManifest.StreamElement; +import org.telegram.messenger.exoplayer.smoothstreaming.SmoothStreamingManifest.TrackElement; +import org.telegram.messenger.exoplayer.upstream.UriLoadable; +import org.telegram.messenger.exoplayer.util.Assertions; +import org.telegram.messenger.exoplayer.util.CodecSpecificDataUtil; +import org.telegram.messenger.exoplayer.util.MimeTypes; +import org.telegram.messenger.exoplayer.util.Util; + +import android.util.Base64; +import android.util.Pair; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlPullParserFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.UUID; + +/** + * Parses SmoothStreaming client manifests. + * + * @see + * IIS Smooth Streaming Client Manifest Format + */ +public class SmoothStreamingManifestParser implements UriLoadable.Parser { + + private final XmlPullParserFactory xmlParserFactory; + + public SmoothStreamingManifestParser() { + try { + xmlParserFactory = XmlPullParserFactory.newInstance(); + } catch (XmlPullParserException e) { + throw new RuntimeException("Couldn't create XmlPullParserFactory instance", e); + } + } + + @Override + public SmoothStreamingManifest parse(String connectionUrl, InputStream inputStream) + throws IOException, ParserException { + try { + XmlPullParser xmlParser = xmlParserFactory.newPullParser(); + xmlParser.setInput(inputStream, null); + SmoothStreamMediaParser smoothStreamMediaParser = + new SmoothStreamMediaParser(null, connectionUrl); + return (SmoothStreamingManifest) smoothStreamMediaParser.parse(xmlParser); + } catch (XmlPullParserException e) { + throw new ParserException(e); + } + } + + /** + * Thrown if a required field is missing. + */ + public static class MissingFieldException extends ParserException { + + public MissingFieldException(String fieldName) { + super("Missing required field: " + fieldName); + } + + } + + /** + * A base class for parsers that parse components of a smooth streaming manifest. + */ + private static abstract class ElementParser { + + private final String baseUri; + private final String tag; + + private final ElementParser parent; + private final List> normalizedAttributes; + + public ElementParser(ElementParser parent, String baseUri, String tag) { + this.parent = parent; + this.baseUri = baseUri; + this.tag = tag; + this.normalizedAttributes = new LinkedList<>(); + } + + public final Object parse(XmlPullParser xmlParser) throws XmlPullParserException, IOException, + ParserException { + String tagName; + boolean foundStartTag = false; + int skippingElementDepth = 0; + while (true) { + int eventType = xmlParser.getEventType(); + switch (eventType) { + case XmlPullParser.START_TAG: + tagName = xmlParser.getName(); + if (tag.equals(tagName)) { + foundStartTag = true; + parseStartTag(xmlParser); + } else if (foundStartTag) { + if (skippingElementDepth > 0) { + skippingElementDepth++; + } else if (handleChildInline(tagName)) { + parseStartTag(xmlParser); + } else { + ElementParser childElementParser = newChildParser(this, tagName, baseUri); + if (childElementParser == null) { + skippingElementDepth = 1; + } else { + addChild(childElementParser.parse(xmlParser)); + } + } + } + break; + case XmlPullParser.TEXT: + if (foundStartTag && skippingElementDepth == 0) { + parseText(xmlParser); + } + break; + case XmlPullParser.END_TAG: + if (foundStartTag) { + if (skippingElementDepth > 0) { + skippingElementDepth--; + } else { + tagName = xmlParser.getName(); + parseEndTag(xmlParser); + if (!handleChildInline(tagName)) { + return build(); + } + } + } + break; + case XmlPullParser.END_DOCUMENT: + return null; + default: + // Do nothing. + break; + } + xmlParser.next(); + } + } + + private ElementParser newChildParser(ElementParser parent, String name, String baseUri) { + if (TrackElementParser.TAG.equals(name)) { + return new TrackElementParser(parent, baseUri); + } else if (ProtectionElementParser.TAG.equals(name)) { + return new ProtectionElementParser(parent, baseUri); + } else if (StreamElementParser.TAG.equals(name)) { + return new StreamElementParser(parent, baseUri); + } + return null; + } + + /** + * Stash an attribute that may be normalized at this level. In other words, an attribute that + * may have been pulled up from the child elements because its value was the same in all + * children. + *

+ * Stashing an attribute allows child element parsers to retrieve the values of normalized + * attributes using {@link #getNormalizedAttribute(String)}. + * + * @param key The name of the attribute. + * @param value The value of the attribute. + */ + protected final void putNormalizedAttribute(String key, Object value) { + normalizedAttributes.add(Pair.create(key, value)); + } + + /** + * Attempt to retrieve a stashed normalized attribute. If there is no stashed attribute with + * the provided name, the parent element parser will be queried, and so on up the chain. + * + * @param key The name of the attribute. + * @return The stashed value, or null if the attribute was not be found. + */ + protected final Object getNormalizedAttribute(String key) { + for (int i = 0; i < normalizedAttributes.size(); i++) { + Pair pair = normalizedAttributes.get(i); + if (pair.first.equals(key)) { + return pair.second; + } + } + return parent == null ? null : parent.getNormalizedAttribute(key); + } + + /** + * Whether this {@link ElementParser} parses a child element inline. + * + * @param tagName The name of the child element. + * @return Whether the child is parsed inline. + */ + protected boolean handleChildInline(String tagName) { + return false; + } + + /** + * @param xmlParser The underlying {@link XmlPullParser} + * @throws ParserException + */ + protected void parseStartTag(XmlPullParser xmlParser) throws ParserException { + // Do nothing. + } + + /** + * @param xmlParser The underlying {@link XmlPullParser} + * @throws ParserException + */ + protected void parseText(XmlPullParser xmlParser) throws ParserException { + // Do nothing. + } + + /** + * @param xmlParser The underlying {@link XmlPullParser} + * @throws ParserException + */ + protected void parseEndTag(XmlPullParser xmlParser) throws ParserException { + // Do nothing. + } + + /** + * @param parsedChild A parsed child object. + */ + protected void addChild(Object parsedChild) { + // Do nothing. + } + + protected abstract Object build(); + + protected final String parseRequiredString(XmlPullParser parser, String key) + throws MissingFieldException { + String value = parser.getAttributeValue(null, key); + if (value != null) { + return value; + } else { + throw new MissingFieldException(key); + } + } + + protected final int parseInt(XmlPullParser parser, String key, int defaultValue) + throws ParserException { + String value = parser.getAttributeValue(null, key); + if (value != null) { + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + throw new ParserException(e); + } + } else { + return defaultValue; + } + } + + protected final int parseRequiredInt(XmlPullParser parser, String key) throws ParserException { + String value = parser.getAttributeValue(null, key); + if (value != null) { + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + throw new ParserException(e); + } + } else { + throw new MissingFieldException(key); + } + } + + protected final long parseLong(XmlPullParser parser, String key, long defaultValue) + throws ParserException { + String value = parser.getAttributeValue(null, key); + if (value != null) { + try { + return Long.parseLong(value); + } catch (NumberFormatException e) { + throw new ParserException(e); + } + } else { + return defaultValue; + } + } + + protected final long parseRequiredLong(XmlPullParser parser, String key) + throws ParserException { + String value = parser.getAttributeValue(null, key); + if (value != null) { + try { + return Long.parseLong(value); + } catch (NumberFormatException e) { + throw new ParserException(e); + } + } else { + throw new MissingFieldException(key); + } + } + + protected final boolean parseBoolean(XmlPullParser parser, String key, boolean defaultValue) { + String value = parser.getAttributeValue(null, key); + if (value != null) { + return Boolean.parseBoolean(value); + } else { + return defaultValue; + } + } + + } + + private static class SmoothStreamMediaParser extends ElementParser { + + public static final String TAG = "SmoothStreamingMedia"; + + private static final String KEY_MAJOR_VERSION = "MajorVersion"; + private static final String KEY_MINOR_VERSION = "MinorVersion"; + private static final String KEY_TIME_SCALE = "TimeScale"; + private static final String KEY_DVR_WINDOW_LENGTH = "DVRWindowLength"; + private static final String KEY_DURATION = "Duration"; + private static final String KEY_LOOKAHEAD_COUNT = "LookaheadCount"; + private static final String KEY_IS_LIVE = "IsLive"; + + private int majorVersion; + private int minorVersion; + private long timescale; + private long duration; + private long dvrWindowLength; + private int lookAheadCount; + private boolean isLive; + private ProtectionElement protectionElement; + private List streamElements; + + public SmoothStreamMediaParser(ElementParser parent, String baseUri) { + super(parent, baseUri, TAG); + lookAheadCount = -1; + protectionElement = null; + streamElements = new LinkedList<>(); + } + + @Override + public void parseStartTag(XmlPullParser parser) throws ParserException { + majorVersion = parseRequiredInt(parser, KEY_MAJOR_VERSION); + minorVersion = parseRequiredInt(parser, KEY_MINOR_VERSION); + timescale = parseLong(parser, KEY_TIME_SCALE, 10000000L); + duration = parseRequiredLong(parser, KEY_DURATION); + dvrWindowLength = parseLong(parser, KEY_DVR_WINDOW_LENGTH, 0); + lookAheadCount = parseInt(parser, KEY_LOOKAHEAD_COUNT, -1); + isLive = parseBoolean(parser, KEY_IS_LIVE, false); + putNormalizedAttribute(KEY_TIME_SCALE, timescale); + } + + @Override + public void addChild(Object child) { + if (child instanceof StreamElement) { + streamElements.add((StreamElement) child); + } else if (child instanceof ProtectionElement) { + Assertions.checkState(protectionElement == null); + protectionElement = (ProtectionElement) child; + } + } + + @Override + public Object build() { + StreamElement[] streamElementArray = new StreamElement[streamElements.size()]; + streamElements.toArray(streamElementArray); + return new SmoothStreamingManifest(majorVersion, minorVersion, timescale, duration, + dvrWindowLength, lookAheadCount, isLive, protectionElement, streamElementArray); + } + + } + + private static class ProtectionElementParser extends ElementParser { + + public static final String TAG = "Protection"; + public static final String TAG_PROTECTION_HEADER = "ProtectionHeader"; + + public static final String KEY_SYSTEM_ID = "SystemID"; + + private boolean inProtectionHeader; + private UUID uuid; + private byte[] initData; + + public ProtectionElementParser(ElementParser parent, String baseUri) { + super(parent, baseUri, TAG); + } + + @Override + public boolean handleChildInline(String tag) { + return TAG_PROTECTION_HEADER.equals(tag); + } + + @Override + public void parseStartTag(XmlPullParser parser) { + if (TAG_PROTECTION_HEADER.equals(parser.getName())) { + inProtectionHeader = true; + String uuidString = parser.getAttributeValue(null, KEY_SYSTEM_ID); + uuidString = stripCurlyBraces(uuidString); + uuid = UUID.fromString(uuidString); + } + } + + @Override + public void parseText(XmlPullParser parser) { + if (inProtectionHeader) { + initData = Base64.decode(parser.getText(), Base64.DEFAULT); + } + } + + @Override + public void parseEndTag(XmlPullParser parser) { + if (TAG_PROTECTION_HEADER.equals(parser.getName())) { + inProtectionHeader = false; + } + } + + @Override + public Object build() { + return new ProtectionElement(uuid, PsshAtomUtil.buildPsshAtom(uuid, initData)); + } + + private static String stripCurlyBraces(String uuidString) { + if (uuidString.charAt(0) == '{' && uuidString.charAt(uuidString.length() - 1) == '}') { + uuidString = uuidString.substring(1, uuidString.length() - 1); + } + return uuidString; + } + } + + private static class StreamElementParser extends ElementParser { + + public static final String TAG = "StreamIndex"; + private static final String TAG_STREAM_FRAGMENT = "c"; + + private static final String KEY_TYPE = "Type"; + private static final String KEY_TYPE_AUDIO = "audio"; + private static final String KEY_TYPE_VIDEO = "video"; + private static final String KEY_TYPE_TEXT = "text"; + private static final String KEY_SUB_TYPE = "Subtype"; + private static final String KEY_NAME = "Name"; + private static final String KEY_QUALITY_LEVELS = "QualityLevels"; + private static final String KEY_URL = "Url"; + private static final String KEY_MAX_WIDTH = "MaxWidth"; + private static final String KEY_MAX_HEIGHT = "MaxHeight"; + private static final String KEY_DISPLAY_WIDTH = "DisplayWidth"; + private static final String KEY_DISPLAY_HEIGHT = "DisplayHeight"; + private static final String KEY_LANGUAGE = "Language"; + private static final String KEY_TIME_SCALE = "TimeScale"; + + private static final String KEY_FRAGMENT_DURATION = "d"; + private static final String KEY_FRAGMENT_START_TIME = "t"; + private static final String KEY_FRAGMENT_REPEAT_COUNT = "r"; + + private final String baseUri; + private final List tracks; + + private int type; + private String subType; + private long timescale; + private String name; + private int qualityLevels; + private String url; + private int maxWidth; + private int maxHeight; + private int displayWidth; + private int displayHeight; + private String language; + private ArrayList startTimes; + + private long lastChunkDuration; + + public StreamElementParser(ElementParser parent, String baseUri) { + super(parent, baseUri, TAG); + this.baseUri = baseUri; + tracks = new LinkedList<>(); + } + + @Override + public boolean handleChildInline(String tag) { + return TAG_STREAM_FRAGMENT.equals(tag); + } + + @Override + public void parseStartTag(XmlPullParser parser) throws ParserException { + if (TAG_STREAM_FRAGMENT.equals(parser.getName())) { + parseStreamFragmentStartTag(parser); + } else { + parseStreamElementStartTag(parser); + } + } + + private void parseStreamFragmentStartTag(XmlPullParser parser) throws ParserException { + int chunkIndex = startTimes.size(); + long startTime = parseLong(parser, KEY_FRAGMENT_START_TIME, -1L); + if (startTime == -1L) { + if (chunkIndex == 0) { + // Assume the track starts at t = 0. + startTime = 0; + } else if (lastChunkDuration != -1L) { + // Infer the start time from the previous chunk's start time and duration. + startTime = startTimes.get(chunkIndex - 1) + lastChunkDuration; + } else { + // We don't have the start time, and we're unable to infer it. + throw new ParserException("Unable to infer start time"); + } + } + chunkIndex++; + startTimes.add(startTime); + lastChunkDuration = parseLong(parser, KEY_FRAGMENT_DURATION, -1L); + // Handle repeated chunks. + long repeatCount = parseLong(parser, KEY_FRAGMENT_REPEAT_COUNT, 1L); + if (repeatCount > 1 && lastChunkDuration == -1L) { + throw new ParserException("Repeated chunk with unspecified duration"); + } + for (int i = 1; i < repeatCount; i++) { + chunkIndex++; + startTimes.add(startTime + (lastChunkDuration * i)); + } + } + + private void parseStreamElementStartTag(XmlPullParser parser) throws ParserException { + type = parseType(parser); + putNormalizedAttribute(KEY_TYPE, type); + if (type == StreamElement.TYPE_TEXT) { + subType = parseRequiredString(parser, KEY_SUB_TYPE); + } else { + subType = parser.getAttributeValue(null, KEY_SUB_TYPE); + } + name = parser.getAttributeValue(null, KEY_NAME); + qualityLevels = parseInt(parser, KEY_QUALITY_LEVELS, -1); + url = parseRequiredString(parser, KEY_URL); + maxWidth = parseInt(parser, KEY_MAX_WIDTH, -1); + maxHeight = parseInt(parser, KEY_MAX_HEIGHT, -1); + displayWidth = parseInt(parser, KEY_DISPLAY_WIDTH, -1); + displayHeight = parseInt(parser, KEY_DISPLAY_HEIGHT, -1); + language = parser.getAttributeValue(null, KEY_LANGUAGE); + putNormalizedAttribute(KEY_LANGUAGE, language); + timescale = parseInt(parser, KEY_TIME_SCALE, -1); + if (timescale == -1) { + timescale = (Long) getNormalizedAttribute(KEY_TIME_SCALE); + } + startTimes = new ArrayList<>(); + } + + private int parseType(XmlPullParser parser) throws ParserException { + String value = parser.getAttributeValue(null, KEY_TYPE); + if (value != null) { + if (KEY_TYPE_AUDIO.equalsIgnoreCase(value)) { + return StreamElement.TYPE_AUDIO; + } else if (KEY_TYPE_VIDEO.equalsIgnoreCase(value)) { + return StreamElement.TYPE_VIDEO; + } else if (KEY_TYPE_TEXT.equalsIgnoreCase(value)) { + return StreamElement.TYPE_TEXT; + } else { + throw new ParserException("Invalid key value[" + value + "]"); + } + } + throw new MissingFieldException(KEY_TYPE); + } + + @Override + public void addChild(Object child) { + if (child instanceof TrackElement) { + tracks.add((TrackElement) child); + } + } + + @Override + public Object build() { + TrackElement[] trackElements = new TrackElement[tracks.size()]; + tracks.toArray(trackElements); + return new StreamElement(baseUri, url, type, subType, timescale, name, qualityLevels, + maxWidth, maxHeight, displayWidth, displayHeight, language, trackElements, startTimes, + lastChunkDuration); + } + + } + + private static class TrackElementParser extends ElementParser { + + public static final String TAG = "QualityLevel"; + + private static final String KEY_INDEX = "Index"; + private static final String KEY_BITRATE = "Bitrate"; + private static final String KEY_CODEC_PRIVATE_DATA = "CodecPrivateData"; + private static final String KEY_SAMPLING_RATE = "SamplingRate"; + private static final String KEY_CHANNELS = "Channels"; + private static final String KEY_FOUR_CC = "FourCC"; + private static final String KEY_TYPE = "Type"; + private static final String KEY_LANGUAGE = "Language"; + private static final String KEY_MAX_WIDTH = "MaxWidth"; + private static final String KEY_MAX_HEIGHT = "MaxHeight"; + + private final List csd; + + private int index; + private int bitrate; + private String mimeType; + private int maxWidth; + private int maxHeight; + private int samplingRate; + private int channels; + private String language; + + public TrackElementParser(ElementParser parent, String baseUri) { + super(parent, baseUri, TAG); + this.csd = new LinkedList<>(); + } + + @Override + public void parseStartTag(XmlPullParser parser) throws ParserException { + int type = (Integer) getNormalizedAttribute(KEY_TYPE); + String value; + + index = parseInt(parser, KEY_INDEX, -1); + bitrate = parseRequiredInt(parser, KEY_BITRATE); + language = (String) getNormalizedAttribute(KEY_LANGUAGE); + + if (type == StreamElement.TYPE_VIDEO) { + maxHeight = parseRequiredInt(parser, KEY_MAX_HEIGHT); + maxWidth = parseRequiredInt(parser, KEY_MAX_WIDTH); + mimeType = fourCCToMimeType(parseRequiredString(parser, KEY_FOUR_CC)); + } else { + maxHeight = -1; + maxWidth = -1; + String fourCC = parser.getAttributeValue(null, KEY_FOUR_CC); + // If fourCC is missing and the stream type is audio, we assume AAC. + mimeType = fourCC != null ? fourCCToMimeType(fourCC) + : type == StreamElement.TYPE_AUDIO ? MimeTypes.AUDIO_AAC : null; + } + + if (type == StreamElement.TYPE_AUDIO) { + samplingRate = parseRequiredInt(parser, KEY_SAMPLING_RATE); + channels = parseRequiredInt(parser, KEY_CHANNELS); + } else { + samplingRate = -1; + channels = -1; + } + + value = parser.getAttributeValue(null, KEY_CODEC_PRIVATE_DATA); + if (value != null && value.length() > 0) { + byte[] codecPrivateData = Util.getBytesFromHexString(value); + byte[][] split = CodecSpecificDataUtil.splitNalUnits(codecPrivateData); + if (split == null) { + csd.add(codecPrivateData); + } else { + for (int i = 0; i < split.length; i++) { + csd.add(split[i]); + } + } + } + } + + @Override + public Object build() { + byte[][] csdArray = null; + if (!csd.isEmpty()) { + csdArray = new byte[csd.size()][]; + csd.toArray(csdArray); + } + return new TrackElement(index, bitrate, mimeType, csdArray, maxWidth, maxHeight, samplingRate, + channels, language); + } + + private static String fourCCToMimeType(String fourCC) { + if (fourCC.equalsIgnoreCase("H264") || fourCC.equalsIgnoreCase("X264") + || fourCC.equalsIgnoreCase("AVC1") || fourCC.equalsIgnoreCase("DAVC")) { + return MimeTypes.VIDEO_H264; + } else if (fourCC.equalsIgnoreCase("AAC") || fourCC.equalsIgnoreCase("AACL") + || fourCC.equalsIgnoreCase("AACH") || fourCC.equalsIgnoreCase("AACP")) { + return MimeTypes.AUDIO_AAC; + } else if (fourCC.equalsIgnoreCase("TTML")) { + return MimeTypes.APPLICATION_TTML; + } else if (fourCC.equalsIgnoreCase("ac-3") || fourCC.equalsIgnoreCase("dac3")) { + return MimeTypes.AUDIO_AC3; + } else if (fourCC.equalsIgnoreCase("ec-3") || fourCC.equalsIgnoreCase("dec3")) { + return MimeTypes.AUDIO_E_AC3; + } else if (fourCC.equalsIgnoreCase("dtsc")) { + return MimeTypes.AUDIO_DTS; + } else if (fourCC.equalsIgnoreCase("dtsh") || fourCC.equalsIgnoreCase("dtsl")) { + return MimeTypes.AUDIO_DTS_HD; + } else if (fourCC.equalsIgnoreCase("dtse")) { + return MimeTypes.AUDIO_DTS_EXPRESS; + } else if (fourCC.equalsIgnoreCase("opus")) { + return MimeTypes.AUDIO_OPUS; + } + return null; + } + + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/smoothstreaming/SmoothStreamingTrackSelector.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/smoothstreaming/SmoothStreamingTrackSelector.java new file mode 100755 index 000000000..3dd92c87b --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/smoothstreaming/SmoothStreamingTrackSelector.java @@ -0,0 +1,59 @@ +/* + * 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.messenger.exoplayer.smoothstreaming; + +import java.io.IOException; + +/** + * Specifies a track selection from a {@link SmoothStreamingManifest}. + */ +public interface SmoothStreamingTrackSelector { + + /** + * Defines a selector output. + */ + interface Output { + + /** + * Outputs an adaptive track, covering the specified tracks in the specified element. + * + * @param manifest The manifest being processed. + * @param element The index of the element within which the adaptive tracks are located. + * @param tracks The indices of the tracks within the element. + */ + void adaptiveTrack(SmoothStreamingManifest manifest, int element, int[] tracks); + + /** + * Outputs a fixed track corresponding to the specified track in the specified element. + * + * @param manifest The manifest being processed. + * @param element The index of the element within which the track is located. + * @param track The index of the track within the element. + */ + void fixedTrack(SmoothStreamingManifest manifest, int element, int track); + + } + + /** + * Outputs a track selection for a given manifest. + * + * @param manifest The manifest to process. + * @param output The output to receive tracks. + * @throws IOException If an error occurs processing the manifest. + */ + void selectTracks(SmoothStreamingManifest manifest, Output output) throws IOException; + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/CaptionStyleCompat.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/CaptionStyleCompat.java new file mode 100755 index 000000000..de379f5c4 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/CaptionStyleCompat.java @@ -0,0 +1,160 @@ +/* + * 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.messenger.exoplayer.text; + +import org.telegram.messenger.exoplayer.util.Util; + +import android.annotation.TargetApi; +import android.graphics.Color; +import android.graphics.Typeface; +import android.view.accessibility.CaptioningManager; +import android.view.accessibility.CaptioningManager.CaptionStyle; + +/** + * A compatibility wrapper for {@link CaptionStyle}. + */ +public final class CaptionStyleCompat { + + /** + * Edge type value specifying no character edges. + */ + public static final int EDGE_TYPE_NONE = 0; + + /** + * Edge type value specifying uniformly outlined character edges. + */ + public static final int EDGE_TYPE_OUTLINE = 1; + + /** + * Edge type value specifying drop-shadowed character edges. + */ + public static final int EDGE_TYPE_DROP_SHADOW = 2; + + /** + * Edge type value specifying raised bevel character edges. + */ + public static final int EDGE_TYPE_RAISED = 3; + + /** + * Edge type value specifying depressed bevel character edges. + */ + public static final int EDGE_TYPE_DEPRESSED = 4; + + /** + * Use color setting specified by the track and fallback to default caption style. + */ + public static final int USE_TRACK_COLOR_SETTINGS = 1; + + /** + * Default caption style. + */ + public static final CaptionStyleCompat DEFAULT = new CaptionStyleCompat( + Color.WHITE, Color.BLACK, Color.TRANSPARENT, EDGE_TYPE_NONE, Color.WHITE, null); + + /** + * The preferred foreground color. + */ + public final int foregroundColor; + + /** + * The preferred background color. + */ + public final int backgroundColor; + + /** + * The preferred window color. + */ + public final int windowColor; + + /** + * The preferred edge type. One of: + *

    + *
  • {@link #EDGE_TYPE_NONE} + *
  • {@link #EDGE_TYPE_OUTLINE} + *
  • {@link #EDGE_TYPE_DROP_SHADOW} + *
  • {@link #EDGE_TYPE_RAISED} + *
  • {@link #EDGE_TYPE_DEPRESSED} + *
+ */ + public final int edgeType; + + /** + * The preferred edge color, if using an edge type other than {@link #EDGE_TYPE_NONE}. + */ + public final int edgeColor; + + /** + * The preferred typeface. + */ + public final Typeface typeface; + + /** + * Creates a {@link CaptionStyleCompat} equivalent to a provided {@link CaptionStyle}. + * + * @param captionStyle A {@link CaptionStyle}. + * @return The equivalent {@link CaptionStyleCompat}. + */ + @TargetApi(19) + public static CaptionStyleCompat createFromCaptionStyle( + CaptioningManager.CaptionStyle captionStyle) { + if (Util.SDK_INT >= 21) { + return createFromCaptionStyleV21(captionStyle); + } else { + // Note - Any caller must be on at least API level 19 or greater (because CaptionStyle did + // not exist in earlier API levels). + return createFromCaptionStyleV19(captionStyle); + } + } + + /** + * @param foregroundColor See {@link #foregroundColor}. + * @param backgroundColor See {@link #backgroundColor}. + * @param windowColor See {@link #windowColor}. + * @param edgeType See {@link #edgeType}. + * @param edgeColor See {@link #edgeColor}. + * @param typeface See {@link #typeface}. + */ + public CaptionStyleCompat(int foregroundColor, int backgroundColor, int windowColor, int edgeType, + int edgeColor, Typeface typeface) { + this.foregroundColor = foregroundColor; + this.backgroundColor = backgroundColor; + this.windowColor = windowColor; + this.edgeType = edgeType; + this.edgeColor = edgeColor; + this.typeface = typeface; + } + + @TargetApi(19) + private static CaptionStyleCompat createFromCaptionStyleV19( + CaptioningManager.CaptionStyle captionStyle) { + return new CaptionStyleCompat( + captionStyle.foregroundColor, captionStyle.backgroundColor, Color.TRANSPARENT, + captionStyle.edgeType, captionStyle.edgeColor, captionStyle.getTypeface()); + } + + @TargetApi(21) + private static CaptionStyleCompat createFromCaptionStyleV21( + CaptioningManager.CaptionStyle captionStyle) { + return new CaptionStyleCompat( + captionStyle.hasForegroundColor() ? captionStyle.foregroundColor : DEFAULT.foregroundColor, + captionStyle.hasBackgroundColor() ? captionStyle.backgroundColor : DEFAULT.backgroundColor, + captionStyle.hasWindowColor() ? captionStyle.windowColor : DEFAULT.windowColor, + captionStyle.hasEdgeType() ? captionStyle.edgeType : DEFAULT.edgeType, + captionStyle.hasEdgeColor() ? captionStyle.edgeColor : DEFAULT.edgeColor, + captionStyle.getTypeface()); + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/Cue.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/Cue.java new file mode 100755 index 000000000..a1a0e650f --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/Cue.java @@ -0,0 +1,140 @@ +/* + * 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.messenger.exoplayer.text; + +import android.text.Layout.Alignment; + +/** + * Contains information about a specific cue, including textual content and formatting data. + */ +public class Cue { + + /** + * An unset position or width. + */ + public static final float DIMEN_UNSET = Float.MIN_VALUE; + /** + * An unset anchor or line type value. + */ + public static final int TYPE_UNSET = Integer.MIN_VALUE; + /** + * Anchors the left (for horizontal positions) or top (for vertical positions) edge of the cue + * box. + */ + public static final int ANCHOR_TYPE_START = 0; + /** + * Anchors the middle of the cue box. + */ + public static final int ANCHOR_TYPE_MIDDLE = 1; + /** + * Anchors the right (for horizontal positions) or bottom (for vertical positions) edge of the cue + * box. + */ + public static final int ANCHOR_TYPE_END = 2; + /** + * Value for {@link #lineType} when {@link #line} is a fractional position. + */ + public static final int LINE_TYPE_FRACTION = 0; + /** + * Value for {@link #lineType} when {@link #line} is a line number. + */ + public static final int LINE_TYPE_NUMBER = 1; + + /** + * The cue text. Note the {@link CharSequence} may be decorated with styling spans. + */ + public final CharSequence text; + /** + * The alignment of the cue text within the cue box. + */ + public final Alignment textAlignment; + /** + * The position of the {@link #lineAnchor} of the cue box within the viewport in the direction + * orthogonal to the writing direction, or {@link #DIMEN_UNSET}. When set, the interpretation of + * the value depends on the value of {@link #lineType}. + *

+ * For horizontal text and {@link #lineType} equal to {@link #LINE_TYPE_FRACTION}, this is the + * fractional vertical position relative to the top of the viewport. + */ + public final float line; + /** + * The type of the {@link #line} value. + *

+ * {@link #LINE_TYPE_FRACTION} indicates that {@link #line} is a fractional position within the + * viewport. + *

+ * {@link #LINE_TYPE_NUMBER} indicates that {@link #line} is a line number, where the size of each + * line is taken to be the size of the first line of the cue. When {@link #line} is greater than + * or equal to 0, lines count from the start of the viewport (the first line is numbered 0). When + * {@link #line} is negative, lines count from the end of the viewport (the last line is numbered + * -1). For horizontal text the size of the first line of the cue is its height, and the start + * and end of the viewport are the top and bottom respectively. + */ + public final int lineType; + /** + * The cue box anchor positioned by {@link #line}. One of {@link #ANCHOR_TYPE_START}, + * {@link #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}. + *

+ * For the normal case of horizontal text, {@link #ANCHOR_TYPE_START}, {@link #ANCHOR_TYPE_MIDDLE} + * and {@link #ANCHOR_TYPE_END} correspond to the top, middle and bottom of the cue box + * respectively. + */ + public final int lineAnchor; + /** + * The fractional position of the {@link #positionAnchor} of the cue box within the viewport in + * the direction orthogonal to {@link #line}, or {@link #DIMEN_UNSET}. + *

+ * For horizontal text, this is the horizontal position relative to the left of the viewport. Note + * that positioning is relative to the left of the viewport even in the case of right-to-left + * text. + */ + public final float position; + /** + * The cue box anchor positioned by {@link #position}. One of {@link #ANCHOR_TYPE_START}, + * {@link #ANCHOR_TYPE_MIDDLE}, {@link #ANCHOR_TYPE_END} and {@link #TYPE_UNSET}. + *

+ * For the normal case of horizontal text, {@link #ANCHOR_TYPE_START}, {@link #ANCHOR_TYPE_MIDDLE} + * and {@link #ANCHOR_TYPE_END} correspond to the left, middle and right of the cue box + * respectively. + */ + public final int positionAnchor; + /** + * The size of the cue box in the writing direction specified as a fraction of the viewport size + * in that direction, or {@link #DIMEN_UNSET}. + */ + public final float size; + + public Cue() { + this(null); + } + + public Cue(CharSequence text) { + this(text, null, DIMEN_UNSET, TYPE_UNSET, TYPE_UNSET, DIMEN_UNSET, TYPE_UNSET, DIMEN_UNSET); + } + + public Cue(CharSequence text, Alignment textAlignment, float line, int lineType, + int lineAnchor, float position, int positionAnchor, float size) { + this.text = text; + this.textAlignment = textAlignment; + this.line = line; + this.lineType = lineType; + this.lineAnchor = lineAnchor; + this.position = position; + this.positionAnchor = positionAnchor; + this.size = size; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/PlayableSubtitle.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/PlayableSubtitle.java new file mode 100755 index 000000000..5c9d3f8a0 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/PlayableSubtitle.java @@ -0,0 +1,75 @@ +package org.telegram.messenger.exoplayer.text; + +/* + * 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. + */ +import java.util.List; + +/** + * A subtitle that wraps another subtitle, making it playable by adjusting it to be correctly + * aligned with the playback timebase. + */ +/* package */ final class PlayableSubtitle implements Subtitle { + + /** + * The start time of the subtitle. + *

+ * May be less than {@code getEventTime(0)}, since a subtitle may begin prior to the time of the + * first event. + */ + public final long startTimeUs; + + private final Subtitle subtitle; + private final long offsetUs; + + /** + * @param subtitle The subtitle to wrap. + * @param isRelative True if the wrapped subtitle's timestamps are relative to the start time. + * False if they are absolute. + * @param startTimeUs The start time of the subtitle. + * @param offsetUs An offset to add to the subtitle timestamps. + */ + public PlayableSubtitle(Subtitle subtitle, boolean isRelative, long startTimeUs, long offsetUs) { + this.subtitle = subtitle; + this.startTimeUs = startTimeUs; + this.offsetUs = (isRelative ? startTimeUs : 0) + offsetUs; + } + + @Override + public int getEventTimeCount() { + return subtitle.getEventTimeCount(); + } + + @Override + public long getEventTime(int index) { + return subtitle.getEventTime(index) + offsetUs; + } + + @Override + public long getLastEventTime() { + return subtitle.getLastEventTime() + offsetUs; + } + + @Override + public int getNextEventTimeIndex(long timeUs) { + return subtitle.getNextEventTimeIndex(timeUs - offsetUs); + } + + @Override + public List getCues(long timeUs) { + return subtitle.getCues(timeUs - offsetUs); + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/Subtitle.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/Subtitle.java new file mode 100755 index 000000000..e4a697215 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/Subtitle.java @@ -0,0 +1,64 @@ +/* + * 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.messenger.exoplayer.text; + +import java.util.List; + +/** + * A subtitle that contains textual data associated with time indices. + */ +public interface Subtitle { + + /** + * Gets the index of the first event that occurs after a given time (exclusive). + * + * @param timeUs The time in microseconds. + * @return The index of the next event, or -1 if there are no events after the specified time. + */ + public int getNextEventTimeIndex(long timeUs); + + /** + * Gets the number of event times, where events are defined as points in time at which the cues + * returned by {@link #getCues(long)} changes. + * + * @return The number of event times. + */ + public int getEventTimeCount(); + + /** + * Gets the event time at a specified index. + * + * @param index The index of the event time to obtain. + * @return The event time in microseconds. + */ + public long getEventTime(int index); + + /** + * Convenience method for obtaining the last event time. + * + * @return The time of the last event in microseconds, or -1 if {@code getEventTimeCount() == 0}. + */ + public long getLastEventTime(); + + /** + * Retrieve the subtitle cues that should be displayed at a given time. + * + * @param timeUs The time in microseconds. + * @return A list of cues that should be displayed, possibly empty. + */ + public List getCues(long timeUs); + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/SubtitleParser.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/SubtitleParser.java new file mode 100755 index 000000000..e7cec2ab8 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/SubtitleParser.java @@ -0,0 +1,44 @@ +/* + * 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.messenger.exoplayer.text; + +import org.telegram.messenger.exoplayer.ParserException; + +/** + * Parses {@link Subtitle}s from a byte array. + */ +public interface SubtitleParser { + + /** + * Checks whether the parser supports a given subtitle mime type. + * + * @param mimeType A subtitle mime type. + * @return Whether the mime type is supported. + */ + public boolean canParse(String mimeType); + + /** + * Parses a {@link Subtitle} from the provided {@code byte[]}. + * + * @param bytes The array holding the subtitle data. + * @param offset The offset of the subtitle data in bytes. + * @param length The length of the subtitle data in bytes. + * @return A parsed representation of the subtitle. + * @throws ParserException If a problem occurred parsing the subtitle data. + */ + public Subtitle parse(byte[] bytes, int offset, int length) throws ParserException; + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/SubtitleParserHelper.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/SubtitleParserHelper.java new file mode 100755 index 000000000..977107b15 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/SubtitleParserHelper.java @@ -0,0 +1,189 @@ +/* + * 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.messenger.exoplayer.text; + +import org.telegram.messenger.exoplayer.MediaFormat; +import org.telegram.messenger.exoplayer.ParserException; +import org.telegram.messenger.exoplayer.SampleHolder; +import org.telegram.messenger.exoplayer.util.Assertions; +import org.telegram.messenger.exoplayer.util.Util; + +import android.media.MediaCodec; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; + +import java.io.IOException; + +/** + * Wraps a {@link SubtitleParser}, exposing an interface similar to {@link MediaCodec} for + * asynchronous parsing of subtitles. + */ +/* package */ final class SubtitleParserHelper implements Handler.Callback { + + private static final int MSG_FORMAT = 0; + private static final int MSG_SAMPLE = 1; + + private final SubtitleParser parser; + private final Handler handler; + + private SampleHolder sampleHolder; + private boolean parsing; + private PlayableSubtitle result; + private IOException error; + private RuntimeException runtimeError; + + private boolean subtitlesAreRelative; + private long subtitleOffsetUs; + + /** + * @param looper The {@link Looper} associated with the thread on which parsing should occur. + * @param parser The parser that should be used to parse the raw data. + */ + public SubtitleParserHelper(Looper looper, SubtitleParser parser) { + this.handler = new Handler(looper, this); + this.parser = parser; + flush(); + } + + /** + * Flushes the helper, canceling the current parsing operation, if there is one. + */ + public synchronized void flush() { + sampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL); + parsing = false; + result = null; + error = null; + runtimeError = null; + } + + /** + * Whether the helper is currently performing a parsing operation. + * + * @return True if the helper is currently performing a parsing operation. False otherwise. + */ + public synchronized boolean isParsing() { + return parsing; + } + + /** + * Gets the holder that should be populated with data to be parsed. + *

+ * The returned holder will remain valid unless {@link #flush()} is called. If {@link #flush()} + * is called the holder is replaced, and this method should be called again to obtain the new + * holder. + * + * @return The holder that should be populated with data to be parsed. + */ + public synchronized SampleHolder getSampleHolder() { + return sampleHolder; + } + + /** + * Sets the format of subsequent samples. + * + * @param format The format. + */ + public void setFormat(MediaFormat format) { + handler.obtainMessage(MSG_FORMAT, format).sendToTarget(); + } + + /** + * Start a parsing operation. + *

+ * The holder returned by {@link #getSampleHolder()} should be populated with the data to be + * parsed prior to calling this method. + */ + public synchronized void startParseOperation() { + Assertions.checkState(!parsing); + parsing = true; + result = null; + error = null; + runtimeError = null; + handler.obtainMessage(MSG_SAMPLE, Util.getTopInt(sampleHolder.timeUs), + Util.getBottomInt(sampleHolder.timeUs), sampleHolder).sendToTarget(); + } + + /** + * Gets the result of the most recent parsing operation. + *

+ * The result is cleared as a result of calling this method, and so subsequent calls will return + * null until a subsequent parsing operation has finished. + * + * @return The result of the parsing operation, or null. + * @throws IOException If the parsing operation failed. + */ + public synchronized PlayableSubtitle getAndClearResult() throws IOException { + try { + if (error != null) { + throw error; + } else if (runtimeError != null) { + throw runtimeError; + } else { + return result; + } + } finally { + result = null; + error = null; + runtimeError = null; + } + } + + @Override + public boolean handleMessage(Message msg) { + switch (msg.what) { + case MSG_FORMAT: + handleFormat((MediaFormat) msg.obj); + break; + case MSG_SAMPLE: + long sampleTimeUs = Util.getLong(msg.arg1, msg.arg2); + SampleHolder holder = (SampleHolder) msg.obj; + handleSample(sampleTimeUs, holder); + break; + } + return true; + } + + private void handleFormat(MediaFormat format) { + subtitlesAreRelative = format.subsampleOffsetUs == MediaFormat.OFFSET_SAMPLE_RELATIVE; + subtitleOffsetUs = subtitlesAreRelative ? 0 : format.subsampleOffsetUs; + } + + private void handleSample(long sampleTimeUs, SampleHolder holder) { + Subtitle parsedSubtitle = null; + ParserException error = null; + RuntimeException runtimeError = null; + try { + parsedSubtitle = parser.parse(holder.data.array(), 0, holder.size); + } catch (ParserException e) { + error = e; + } catch (RuntimeException e) { + runtimeError = e; + } + synchronized (this) { + if (sampleHolder != holder) { + // A flush has occurred since this holder was posted. Do nothing. + } else { + this.result = new PlayableSubtitle(parsedSubtitle, subtitlesAreRelative, sampleTimeUs, + subtitleOffsetUs); + this.error = error; + this.runtimeError = runtimeError; + this.parsing = false; + } + } + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/TextRenderer.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/TextRenderer.java new file mode 100755 index 000000000..77a0682a5 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/TextRenderer.java @@ -0,0 +1,32 @@ +/* + * 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.messenger.exoplayer.text; + +import java.util.List; + +/** + * An interface for components that render text. + */ +public interface TextRenderer { + + /** + * Invoked each time there is a change in the {@link Cue}s to be rendered. + * + * @param cues The {@link Cue}s to be rendered, or an empty list if no cues are to be rendered. + */ + void onCues(List cues); + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/TextTrackRenderer.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/TextTrackRenderer.java new file mode 100755 index 000000000..ec4883f64 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/TextTrackRenderer.java @@ -0,0 +1,328 @@ +/* + * 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.messenger.exoplayer.text; + +import org.telegram.messenger.exoplayer.ExoPlaybackException; +import org.telegram.messenger.exoplayer.MediaFormat; +import org.telegram.messenger.exoplayer.MediaFormatHolder; +import org.telegram.messenger.exoplayer.SampleHolder; +import org.telegram.messenger.exoplayer.SampleSource; +import org.telegram.messenger.exoplayer.SampleSourceTrackRenderer; +import org.telegram.messenger.exoplayer.TrackRenderer; +import org.telegram.messenger.exoplayer.util.Assertions; + +import android.annotation.TargetApi; +import android.os.Handler; +import android.os.Handler.Callback; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * A {@link TrackRenderer} for subtitles. Text is parsed from sample data using a + * {@link SubtitleParser}. The actual rendering of each line of text is delegated to a + * {@link TextRenderer}. + *

+ * If no {@link SubtitleParser} instances are passed to the constructor, the subtitle type will be + * detected automatically for the following supported formats: + * + *

    + *
  • WebVTT ({@link org.telegram.messenger.exoplayer.text.webvtt.WebvttParser})
  • + *
  • TTML + * ({@link org.telegram.messenger.exoplayer.text.ttml.TtmlParser})
  • + *
  • SubRip + * ({@link org.telegram.messenger.exoplayer.text.subrip.SubripParser})
  • + *
  • TX3G + * ({@link org.telegram.messenger.exoplayer.text.tx3g.Tx3gParser})
  • + *
+ * + *

To override the default parsers, pass one or more {@link SubtitleParser} instances to the + * constructor. The first {@link SubtitleParser} that returns {@code true} from + * {@link SubtitleParser#canParse(String)} will be used. + */ +@TargetApi(16) +public final class TextTrackRenderer extends SampleSourceTrackRenderer implements Callback { + + private static final int MSG_UPDATE_OVERLAY = 0; + + /** + * Default parser classes in priority order. They are referred to indirectly so that it is + * possible to remove unused parsers. + */ + private static final List> DEFAULT_PARSER_CLASSES; + static { + DEFAULT_PARSER_CLASSES = new ArrayList<>(); + // Load parsers using reflection so that they can be deleted cleanly. + // Class.forName() appears for each parser so that automated tools like proguard + // can detect the use of reflection (see http://proguard.sourceforge.net/FAQ.html#forname). + try { + DEFAULT_PARSER_CLASSES.add( + Class.forName("org.telegram.messenger.exoplayer.text.webvtt.WebvttParser") + .asSubclass(SubtitleParser.class)); + } catch (ClassNotFoundException e) { + // Parser not found. + } + try { + DEFAULT_PARSER_CLASSES.add( + Class.forName("org.telegram.messenger.exoplayer.text.ttml.TtmlParser") + .asSubclass(SubtitleParser.class)); + } catch (ClassNotFoundException e) { + // Parser not found. + } + try { + DEFAULT_PARSER_CLASSES.add( + Class.forName("org.telegram.messenger.exoplayer.text.webvtt.Mp4WebvttParser") + .asSubclass(SubtitleParser.class)); + } catch (ClassNotFoundException e) { + // Parser not found. + } + try { + DEFAULT_PARSER_CLASSES.add( + Class.forName("org.telegram.messenger.exoplayer.text.subrip.SubripParser") + .asSubclass(SubtitleParser.class)); + } catch (ClassNotFoundException e) { + // Parser not found. + } + try { + DEFAULT_PARSER_CLASSES.add( + Class.forName("org.telegram.messenger.exoplayer.text.tx3g.Tx3gParser") + .asSubclass(SubtitleParser.class)); + } catch (ClassNotFoundException e) { + // Parser not found. + } + } + + private final Handler textRendererHandler; + private final TextRenderer textRenderer; + private final MediaFormatHolder formatHolder; + private final SubtitleParser[] subtitleParsers; + + private int parserIndex; + private boolean inputStreamEnded; + private PlayableSubtitle subtitle; + private PlayableSubtitle nextSubtitle; + private SubtitleParserHelper parserHelper; + private HandlerThread parserThread; + private int nextSubtitleEventIndex; + + /** + * @param source A source from which samples containing subtitle data can be read. + * @param textRenderer The text renderer. + * @param textRendererLooper The looper associated with the thread on which textRenderer should be + * invoked. If the renderer makes use of standard Android UI components, then this should + * normally be the looper associated with the applications' main thread, which can be + * obtained using {@link android.app.Activity#getMainLooper()}. Null may be passed if the + * renderer should be invoked directly on the player's internal rendering thread. + * @param subtitleParsers {@link SubtitleParser}s to parse text samples, in order of decreasing + * priority. If omitted, the default parsers will be used. + */ + public TextTrackRenderer(SampleSource source, TextRenderer textRenderer, + Looper textRendererLooper, SubtitleParser... subtitleParsers) { + this(new SampleSource[] {source}, textRenderer, textRendererLooper, subtitleParsers); + } + + /** + * @param sources Sources from which samples containing subtitle data can be read. + * @param textRenderer The text renderer. + * @param textRendererLooper The looper associated with the thread on which textRenderer should be + * invoked. If the renderer makes use of standard Android UI components, then this should + * normally be the looper associated with the applications' main thread, which can be + * obtained using {@link android.app.Activity#getMainLooper()}. Null may be passed if the + * renderer should be invoked directly on the player's internal rendering thread. + * @param subtitleParsers {@link SubtitleParser}s to parse text samples, in order of decreasing + * priority. If omitted, the default parsers will be used. + */ + public TextTrackRenderer(SampleSource[] sources, TextRenderer textRenderer, + Looper textRendererLooper, SubtitleParser... subtitleParsers) { + super(sources); + this.textRenderer = Assertions.checkNotNull(textRenderer); + this.textRendererHandler = textRendererLooper == null ? null + : new Handler(textRendererLooper, this); + if (subtitleParsers == null || subtitleParsers.length == 0) { + subtitleParsers = new SubtitleParser[DEFAULT_PARSER_CLASSES.size()]; + for (int i = 0; i < subtitleParsers.length; i++) { + try { + subtitleParsers[i] = DEFAULT_PARSER_CLASSES.get(i).newInstance(); + } catch (InstantiationException e) { + throw new IllegalStateException("Unexpected error creating default parser", e); + } catch (IllegalAccessException e) { + throw new IllegalStateException("Unexpected error creating default parser", e); + } + } + } + this.subtitleParsers = subtitleParsers; + formatHolder = new MediaFormatHolder(); + } + + @Override + protected boolean handlesTrack(MediaFormat mediaFormat) { + return getParserIndex(mediaFormat) != -1; + } + + @Override + protected void onEnabled(int track, long positionUs, boolean joining) + throws ExoPlaybackException { + super.onEnabled(track, positionUs, joining); + parserIndex = getParserIndex(getFormat(track)); + parserThread = new HandlerThread("textParser"); + parserThread.start(); + parserHelper = new SubtitleParserHelper(parserThread.getLooper(), subtitleParsers[parserIndex]); + } + + @Override + protected void onDiscontinuity(long positionUs) { + inputStreamEnded = false; + subtitle = null; + nextSubtitle = null; + clearTextRenderer(); + if (parserHelper != null) { + parserHelper.flush(); + } + } + + @Override + protected void doSomeWork(long positionUs, long elapsedRealtimeUs, boolean sourceIsReady) + throws ExoPlaybackException { + if (nextSubtitle == null) { + try { + nextSubtitle = parserHelper.getAndClearResult(); + } catch (IOException e) { + throw new ExoPlaybackException(e); + } + } + + if (getState() != TrackRenderer.STATE_STARTED) { + return; + } + + boolean textRendererNeedsUpdate = false; + long subtitleNextEventTimeUs = Long.MAX_VALUE; + if (subtitle != null) { + // We're iterating through the events in a subtitle. Set textRendererNeedsUpdate if we + // advance to the next event. + subtitleNextEventTimeUs = getNextEventTime(); + while (subtitleNextEventTimeUs <= positionUs) { + nextSubtitleEventIndex++; + subtitleNextEventTimeUs = getNextEventTime(); + textRendererNeedsUpdate = true; + } + } + + if (nextSubtitle != null && nextSubtitle.startTimeUs <= positionUs) { + // Advance to the next subtitle. Sync the next event index and trigger an update. + subtitle = nextSubtitle; + nextSubtitle = null; + nextSubtitleEventIndex = subtitle.getNextEventTimeIndex(positionUs); + textRendererNeedsUpdate = true; + } + + if (textRendererNeedsUpdate) { + // textRendererNeedsUpdate is set and we're playing. Update the renderer. + updateTextRenderer(subtitle.getCues(positionUs)); + } + + if (!inputStreamEnded && nextSubtitle == null && !parserHelper.isParsing()) { + // Try and read the next subtitle from the source. + SampleHolder sampleHolder = parserHelper.getSampleHolder(); + sampleHolder.clearData(); + int result = readSource(positionUs, formatHolder, sampleHolder); + if (result == SampleSource.FORMAT_READ) { + parserHelper.setFormat(formatHolder.format); + } else if (result == SampleSource.SAMPLE_READ) { + parserHelper.startParseOperation(); + } else if (result == SampleSource.END_OF_STREAM) { + inputStreamEnded = true; + } + } + } + + @Override + protected void onDisabled() throws ExoPlaybackException { + subtitle = null; + nextSubtitle = null; + parserThread.quit(); + parserThread = null; + parserHelper = null; + clearTextRenderer(); + super.onDisabled(); + } + + @Override + protected long getBufferedPositionUs() { + // Don't block playback whilst subtitles are loading. + return END_OF_TRACK_US; + } + + @Override + protected boolean isEnded() { + return inputStreamEnded && (subtitle == null || getNextEventTime() == Long.MAX_VALUE); + } + + @Override + protected boolean isReady() { + // Don't block playback whilst subtitles are loading. + // Note: To change this behavior, it will be necessary to consider [Internal: b/12949941]. + return true; + } + + private long getNextEventTime() { + return ((nextSubtitleEventIndex == -1) + || (nextSubtitleEventIndex >= subtitle.getEventTimeCount())) ? Long.MAX_VALUE + : (subtitle.getEventTime(nextSubtitleEventIndex)); + } + + private void updateTextRenderer(List cues) { + if (textRendererHandler != null) { + textRendererHandler.obtainMessage(MSG_UPDATE_OVERLAY, cues).sendToTarget(); + } else { + invokeRendererInternalCues(cues); + } + } + + private void clearTextRenderer() { + updateTextRenderer(Collections.emptyList()); + } + + @SuppressWarnings("unchecked") + @Override + public boolean handleMessage(Message msg) { + switch (msg.what) { + case MSG_UPDATE_OVERLAY: + invokeRendererInternalCues((List) msg.obj); + return true; + } + return false; + } + + private void invokeRendererInternalCues(List cues) { + textRenderer.onCues(cues); + } + + private int getParserIndex(MediaFormat mediaFormat) { + for (int i = 0; i < subtitleParsers.length; i++) { + if (subtitleParsers[i].canParse(mediaFormat.mimeType)) { + return i; + } + } + return -1; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/eia608/ClosedCaption.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/eia608/ClosedCaption.java new file mode 100755 index 000000000..4e12566b6 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/eia608/ClosedCaption.java @@ -0,0 +1,41 @@ +/* + * 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.messenger.exoplayer.text.eia608; + +/** + * A Closed Caption that contains textual data associated with time indices. + */ +/* package */ abstract class ClosedCaption { + + /** + * Identifies closed captions with control characters. + */ + public static final int TYPE_CTRL = 0; + /** + * Identifies closed captions with textual information. + */ + public static final int TYPE_TEXT = 1; + + /** + * The type of the closed caption data. + */ + public final int type; + + protected ClosedCaption(int type) { + this.type = type; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/eia608/ClosedCaptionCtrl.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/eia608/ClosedCaptionCtrl.java new file mode 100755 index 000000000..846a6f576 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/eia608/ClosedCaptionCtrl.java @@ -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.messenger.exoplayer.text.eia608; + +/* package */ final class ClosedCaptionCtrl extends ClosedCaption { + + /** + * The receipt of the {@link #RESUME_CAPTION_LOADING} command initiates pop-on style captioning. + * Subsequent data should be loaded into a non-displayed memory and held there until the + * {@link #END_OF_CAPTION} command is received, at which point the non-displayed memory becomes + * the displayed memory (and vice versa). + */ + public static final byte RESUME_CAPTION_LOADING = 0x20; + /** + * The receipt of the {@link #ROLL_UP_CAPTIONS_2_ROWS} command initiates roll-up style + * captioning, with the maximum of 2 rows displayed simultaneously. + */ + public static final byte ROLL_UP_CAPTIONS_2_ROWS = 0x25; + /** + * The receipt of the {@link #ROLL_UP_CAPTIONS_3_ROWS} command initiates roll-up style + * captioning, with the maximum of 3 rows displayed simultaneously. + */ + public static final byte ROLL_UP_CAPTIONS_3_ROWS = 0x26; + /** + * The receipt of the {@link #ROLL_UP_CAPTIONS_4_ROWS} command initiates roll-up style + * captioning, with the maximum of 4 rows displayed simultaneously. + */ + public static final byte ROLL_UP_CAPTIONS_4_ROWS = 0x27; + /** + * The receipt of the {@link #RESUME_DIRECT_CAPTIONING} command initiates paint-on style + * captioning. Subsequent data should be addressed immediately to displayed memory without need + * for the {@link #RESUME_CAPTION_LOADING} command. + */ + public static final byte RESUME_DIRECT_CAPTIONING = 0x29; + /** + * The receipt of the {@link #END_OF_CAPTION} command indicates the end of pop-on style caption, + * at this point already loaded in non-displayed memory caption should become the displayed + * memory (and vice versa). If no {@link #RESUME_CAPTION_LOADING} command has been received, + * {@link #END_OF_CAPTION} command forces the receiver into pop-on style. + */ + public static final byte END_OF_CAPTION = 0x2F; + + public static final byte ERASE_DISPLAYED_MEMORY = 0x2C; + public static final byte CARRIAGE_RETURN = 0x2D; + public static final byte ERASE_NON_DISPLAYED_MEMORY = 0x2E; + + public static final byte BACKSPACE = 0x21; + + + public static final byte MID_ROW_CHAN_1 = 0x11; + public static final byte MID_ROW_CHAN_2 = 0x19; + + public static final byte MISC_CHAN_1 = 0x14; + public static final byte MISC_CHAN_2 = 0x1C; + + public static final byte TAB_OFFSET_CHAN_1 = 0x17; + public static final byte TAB_OFFSET_CHAN_2 = 0x1F; + + public final byte cc1; + public final byte cc2; + + protected ClosedCaptionCtrl(byte cc1, byte cc2) { + super(ClosedCaption.TYPE_CTRL); + this.cc1 = cc1; + this.cc2 = cc2; + } + + public boolean isMidRowCode() { + return (cc1 == MID_ROW_CHAN_1 || cc1 == MID_ROW_CHAN_2) && (cc2 >= 0x20 && cc2 <= 0x2F); + } + + public boolean isMiscCode() { + return (cc1 == MISC_CHAN_1 || cc1 == MISC_CHAN_2) && (cc2 >= 0x20 && cc2 <= 0x2F); + } + + public boolean isTabOffsetCode() { + return (cc1 == TAB_OFFSET_CHAN_1 || cc1 == TAB_OFFSET_CHAN_2) && (cc2 >= 0x21 && cc2 <= 0x23); + } + + public boolean isPreambleAddressCode() { + return (cc1 >= 0x10 && cc1 <= 0x1F) && (cc2 >= 0x40 && cc2 <= 0x7F); + } + + public boolean isRepeatable() { + return cc1 >= 0x10 && cc1 <= 0x1F; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/eia608/ClosedCaptionList.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/eia608/ClosedCaptionList.java new file mode 100755 index 000000000..022d75116 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/eia608/ClosedCaptionList.java @@ -0,0 +1,39 @@ +/* + * 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.messenger.exoplayer.text.eia608; + +/* package */ final class ClosedCaptionList implements Comparable { + + public final long timeUs; + public final boolean decodeOnly; + public final ClosedCaption[] captions; + + public ClosedCaptionList(long timeUs, boolean decodeOnly, ClosedCaption[] captions) { + this.timeUs = timeUs; + this.decodeOnly = decodeOnly; + this.captions = captions; + } + + @Override + public int compareTo(ClosedCaptionList other) { + long delta = timeUs - other.timeUs; + if (delta == 0) { + return 0; + } + return delta > 0 ? 1 : -1; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/eia608/ClosedCaptionText.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/eia608/ClosedCaptionText.java new file mode 100755 index 000000000..a23f85a50 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/eia608/ClosedCaptionText.java @@ -0,0 +1,27 @@ +/* + * 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.messenger.exoplayer.text.eia608; + +/* package */ final class ClosedCaptionText extends ClosedCaption { + + public final String text; + + public ClosedCaptionText(String text) { + super(ClosedCaption.TYPE_TEXT); + this.text = text; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/eia608/Eia608Parser.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/eia608/Eia608Parser.java new file mode 100755 index 000000000..dd5dc17ba --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/eia608/Eia608Parser.java @@ -0,0 +1,267 @@ +/* + * 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.messenger.exoplayer.text.eia608; + +import org.telegram.messenger.exoplayer.SampleHolder; +import org.telegram.messenger.exoplayer.util.MimeTypes; +import org.telegram.messenger.exoplayer.util.ParsableBitArray; +import org.telegram.messenger.exoplayer.util.ParsableByteArray; + +import java.util.ArrayList; + +/** + * Facilitates the extraction and parsing of EIA-608 (a.k.a. "line 21 captions" and "CEA-608") + * Closed Captions from the SEI data block from H.264. + */ +public final class Eia608Parser { + + private static final int PAYLOAD_TYPE_CC = 4; + private static final int COUNTRY_CODE = 0xB5; + private static final int PROVIDER_CODE = 0x31; + private static final int USER_ID = 0x47413934; // "GA94" + private static final int USER_DATA_TYPE_CODE = 0x3; + + // Basic North American 608 CC char set, mostly ASCII. Indexed by (char-0x20). + private static final int[] BASIC_CHARACTER_SET = new int[] { + 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, // ! " # $ % & ' + 0x28, 0x29, // ( ) + 0xE1, // 2A: 225 'á' "Latin small letter A with acute" + 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, // + , - . / + 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, // 0 1 2 3 4 5 6 7 + 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, // 8 9 : ; < = > ? + 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, // @ A B C D E F G + 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, // H I J K L M N O + 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, // P Q R S T U V W + 0x58, 0x59, 0x5A, 0x5B, // X Y Z [ + 0xE9, // 5C: 233 'é' "Latin small letter E with acute" + 0x5D, // ] + 0xED, // 5E: 237 'í' "Latin small letter I with acute" + 0xF3, // 5F: 243 'ó' "Latin small letter O with acute" + 0xFA, // 60: 250 'ú' "Latin small letter U with acute" + 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, // a b c d e f g + 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, // h i j k l m n o + 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, // p q r s t u v w + 0x78, 0x79, 0x7A, // x y z + 0xE7, // 7B: 231 'ç' "Latin small letter C with cedilla" + 0xF7, // 7C: 247 '÷' "Division sign" + 0xD1, // 7D: 209 'Ñ' "Latin capital letter N with tilde" + 0xF1, // 7E: 241 'ñ' "Latin small letter N with tilde" + 0x25A0 // 7F: "Black Square" (NB: 2588 = Full Block) + }; + + // Special North American 608 CC char set. + private static final int[] SPECIAL_CHARACTER_SET = new int[] { + 0xAE, // 30: 174 '®' "Registered Sign" - registered trademark symbol + 0xB0, // 31: 176 '°' "Degree Sign" + 0xBD, // 32: 189 '½' "Vulgar Fraction One Half" (1/2 symbol) + 0xBF, // 33: 191 '¿' "Inverted Question Mark" + 0x2122, // 34: "Trade Mark Sign" (tm superscript) + 0xA2, // 35: 162 '¢' "Cent Sign" + 0xA3, // 36: 163 '£' "Pound Sign" - pounds sterling + 0x266A, // 37: "Eighth Note" - music note + 0xE0, // 38: 224 'à' "Latin small letter A with grave" + 0x20, // 39: TRANSPARENT SPACE - for now use ordinary space + 0xE8, // 3A: 232 'è' "Latin small letter E with grave" + 0xE2, // 3B: 226 'â' "Latin small letter A with circumflex" + 0xEA, // 3C: 234 'ê' "Latin small letter E with circumflex" + 0xEE, // 3D: 238 'î' "Latin small letter I with circumflex" + 0xF4, // 3E: 244 'ô' "Latin small letter O with circumflex" + 0xFB // 3F: 251 'û' "Latin small letter U with circumflex" + }; + + // Extended Spanish/Miscellaneous and French char set. + private static final int[] SPECIAL_ES_FR_CHARACTER_SET = new int[] { + // Spanish and misc. + 0xC1, 0xC9, 0xD3, 0xDA, 0xDC, 0xFC, 0x2018, 0xA1, + 0x2A, 0x27, 0x2014, 0xA9, 0x2120, 0x2022, 0x201C, 0x201D, + // French. + 0xC0, 0xC2, 0xC7, 0xC8, 0xCA, 0xCB, 0xEB, 0xCE, + 0xCF, 0xEF, 0xD4, 0xD9, 0xF9, 0xDB, 0xAB, 0xBB + }; + + //Extended Portuguese and German/Danish char set. + private static final int[] SPECIAL_PT_DE_CHARACTER_SET = new int[] { + // Portuguese. + 0xC3, 0xE3, 0xCD, 0xCC, 0xEC, 0xD2, 0xF2, 0xD5, + 0xF5, 0x7B, 0x7D, 0x5C, 0x5E, 0x5F, 0x7C, 0x7E, + // German/Danish. + 0xC4, 0xE4, 0xD6, 0xF6, 0xDF, 0xA5, 0xA4, 0x2502, + 0xC5, 0xE5, 0xD8, 0xF8, 0x250C, 0x2510, 0x2514, 0x2518 + }; + + private final ParsableBitArray seiBuffer; + private final StringBuilder stringBuilder; + private final ArrayList captions; + + /* package */ Eia608Parser() { + seiBuffer = new ParsableBitArray(); + stringBuilder = new StringBuilder(); + captions = new ArrayList<>(); + } + + /* package */ boolean canParse(String mimeType) { + return mimeType.equals(MimeTypes.APPLICATION_EIA608); + } + + /* package */ ClosedCaptionList parse(SampleHolder sampleHolder) { + if (sampleHolder.size < 10) { + return null; + } + + captions.clear(); + stringBuilder.setLength(0); + seiBuffer.reset(sampleHolder.data.array()); + + // country_code (8) + provider_code (16) + user_identifier (32) + user_data_type_code (8) + + // reserved (1) + process_cc_data_flag (1) + zero_bit (1) + seiBuffer.skipBits(67); + int ccCount = seiBuffer.readBits(5); + seiBuffer.skipBits(8); + + for (int i = 0; i < ccCount; i++) { + seiBuffer.skipBits(5); // one_bit + reserved + boolean ccValid = seiBuffer.readBit(); + if (!ccValid) { + seiBuffer.skipBits(18); + continue; + } + int ccType = seiBuffer.readBits(2); + if (ccType != 0) { + seiBuffer.skipBits(16); + continue; + } + seiBuffer.skipBits(1); + byte ccData1 = (byte) seiBuffer.readBits(7); + seiBuffer.skipBits(1); + byte ccData2 = (byte) seiBuffer.readBits(7); + + // Ignore empty captions. + if (ccData1 == 0 && ccData2 == 0) { + continue; + } + + // Special North American character set. + // ccData2 - P|0|1|1|X|X|X|X + if ((ccData1 == 0x11 || ccData1 == 0x19) + && ((ccData2 & 0x70) == 0x30)) { + stringBuilder.append(getSpecialChar(ccData2)); + continue; + } + + // Extended Spanish/Miscellaneous and French character set. + // ccData2 - P|0|1|X|X|X|X|X + if ((ccData1 == 0x12 || ccData1 == 0x1A) + && ((ccData2 & 0x60) == 0x20)) { + backspace(); // Remove standard equivalent of the special extended char. + stringBuilder.append(getExtendedEsFrChar(ccData2)); + continue; + } + + // Extended Portuguese and German/Danish character set. + // ccData2 - P|0|1|X|X|X|X|X + if ((ccData1 == 0x13 || ccData1 == 0x1B) + && ((ccData2 & 0x60) == 0x20)) { + backspace(); // Remove standard equivalent of the special extended char. + stringBuilder.append(getExtendedPtDeChar(ccData2)); + continue; + } + + // Control character. + if (ccData1 < 0x20) { + addCtrl(ccData1, ccData2); + continue; + } + + // Basic North American character set. + stringBuilder.append(getChar(ccData1)); + if (ccData2 >= 0x20) { + stringBuilder.append(getChar(ccData2)); + } + } + + addBufferedText(); + + if (captions.isEmpty()) { + return null; + } + + ClosedCaption[] captionArray = new ClosedCaption[captions.size()]; + captions.toArray(captionArray); + return new ClosedCaptionList(sampleHolder.timeUs, sampleHolder.isDecodeOnly(), captionArray); + } + + private static char getChar(byte ccData) { + int index = (ccData & 0x7F) - 0x20; + return (char) BASIC_CHARACTER_SET[index]; + } + + private static char getSpecialChar(byte ccData) { + int index = ccData & 0xF; + return (char) SPECIAL_CHARACTER_SET[index]; + } + + private static char getExtendedEsFrChar(byte ccData) { + int index = ccData & 0x1F; + return (char) SPECIAL_ES_FR_CHARACTER_SET[index]; + } + + private static char getExtendedPtDeChar(byte ccData) { + int index = ccData & 0x1F; + return (char) SPECIAL_PT_DE_CHARACTER_SET[index]; + } + + private void addBufferedText() { + if (stringBuilder.length() > 0) { + captions.add(new ClosedCaptionText(stringBuilder.toString())); + stringBuilder.setLength(0); + } + } + + private void addCtrl(byte ccData1, byte ccData2) { + addBufferedText(); + captions.add(new ClosedCaptionCtrl(ccData1, ccData2)); + } + + private void backspace() { + addCtrl((byte) 0x14, ClosedCaptionCtrl.BACKSPACE); + } + + /** + * Inspects an sei message to determine whether it contains EIA-608. + *

+ * The position of {@code payload} is left unchanged. + * + * @param payloadType The payload type of the message. + * @param payloadLength The length of the payload. + * @param payload A {@link ParsableByteArray} containing the payload. + * @return True if the sei message contains EIA-608. False otherwise. + */ + public static boolean isSeiMessageEia608(int payloadType, int payloadLength, + ParsableByteArray payload) { + if (payloadType != PAYLOAD_TYPE_CC || payloadLength < 8) { + return false; + } + int startPosition = payload.getPosition(); + int countryCode = payload.readUnsignedByte(); + int providerCode = payload.readUnsignedShort(); + int userIdentifier = payload.readInt(); + int userDataTypeCode = payload.readUnsignedByte(); + payload.setPosition(startPosition); + return countryCode == COUNTRY_CODE && providerCode == PROVIDER_CODE + && userIdentifier == USER_ID && userDataTypeCode == USER_DATA_TYPE_CODE; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/eia608/Eia608TrackRenderer.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/eia608/Eia608TrackRenderer.java new file mode 100755 index 000000000..b965d6b2a --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/eia608/Eia608TrackRenderer.java @@ -0,0 +1,363 @@ +/* + * 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.messenger.exoplayer.text.eia608; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.ExoPlaybackException; +import org.telegram.messenger.exoplayer.MediaFormat; +import org.telegram.messenger.exoplayer.MediaFormatHolder; +import org.telegram.messenger.exoplayer.SampleHolder; +import org.telegram.messenger.exoplayer.SampleSource; +import org.telegram.messenger.exoplayer.SampleSourceTrackRenderer; +import org.telegram.messenger.exoplayer.TrackRenderer; +import org.telegram.messenger.exoplayer.text.Cue; +import org.telegram.messenger.exoplayer.text.TextRenderer; +import org.telegram.messenger.exoplayer.util.Assertions; +import org.telegram.messenger.exoplayer.util.Util; + +import android.os.Handler; +import android.os.Handler.Callback; +import android.os.Looper; +import android.os.Message; + +import java.util.Collections; +import java.util.TreeSet; + +/** + * A {@link TrackRenderer} for EIA-608 closed captions in a media stream. + */ +public final class Eia608TrackRenderer extends SampleSourceTrackRenderer implements Callback { + + private static final int MSG_INVOKE_RENDERER = 0; + + private static final int CC_MODE_UNKNOWN = 0; + private static final int CC_MODE_ROLL_UP = 1; + private static final int CC_MODE_POP_ON = 2; + private static final int CC_MODE_PAINT_ON = 3; + + // The default number of rows to display in roll-up captions mode. + private static final int DEFAULT_CAPTIONS_ROW_COUNT = 4; + // The maximum duration that captions are parsed ahead of the current position. + private static final int MAX_SAMPLE_READAHEAD_US = 5000000; + + private final Eia608Parser eia608Parser; + private final TextRenderer textRenderer; + private final Handler textRendererHandler; + private final MediaFormatHolder formatHolder; + private final SampleHolder sampleHolder; + private final StringBuilder captionStringBuilder; + private final TreeSet pendingCaptionLists; + + private boolean inputStreamEnded; + private int captionMode; + private int captionRowCount; + private String caption; + private String lastRenderedCaption; + private ClosedCaptionCtrl repeatableControl; + + /** + * @param source A source from which samples containing EIA-608 closed captions can be read. + * @param textRenderer The text renderer. + * @param textRendererLooper The looper associated with the thread on which textRenderer should be + * invoked. If the renderer makes use of standard Android UI components, then this should + * normally be the looper associated with the applications' main thread, which can be + * obtained using {@link android.app.Activity#getMainLooper()}. Null may be passed if the + * renderer should be invoked directly on the player's internal rendering thread. + */ + public Eia608TrackRenderer(SampleSource source, TextRenderer textRenderer, + Looper textRendererLooper) { + super(source); + this.textRenderer = Assertions.checkNotNull(textRenderer); + textRendererHandler = textRendererLooper == null ? null : new Handler(textRendererLooper, this); + eia608Parser = new Eia608Parser(); + formatHolder = new MediaFormatHolder(); + sampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL); + captionStringBuilder = new StringBuilder(); + pendingCaptionLists = new TreeSet<>(); + } + + @Override + protected boolean handlesTrack(MediaFormat mediaFormat) { + return eia608Parser.canParse(mediaFormat.mimeType); + } + + @Override + protected void onEnabled(int track, long positionUs, boolean joining) + throws ExoPlaybackException { + super.onEnabled(track, positionUs, joining); + } + + @Override + protected void onDiscontinuity(long positionUs) { + inputStreamEnded = false; + repeatableControl = null; + pendingCaptionLists.clear(); + clearPendingSample(); + captionRowCount = DEFAULT_CAPTIONS_ROW_COUNT; + setCaptionMode(CC_MODE_UNKNOWN); + invokeRenderer(null); + } + + @Override + protected void doSomeWork(long positionUs, long elapsedRealtimeUs, boolean sourceIsReady) + throws ExoPlaybackException { + if (isSamplePending()) { + maybeParsePendingSample(positionUs); + } + + int result = inputStreamEnded ? SampleSource.END_OF_STREAM : SampleSource.SAMPLE_READ; + while (!isSamplePending() && result == SampleSource.SAMPLE_READ) { + result = readSource(positionUs, formatHolder, sampleHolder); + if (result == SampleSource.SAMPLE_READ) { + maybeParsePendingSample(positionUs); + } else if (result == SampleSource.END_OF_STREAM) { + inputStreamEnded = true; + } + } + + while (!pendingCaptionLists.isEmpty()) { + if (pendingCaptionLists.first().timeUs > positionUs) { + // We're too early to render any of the pending caption lists. + return; + } + // Remove and consume the next caption list. + ClosedCaptionList nextCaptionList = pendingCaptionLists.pollFirst(); + consumeCaptionList(nextCaptionList); + // Update the renderer, unless the caption list was marked for decoding only. + if (!nextCaptionList.decodeOnly) { + invokeRenderer(caption); + } + } + } + + @Override + protected long getBufferedPositionUs() { + return TrackRenderer.END_OF_TRACK_US; + } + + @Override + protected boolean isEnded() { + return inputStreamEnded; + } + + @Override + protected boolean isReady() { + return true; + } + + private void invokeRenderer(String text) { + if (Util.areEqual(lastRenderedCaption, text)) { + // No change. + return; + } + this.lastRenderedCaption = text; + if (textRendererHandler != null) { + textRendererHandler.obtainMessage(MSG_INVOKE_RENDERER, text).sendToTarget(); + } else { + invokeRendererInternal(text); + } + } + + @SuppressWarnings("unchecked") + @Override + public boolean handleMessage(Message msg) { + switch (msg.what) { + case MSG_INVOKE_RENDERER: + invokeRendererInternal((String) msg.obj); + return true; + } + return false; + } + + private void invokeRendererInternal(String cueText) { + if (cueText == null) { + textRenderer.onCues(Collections.emptyList()); + } else { + textRenderer.onCues(Collections.singletonList(new Cue(cueText))); + } + } + + private void maybeParsePendingSample(long positionUs) { + if (sampleHolder.timeUs > positionUs + MAX_SAMPLE_READAHEAD_US) { + // We're too early to parse the sample. + return; + } + ClosedCaptionList holder = eia608Parser.parse(sampleHolder); + clearPendingSample(); + if (holder != null) { + pendingCaptionLists.add(holder); + } + } + + private void consumeCaptionList(ClosedCaptionList captionList) { + int captionBufferSize = captionList.captions.length; + if (captionBufferSize == 0) { + return; + } + + boolean isRepeatableControl = false; + for (int i = 0; i < captionBufferSize; i++) { + ClosedCaption caption = captionList.captions[i]; + if (caption.type == ClosedCaption.TYPE_CTRL) { + ClosedCaptionCtrl captionCtrl = (ClosedCaptionCtrl) caption; + isRepeatableControl = captionBufferSize == 1 && captionCtrl.isRepeatable(); + if (isRepeatableControl && repeatableControl != null + && repeatableControl.cc1 == captionCtrl.cc1 + && repeatableControl.cc2 == captionCtrl.cc2) { + repeatableControl = null; + continue; + } else if (isRepeatableControl) { + repeatableControl = captionCtrl; + } + if (captionCtrl.isMiscCode()) { + handleMiscCode(captionCtrl); + } else if (captionCtrl.isPreambleAddressCode()) { + handlePreambleAddressCode(); + } + } else { + handleText((ClosedCaptionText) caption); + } + } + + if (!isRepeatableControl) { + repeatableControl = null; + } + if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_PAINT_ON) { + caption = getDisplayCaption(); + } + } + + private void handleText(ClosedCaptionText captionText) { + if (captionMode != CC_MODE_UNKNOWN) { + captionStringBuilder.append(captionText.text); + } + } + + private void handleMiscCode(ClosedCaptionCtrl captionCtrl) { + switch (captionCtrl.cc2) { + case ClosedCaptionCtrl.ROLL_UP_CAPTIONS_2_ROWS: + captionRowCount = 2; + setCaptionMode(CC_MODE_ROLL_UP); + return; + case ClosedCaptionCtrl.ROLL_UP_CAPTIONS_3_ROWS: + captionRowCount = 3; + setCaptionMode(CC_MODE_ROLL_UP); + return; + case ClosedCaptionCtrl.ROLL_UP_CAPTIONS_4_ROWS: + captionRowCount = 4; + setCaptionMode(CC_MODE_ROLL_UP); + return; + case ClosedCaptionCtrl.RESUME_CAPTION_LOADING: + setCaptionMode(CC_MODE_POP_ON); + return; + case ClosedCaptionCtrl.RESUME_DIRECT_CAPTIONING: + setCaptionMode(CC_MODE_PAINT_ON); + return; + } + + if (captionMode == CC_MODE_UNKNOWN) { + return; + } + + switch (captionCtrl.cc2) { + case ClosedCaptionCtrl.ERASE_DISPLAYED_MEMORY: + caption = null; + if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_PAINT_ON) { + captionStringBuilder.setLength(0); + } + return; + case ClosedCaptionCtrl.ERASE_NON_DISPLAYED_MEMORY: + captionStringBuilder.setLength(0); + return; + case ClosedCaptionCtrl.END_OF_CAPTION: + caption = getDisplayCaption(); + captionStringBuilder.setLength(0); + return; + case ClosedCaptionCtrl.CARRIAGE_RETURN: + maybeAppendNewline(); + return; + case ClosedCaptionCtrl.BACKSPACE: + if (captionStringBuilder.length() > 0) { + captionStringBuilder.setLength(captionStringBuilder.length() - 1); + } + return; + } + } + + private void handlePreambleAddressCode() { + // TODO: Add better handling of this with specific positioning. + maybeAppendNewline(); + } + + private void setCaptionMode(int captionMode) { + if (this.captionMode == captionMode) { + return; + } + + this.captionMode = captionMode; + // Clear the working memory. + captionStringBuilder.setLength(0); + if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_UNKNOWN) { + // When switching to roll-up or unknown, we also need to clear the caption. + caption = null; + } + } + + private void maybeAppendNewline() { + int buildLength = captionStringBuilder.length(); + if (buildLength > 0 && captionStringBuilder.charAt(buildLength - 1) != '\n') { + captionStringBuilder.append('\n'); + } + } + + private String getDisplayCaption() { + int buildLength = captionStringBuilder.length(); + if (buildLength == 0) { + return null; + } + + boolean endsWithNewline = captionStringBuilder.charAt(buildLength - 1) == '\n'; + if (buildLength == 1 && endsWithNewline) { + return null; + } + + int endIndex = endsWithNewline ? buildLength - 1 : buildLength; + if (captionMode != CC_MODE_ROLL_UP) { + return captionStringBuilder.substring(0, endIndex); + } + + int startIndex = 0; + int searchBackwardFromIndex = endIndex; + for (int i = 0; i < captionRowCount && searchBackwardFromIndex != -1; i++) { + searchBackwardFromIndex = captionStringBuilder.lastIndexOf("\n", searchBackwardFromIndex - 1); + } + if (searchBackwardFromIndex != -1) { + startIndex = searchBackwardFromIndex + 1; + } + captionStringBuilder.delete(0, startIndex); + return captionStringBuilder.substring(0, endIndex - startIndex); + } + + private void clearPendingSample() { + sampleHolder.timeUs = C.UNKNOWN_TIME_US; + sampleHolder.clearData(); + } + + private boolean isSamplePending() { + return sampleHolder.timeUs != C.UNKNOWN_TIME_US; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/subrip/SubripParser.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/subrip/SubripParser.java new file mode 100755 index 000000000..93ef07c2a --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/subrip/SubripParser.java @@ -0,0 +1,128 @@ +/* + * 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.messenger.exoplayer.text.subrip; + +import org.telegram.messenger.exoplayer.text.Cue; +import org.telegram.messenger.exoplayer.text.SubtitleParser; +import org.telegram.messenger.exoplayer.util.LongArray; +import org.telegram.messenger.exoplayer.util.MimeTypes; +import org.telegram.messenger.exoplayer.util.ParsableByteArray; + +import android.text.Html; +import android.text.Spanned; +import android.text.TextUtils; +import android.util.Log; + +import java.util.ArrayList; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A simple SubRip parser. + */ +public final class SubripParser implements SubtitleParser { + + private static final String TAG = "SubripParser"; + + private static final Pattern SUBRIP_TIMING_LINE = Pattern.compile("(\\S*)\\s*-->\\s*(\\S*)"); + private static final Pattern SUBRIP_TIMESTAMP = + Pattern.compile("(?:(\\d+):)?(\\d+):(\\d+),(\\d+)"); + + private final StringBuilder textBuilder; + + public SubripParser() { + textBuilder = new StringBuilder(); + } + + @Override + public boolean canParse(String mimeType) { + return MimeTypes.APPLICATION_SUBRIP.equals(mimeType); + } + + @Override + public SubripSubtitle parse(byte[] bytes, int offset, int length) { + ArrayList cues = new ArrayList<>(); + LongArray cueTimesUs = new LongArray(); + ParsableByteArray subripData = new ParsableByteArray(bytes, offset + length); + subripData.setPosition(offset); + boolean haveEndTimecode; + String currentLine; + + while ((currentLine = subripData.readLine()) != null) { + if (currentLine.length() == 0) { + // Skip blank lines. + continue; + } + + // Parse the index line as a sanity check. + try { + Integer.parseInt(currentLine); + } catch (NumberFormatException e) { + Log.w(TAG, "Skipping invalid index: " + currentLine); + continue; + } + + // Read and parse the timing line. + haveEndTimecode = false; + currentLine = subripData.readLine(); + Matcher matcher = SUBRIP_TIMING_LINE.matcher(currentLine); + if (matcher.find()) { + cueTimesUs.add(parseTimecode(matcher.group(1))); + String endTimecode = matcher.group(2); + if (!TextUtils.isEmpty(endTimecode)) { + haveEndTimecode = true; + cueTimesUs.add(parseTimecode(matcher.group(2))); + } + } else { + Log.w(TAG, "Skipping invalid timing: " + currentLine); + continue; + } + + // Read and parse the text. + textBuilder.setLength(0); + while (!TextUtils.isEmpty(currentLine = subripData.readLine())) { + if (textBuilder.length() > 0) { + textBuilder.append("
"); + } + textBuilder.append(currentLine.trim()); + } + + Spanned text = Html.fromHtml(textBuilder.toString()); + cues.add(new Cue(text)); + if (haveEndTimecode) { + cues.add(null); + } + } + + Cue[] cuesArray = new Cue[cues.size()]; + cues.toArray(cuesArray); + long[] cueTimesUsArray = cueTimesUs.toArray(); + return new SubripSubtitle(cuesArray, cueTimesUsArray); + } + + private static long parseTimecode(String s) throws NumberFormatException { + Matcher matcher = SUBRIP_TIMESTAMP.matcher(s); + if (!matcher.matches()) { + throw new NumberFormatException("has invalid format"); + } + long timestampMs = Long.parseLong(matcher.group(1)) * 60 * 60 * 1000; + timestampMs += Long.parseLong(matcher.group(2)) * 60 * 1000; + timestampMs += Long.parseLong(matcher.group(3)) * 1000; + timestampMs += Long.parseLong(matcher.group(4)); + return timestampMs * 1000; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/subrip/SubripSubtitle.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/subrip/SubripSubtitle.java new file mode 100755 index 000000000..d49a0482d --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/subrip/SubripSubtitle.java @@ -0,0 +1,80 @@ +/* + * 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.messenger.exoplayer.text.subrip; + +import org.telegram.messenger.exoplayer.text.Cue; +import org.telegram.messenger.exoplayer.text.Subtitle; +import org.telegram.messenger.exoplayer.util.Assertions; +import org.telegram.messenger.exoplayer.util.Util; + +import java.util.Collections; +import java.util.List; + +/** + * A representation of a SubRip subtitle. + */ +/* package */ final class SubripSubtitle implements Subtitle { + + private final Cue[] cues; + private final long[] cueTimesUs; + + /** + * @param cues The cues in the subtitle. Null entries may be used to represent empty cues. + * @param cueTimesUs The cue times, in microseconds. + */ + public SubripSubtitle(Cue[] cues, long[] cueTimesUs) { + this.cues = cues; + this.cueTimesUs = cueTimesUs; + } + + @Override + public int getNextEventTimeIndex(long timeUs) { + int index = Util.binarySearchCeil(cueTimesUs, timeUs, false, false); + return index < cueTimesUs.length ? index : -1; + } + + @Override + public int getEventTimeCount() { + return cueTimesUs.length; + } + + @Override + public long getEventTime(int index) { + Assertions.checkArgument(index >= 0); + Assertions.checkArgument(index < cueTimesUs.length); + return cueTimesUs[index]; + } + + @Override + public long getLastEventTime() { + if (getEventTimeCount() == 0) { + return -1; + } + return cueTimesUs[cueTimesUs.length - 1]; + } + + @Override + public List getCues(long timeUs) { + int index = Util.binarySearchFloor(cueTimesUs, timeUs, true, false); + if (index == -1 || cues[index] == null) { + // timeUs is earlier than the start of the first cue, or we have an empty cue. + return Collections.emptyList(); + } else { + return Collections.singletonList(cues[index]); + } + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/ttml/TtmlColorParser.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/ttml/TtmlColorParser.java new file mode 100755 index 000000000..351779a4c --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/ttml/TtmlColorParser.java @@ -0,0 +1,140 @@ +/* + * 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.messenger.exoplayer.text.ttml; + +import org.telegram.messenger.exoplayer.util.Assertions; +import org.telegram.messenger.exoplayer.util.Util; + +import android.text.TextUtils; + +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Parser to parse ttml color value expression + * (http://www.w3.org/TR/ttml1/#style-value-color) + */ +/*package*/ final class TtmlColorParser { + + private static final String RGB = "rgb"; + private static final String RGBA = "rgba"; + + private static final Pattern RGB_PATTERN = Pattern.compile( + "^rgb\\((\\d{1,3}),(\\d{1,3}),(\\d{1,3})\\)$"); + + private static final Pattern RGBA_PATTERN = Pattern.compile( + "^rgba\\((\\d{1,3}),(\\d{1,3}),(\\d{1,3}),(\\d{1,3})\\)$"); + + static final int TRANSPARENT = 0x00000000; + static final int BLACK = 0xFF000000; + static final int SILVER = 0xFFC0C0C0; + static final int GRAY = 0xFF808080; + static final int WHITE = 0xFFFFFFFF; + static final int MAROON = 0xFF800000; + static final int RED = 0xFFFF0000; + static final int PURPLE = 0xFF800080; + static final int FUCHSIA = 0xFFFF00FF; + static final int MAGENTA = FUCHSIA; + static final int GREEN = 0xFF008000; + static final int LIME = 0xFF00FF00; + static final int OLIVE = 0xFF808000; + static final int YELLOW = 0xFFFFFF00; + static final int NAVY = 0xFF000080; + static final int BLUE = 0xFF0000FF; + static final int TEAL = 0xFF008080; + static final int AQUA = 0x00FFFFFF; + static final int CYAN = 0xFF00FFFF; + + private static final Map COLOR_NAME_MAP; + static { + COLOR_NAME_MAP = new HashMap<>(); + COLOR_NAME_MAP.put("transparent", TRANSPARENT); + COLOR_NAME_MAP.put("black", BLACK); + COLOR_NAME_MAP.put("silver", SILVER); + COLOR_NAME_MAP.put("gray", GRAY); + COLOR_NAME_MAP.put("white", WHITE); + COLOR_NAME_MAP.put("maroon", MAROON); + COLOR_NAME_MAP.put("red", RED); + COLOR_NAME_MAP.put("purple", PURPLE); + COLOR_NAME_MAP.put("fuchsia", FUCHSIA); + COLOR_NAME_MAP.put("magenta", MAGENTA); + COLOR_NAME_MAP.put("green", GREEN); + COLOR_NAME_MAP.put("lime", LIME); + COLOR_NAME_MAP.put("olive", OLIVE); + COLOR_NAME_MAP.put("yellow", YELLOW); + COLOR_NAME_MAP.put("navy", NAVY); + COLOR_NAME_MAP.put("blue", BLUE); + COLOR_NAME_MAP.put("teal", TEAL); + COLOR_NAME_MAP.put("aqua", AQUA); + COLOR_NAME_MAP.put("cyan", CYAN); + } + + public static int parseColor(String colorExpression) { + Assertions.checkArgument(!TextUtils.isEmpty(colorExpression)); + colorExpression = colorExpression.replace(" ", ""); + if (colorExpression.charAt(0) == '#') { + // Parse using Long to avoid failure when colorExpression is greater than #7FFFFFFF. + int color = (int) Long.parseLong(colorExpression.substring(1), 16); + if (colorExpression.length() == 7) { + // Set the alpha value + color |= 0xFF000000; + } else if (colorExpression.length() == 9) { + // We have #RRGGBBAA, but we need #AARRGGBB + color = ((color & 0xFF) << 24) | (color >>> 8); + } else { + throw new IllegalArgumentException(); + } + return color; + } else if (colorExpression.startsWith(RGBA)) { + Matcher matcher = RGBA_PATTERN.matcher(colorExpression); + if (matcher.matches()) { + return argb( + 255 - Integer.parseInt(matcher.group(4), 10), + Integer.parseInt(matcher.group(1), 10), + Integer.parseInt(matcher.group(2), 10), + Integer.parseInt(matcher.group(3), 10) + ); + } + } else if (colorExpression.startsWith(RGB)) { + Matcher matcher = RGB_PATTERN.matcher(colorExpression); + if (matcher.matches()) { + return rgb( + Integer.parseInt(matcher.group(1), 10), + Integer.parseInt(matcher.group(2), 10), + Integer.parseInt(matcher.group(3), 10) + ); + } + } else { + // we use our own color map + Integer color = COLOR_NAME_MAP.get(Util.toLowerInvariant(colorExpression)); + if (color != null) { + return color; + } + } + throw new IllegalArgumentException(); + } + + private static int argb(int alpha, int red, int green, int blue) { + return (alpha << 24) | (red << 16) | (green << 8) | blue; + } + + private static int rgb(int red, int green, int blue) { + return argb(0xFF, red, green, blue); + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/ttml/TtmlNode.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/ttml/TtmlNode.java new file mode 100755 index 000000000..b768ced1b --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/ttml/TtmlNode.java @@ -0,0 +1,298 @@ +/* + * 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.messenger.exoplayer.text.ttml; + +import org.telegram.messenger.exoplayer.text.Cue; +import org.telegram.messenger.exoplayer.util.Assertions; + +import android.text.SpannableStringBuilder; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.TreeMap; +import java.util.TreeSet; + +/** + * A package internal representation of TTML node. + */ +/* package */ final class TtmlNode { + + public static final long UNDEFINED_TIME = -1; + public static final String TAG_TT = "tt"; + public static final String TAG_HEAD = "head"; + public static final String TAG_BODY = "body"; + public static final String TAG_DIV = "div"; + public static final String TAG_P = "p"; + public static final String TAG_SPAN = "span"; + public static final String TAG_BR = "br"; + public static final String TAG_STYLE = "style"; + public static final String TAG_STYLING = "styling"; + public static final String TAG_LAYOUT = "layout"; + public static final String TAG_REGION = "region"; + public static final String TAG_METADATA = "metadata"; + public static final String TAG_SMPTE_IMAGE = "smpte:image"; + public static final String TAG_SMPTE_DATA = "smpte:data"; + public static final String TAG_SMPTE_INFORMATION = "smpte:information"; + + public static final String ANONYMOUS_REGION_ID = ""; + public static final String ATTR_ID = "id"; + public static final String ATTR_TTS_BACKGROUND_COLOR = "backgroundColor"; + public static final String ATTR_TTS_EXTENT = "extent"; + public static final String ATTR_TTS_FONT_STYLE = "fontStyle"; + public static final String ATTR_TTS_FONT_SIZE = "fontSize"; + public static final String ATTR_TTS_FONT_FAMILY = "fontFamily"; + public static final String ATTR_TTS_FONT_WEIGHT = "fontWeight"; + public static final String ATTR_TTS_COLOR = "color"; + public static final String ATTR_TTS_ORIGIN = "origin"; + public static final String ATTR_TTS_TEXT_DECORATION = "textDecoration"; + public static final String ATTR_TTS_TEXT_ALIGN = "textAlign"; + + public static final String LINETHROUGH = "linethrough"; + public static final String NO_LINETHROUGH = "nolinethrough"; + public static final String UNDERLINE = "underline"; + public static final String NO_UNDERLINE = "nounderline"; + public static final String ITALIC = "italic"; + public static final String BOLD = "bold"; + + public static final String LEFT = "left"; + public static final String CENTER = "center"; + public static final String RIGHT = "right"; + public static final String START = "start"; + public static final String END = "end"; + + public final String tag; + public final String text; + public final boolean isTextNode; + public final long startTimeUs; + public final long endTimeUs; + public final TtmlStyle style; + public final String regionId; + + private final String[] styleIds; + private final HashMap nodeStartsByRegion; + private final HashMap nodeEndsByRegion; + + private List children; + + public static TtmlNode buildTextNode(String text) { + return new TtmlNode(null, TtmlRenderUtil.applyTextElementSpacePolicy(text), UNDEFINED_TIME, + UNDEFINED_TIME, null, null, ANONYMOUS_REGION_ID); + } + + public static TtmlNode buildNode(String tag, long startTimeUs, long endTimeUs, + TtmlStyle style, String[] styleIds, String regionId) { + return new TtmlNode(tag, null, startTimeUs, endTimeUs, style, styleIds, regionId); + } + + private TtmlNode(String tag, String text, long startTimeUs, long endTimeUs, + TtmlStyle style, String[] styleIds, String regionId) { + this.tag = tag; + this.text = text; + this.style = style; + this.styleIds = styleIds; + this.isTextNode = text != null; + this.startTimeUs = startTimeUs; + this.endTimeUs = endTimeUs; + this.regionId = Assertions.checkNotNull(regionId); + nodeStartsByRegion = new HashMap<>(); + nodeEndsByRegion = new HashMap<>(); + } + + public boolean isActive(long timeUs) { + return (startTimeUs == UNDEFINED_TIME && endTimeUs == UNDEFINED_TIME) + || (startTimeUs <= timeUs && endTimeUs == UNDEFINED_TIME) + || (startTimeUs == UNDEFINED_TIME && timeUs < endTimeUs) + || (startTimeUs <= timeUs && timeUs < endTimeUs); + } + + public void addChild(TtmlNode child) { + if (children == null) { + children = new ArrayList<>(); + } + children.add(child); + } + + public TtmlNode getChild(int index) { + if (children == null) { + throw new IndexOutOfBoundsException(); + } + return children.get(index); + } + + public int getChildCount() { + return children == null ? 0 : children.size(); + } + + public long[] getEventTimesUs() { + TreeSet eventTimeSet = new TreeSet<>(); + getEventTimes(eventTimeSet, false); + long[] eventTimes = new long[eventTimeSet.size()]; + int i = 0; + for (long eventTimeUs : eventTimeSet) { + eventTimes[i++] = eventTimeUs; + } + return eventTimes; + } + + private void getEventTimes(TreeSet out, boolean descendsPNode) { + boolean isPNode = TAG_P.equals(tag); + if (descendsPNode || isPNode) { + if (startTimeUs != UNDEFINED_TIME) { + out.add(startTimeUs); + } + if (endTimeUs != UNDEFINED_TIME) { + out.add(endTimeUs); + } + } + if (children == null) { + return; + } + for (int i = 0; i < children.size(); i++) { + children.get(i).getEventTimes(out, descendsPNode || isPNode); + } + } + + public String[] getStyleIds() { + return styleIds; + } + + public List getCues(long timeUs, Map globalStyles, + Map regionMap) { + TreeMap regionOutputs = new TreeMap<>(); + traverseForText(timeUs, false, regionId, regionOutputs); + traverseForStyle(globalStyles, regionOutputs); + List cues = new ArrayList<>(); + for (Entry entry : regionOutputs.entrySet()) { + TtmlRegion region = regionMap.get(entry.getKey()); + cues.add(new Cue(cleanUpText(entry.getValue()), null, region.line, Cue.TYPE_UNSET, + Cue.TYPE_UNSET, region.position, Cue.TYPE_UNSET, region.width)); + } + return cues; + } + + private void traverseForText(long timeUs, boolean descendsPNode, + String inheritedRegion, Map regionOutputs) { + nodeStartsByRegion.clear(); + nodeEndsByRegion.clear(); + String resolvedRegionId = regionId; + if (ANONYMOUS_REGION_ID.equals(resolvedRegionId)) { + resolvedRegionId = inheritedRegion; + } + if (isTextNode && descendsPNode) { + getRegionOutput(resolvedRegionId, regionOutputs).append(text); + } else if (TAG_BR.equals(tag) && descendsPNode) { + getRegionOutput(resolvedRegionId, regionOutputs).append('\n'); + } else if (TAG_METADATA.equals(tag)) { + // Do nothing. + } else if (isActive(timeUs)) { + boolean isPNode = TAG_P.equals(tag); + for (Entry entry : regionOutputs.entrySet()) { + nodeStartsByRegion.put(entry.getKey(), entry.getValue().length()); + } + for (int i = 0; i < getChildCount(); i++) { + getChild(i).traverseForText(timeUs, descendsPNode || isPNode, resolvedRegionId, + regionOutputs); + } + if (isPNode) { + TtmlRenderUtil.endParagraph(getRegionOutput(resolvedRegionId, regionOutputs)); + } + for (Entry entry : regionOutputs.entrySet()) { + nodeEndsByRegion.put(entry.getKey(), entry.getValue().length()); + } + } + } + + private static SpannableStringBuilder getRegionOutput(String resolvedRegionId, + Map regionOutputs) { + if (!regionOutputs.containsKey(resolvedRegionId)) { + regionOutputs.put(resolvedRegionId, new SpannableStringBuilder()); + } + return regionOutputs.get(resolvedRegionId); + } + + private void traverseForStyle(Map globalStyles, + Map regionOutputs) { + for (Entry entry : nodeEndsByRegion.entrySet()) { + String regionId = entry.getKey(); + int start = nodeStartsByRegion.containsKey(regionId) ? nodeStartsByRegion.get(regionId) : 0; + applyStyleToOutput(globalStyles, regionOutputs.get(regionId), start, entry.getValue()); + for (int i = 0; i < getChildCount(); ++i) { + getChild(i).traverseForStyle(globalStyles, regionOutputs); + } + } + } + + private void applyStyleToOutput(Map globalStyles, + SpannableStringBuilder regionOutput, int start, int end) { + if (start != end) { + TtmlStyle resolvedStyle = TtmlRenderUtil.resolveStyle(style, styleIds, globalStyles); + if (resolvedStyle != null) { + TtmlRenderUtil.applyStylesToSpan(regionOutput, start, end, resolvedStyle); + } + } + } + + private SpannableStringBuilder cleanUpText(SpannableStringBuilder builder) { + // Having joined the text elements, we need to do some final cleanup on the result. + // 1. Collapse multiple consecutive spaces into a single space. + int builderLength = builder.length(); + for (int i = 0; i < builderLength; i++) { + if (builder.charAt(i) == ' ') { + int j = i + 1; + while (j < builder.length() && builder.charAt(j) == ' ') { + j++; + } + int spacesToDelete = j - (i + 1); + if (spacesToDelete > 0) { + builder.delete(i, i + spacesToDelete); + builderLength -= spacesToDelete; + } + } + } + // 2. Remove any spaces from the start of each line. + if (builderLength > 0 && builder.charAt(0) == ' ') { + builder.delete(0, 1); + builderLength--; + } + for (int i = 0; i < builderLength - 1; i++) { + if (builder.charAt(i) == '\n' && builder.charAt(i + 1) == ' ') { + builder.delete(i + 1, i + 2); + builderLength--; + } + } + // 3. Remove any spaces from the end of each line. + if (builderLength > 0 && builder.charAt(builderLength - 1) == ' ') { + builder.delete(builderLength - 1, builderLength); + builderLength--; + } + for (int i = 0; i < builderLength - 1; i++) { + if (builder.charAt(i) == ' ' && builder.charAt(i + 1) == '\n') { + builder.delete(i, i + 1); + builderLength--; + } + } + // 4. Trim a trailing newline, if there is one. + if (builderLength > 0 && builder.charAt(builderLength - 1) == '\n') { + builder.delete(builderLength - 1, builderLength); + /*builderLength--;*/ + } + return builder; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/ttml/TtmlParser.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/ttml/TtmlParser.java new file mode 100755 index 000000000..da62303ef --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/ttml/TtmlParser.java @@ -0,0 +1,492 @@ +/*arse + * 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.messenger.exoplayer.text.ttml; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.ParserException; +import org.telegram.messenger.exoplayer.text.Cue; +import org.telegram.messenger.exoplayer.text.SubtitleParser; +import org.telegram.messenger.exoplayer.util.MimeTypes; +import org.telegram.messenger.exoplayer.util.ParserUtil; +import org.telegram.messenger.exoplayer.util.Util; + +import android.text.Layout; +import android.util.Log; +import android.util.Pair; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlPullParserFactory; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A simple TTML parser that supports DFXP presentation profile. + *

+ * Supported features in this parser are: + *

    + *
  • content + *
  • core + *
  • presentation + *
  • profile + *
  • structure + *
  • time-offset + *
  • timing + *
  • tickRate + *
  • time-clock-with-frames + *
  • time-clock + *
  • time-offset-with-frames + *
  • time-offset-with-ticks + *
+ *

+ * @see TTML specification + */ +public final class TtmlParser implements SubtitleParser { + + private static final String TAG = "TtmlParser"; + + private static final String ATTR_BEGIN = "begin"; + private static final String ATTR_DURATION = "dur"; + private static final String ATTR_END = "end"; + private static final String ATTR_STYLE = "style"; + private static final String ATTR_REGION = "region"; + + private static final Pattern CLOCK_TIME = + Pattern.compile("^([0-9][0-9]+):([0-9][0-9]):([0-9][0-9])" + + "(?:(\\.[0-9]+)|:([0-9][0-9])(?:\\.([0-9]+))?)?$"); + private static final Pattern OFFSET_TIME = + Pattern.compile("^([0-9]+(?:\\.[0-9]+)?)(h|m|s|ms|f|t)$"); + private static final Pattern FONT_SIZE = Pattern.compile("^(([0-9]*.)?[0-9]+)(px|em|%)$"); + private static final Pattern PERCENTAGE_COORDINATES = + Pattern.compile("^(\\d+\\.?\\d*?)% (\\d+\\.?\\d*?)%$"); + + // TODO: read and apply the following attributes if specified. + private static final int DEFAULT_FRAMERATE = 30; + private static final int DEFAULT_SUBFRAMERATE = 1; + private static final int DEFAULT_TICKRATE = 1; + + private final XmlPullParserFactory xmlParserFactory; + + public TtmlParser() { + try { + xmlParserFactory = XmlPullParserFactory.newInstance(); + xmlParserFactory.setNamespaceAware(true); + } catch (XmlPullParserException e) { + throw new RuntimeException("Couldn't create XmlPullParserFactory instance", e); + } + } + + @Override + public boolean canParse(String mimeType) { + return MimeTypes.APPLICATION_TTML.equals(mimeType); + } + + @Override + public TtmlSubtitle parse(byte[] bytes, int offset, int length) throws ParserException { + try { + XmlPullParser xmlParser = xmlParserFactory.newPullParser(); + Map globalStyles = new HashMap<>(); + Map regionMap = new HashMap<>(); + regionMap.put(TtmlNode.ANONYMOUS_REGION_ID, new TtmlRegion()); + ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes, offset, length); + xmlParser.setInput(inputStream, null); + TtmlSubtitle ttmlSubtitle = null; + LinkedList nodeStack = new LinkedList<>(); + int unsupportedNodeDepth = 0; + int eventType = xmlParser.getEventType(); + while (eventType != XmlPullParser.END_DOCUMENT) { + TtmlNode parent = nodeStack.peekLast(); + if (unsupportedNodeDepth == 0) { + String name = xmlParser.getName(); + if (eventType == XmlPullParser.START_TAG) { + if (!isSupportedTag(name)) { + Log.i(TAG, "Ignoring unsupported tag: " + xmlParser.getName()); + unsupportedNodeDepth++; + } else if (TtmlNode.TAG_HEAD.equals(name)) { + parseHeader(xmlParser, globalStyles, regionMap); + } else { + try { + TtmlNode node = parseNode(xmlParser, parent, regionMap); + nodeStack.addLast(node); + if (parent != null) { + parent.addChild(node); + } + } catch (ParserException e) { + Log.w(TAG, "Suppressing parser error", e); + // Treat the node (and by extension, all of its children) as unsupported. + unsupportedNodeDepth++; + } + } + } else if (eventType == XmlPullParser.TEXT) { + parent.addChild(TtmlNode.buildTextNode(xmlParser.getText())); + } else if (eventType == XmlPullParser.END_TAG) { + if (xmlParser.getName().equals(TtmlNode.TAG_TT)) { + ttmlSubtitle = new TtmlSubtitle(nodeStack.getLast(), globalStyles, regionMap); + } + nodeStack.removeLast(); + } + } else { + if (eventType == XmlPullParser.START_TAG) { + unsupportedNodeDepth++; + } else if (eventType == XmlPullParser.END_TAG) { + unsupportedNodeDepth--; + } + } + xmlParser.next(); + eventType = xmlParser.getEventType(); + } + return ttmlSubtitle; + } catch (XmlPullParserException xppe) { + throw new ParserException("Unable to parse source", xppe); + } catch (IOException e) { + throw new IllegalStateException("Unexpected error when reading input.", e); + } + } + + private Map parseHeader(XmlPullParser xmlParser, + Map globalStyles, Map globalRegions) + throws IOException, XmlPullParserException { + do { + xmlParser.next(); + if (ParserUtil.isStartTag(xmlParser, TtmlNode.TAG_STYLE)) { + String parentStyleId = ParserUtil.getAttributeValue(xmlParser, ATTR_STYLE); + TtmlStyle style = parseStyleAttributes(xmlParser, new TtmlStyle()); + if (parentStyleId != null) { + String[] ids = parseStyleIds(parentStyleId); + for (int i = 0; i < ids.length; i++) { + style.chain(globalStyles.get(ids[i])); + } + } + if (style.getId() != null) { + globalStyles.put(style.getId(), style); + } + } else if (ParserUtil.isStartTag(xmlParser, TtmlNode.TAG_REGION)) { + Pair ttmlRegionInfo = parseRegionAttributes(xmlParser); + if (ttmlRegionInfo != null) { + globalRegions.put(ttmlRegionInfo.first, ttmlRegionInfo.second); + } + } + } while (!ParserUtil.isEndTag(xmlParser, TtmlNode.TAG_HEAD)); + return globalStyles; + } + + /** + * Parses a region declaration. Supports origin and extent definition but only when defined in + * terms of percentage of the viewport. Regions that do not correctly declare origin are ignored. + */ + private Pair parseRegionAttributes(XmlPullParser xmlParser) { + String regionId = ParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_ID); + String regionOrigin = ParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_TTS_ORIGIN); + String regionExtent = ParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_TTS_EXTENT); + if (regionOrigin == null || regionId == null) { + return null; + } + float position = Cue.DIMEN_UNSET; + float line = Cue.DIMEN_UNSET; + Matcher originMatcher = PERCENTAGE_COORDINATES.matcher(regionOrigin); + if (originMatcher.matches()) { + try { + position = Float.parseFloat(originMatcher.group(1)) / 100.f; + line = Float.parseFloat(originMatcher.group(2)) / 100.f; + } catch (NumberFormatException e) { + Log.w(TAG, "Ignoring region with malformed origin: '" + regionOrigin + "'", e); + position = Cue.DIMEN_UNSET; + } + } + float width = Cue.DIMEN_UNSET; + if (regionExtent != null) { + Matcher extentMatcher = PERCENTAGE_COORDINATES.matcher(regionExtent); + if (extentMatcher.matches()) { + try { + width = Float.parseFloat(extentMatcher.group(1)) / 100.f; + } catch (NumberFormatException e) { + Log.w(TAG, "Ignoring malformed region extent: '" + regionExtent + "'", e); + } + } + } + return position != Cue.DIMEN_UNSET ? new Pair<>(regionId, new TtmlRegion(position, line, width)) + : null; + } + + private String[] parseStyleIds(String parentStyleIds) { + return parentStyleIds.split("\\s+"); + } + + private TtmlStyle parseStyleAttributes(XmlPullParser parser, TtmlStyle style) { + int attributeCount = parser.getAttributeCount(); + for (int i = 0; i < attributeCount; i++) { + String attributeValue = parser.getAttributeValue(i); + switch (parser.getAttributeName(i)) { + case TtmlNode.ATTR_ID: + if (TtmlNode.TAG_STYLE.equals(parser.getName())) { + style = createIfNull(style).setId(attributeValue); + } + break; + case TtmlNode.ATTR_TTS_BACKGROUND_COLOR: + style = createIfNull(style); + try { + style.setBackgroundColor(TtmlColorParser.parseColor(attributeValue)); + } catch (IllegalArgumentException e) { + Log.w(TAG, "failed parsing background value: '" + attributeValue + "'"); + } + break; + case TtmlNode.ATTR_TTS_COLOR: + style = createIfNull(style); + try { + style.setFontColor(TtmlColorParser.parseColor(attributeValue)); + } catch (IllegalArgumentException e) { + Log.w(TAG, "failed parsing color value: '" + attributeValue + "'"); + } + break; + case TtmlNode.ATTR_TTS_FONT_FAMILY: + style = createIfNull(style).setFontFamily(attributeValue); + break; + case TtmlNode.ATTR_TTS_FONT_SIZE: + try { + style = createIfNull(style); + parseFontSize(attributeValue, style); + } catch (ParserException e) { + Log.w(TAG, "failed parsing fontSize value: '" + attributeValue + "'"); + } + break; + case TtmlNode.ATTR_TTS_FONT_WEIGHT: + style = createIfNull(style).setBold( + TtmlNode.BOLD.equalsIgnoreCase(attributeValue)); + break; + case TtmlNode.ATTR_TTS_FONT_STYLE: + style = createIfNull(style).setItalic( + TtmlNode.ITALIC.equalsIgnoreCase(attributeValue)); + break; + case TtmlNode.ATTR_TTS_TEXT_ALIGN: + switch (Util.toLowerInvariant(attributeValue)) { + case TtmlNode.LEFT: + style = createIfNull(style).setTextAlign(Layout.Alignment.ALIGN_NORMAL); + break; + case TtmlNode.START: + style = createIfNull(style).setTextAlign(Layout.Alignment.ALIGN_NORMAL); + break; + case TtmlNode.RIGHT: + style = createIfNull(style).setTextAlign(Layout.Alignment.ALIGN_OPPOSITE); + break; + case TtmlNode.END: + style = createIfNull(style).setTextAlign(Layout.Alignment.ALIGN_OPPOSITE); + break; + case TtmlNode.CENTER: + style = createIfNull(style).setTextAlign(Layout.Alignment.ALIGN_CENTER); + break; + } + break; + case TtmlNode.ATTR_TTS_TEXT_DECORATION: + switch (Util.toLowerInvariant(attributeValue)) { + case TtmlNode.LINETHROUGH: + style = createIfNull(style).setLinethrough(true); + break; + case TtmlNode.NO_LINETHROUGH: + style = createIfNull(style).setLinethrough(false); + break; + case TtmlNode.UNDERLINE: + style = createIfNull(style).setUnderline(true); + break; + case TtmlNode.NO_UNDERLINE: + style = createIfNull(style).setUnderline(false); + break; + } + break; + default: + // ignore + break; + } + } + return style; + } + + private TtmlStyle createIfNull(TtmlStyle style) { + return style == null ? new TtmlStyle() : style; + } + + private TtmlNode parseNode(XmlPullParser parser, TtmlNode parent, + Map regionMap) throws ParserException { + long duration = 0; + long startTime = TtmlNode.UNDEFINED_TIME; + long endTime = TtmlNode.UNDEFINED_TIME; + String regionId = TtmlNode.ANONYMOUS_REGION_ID; + String[] styleIds = null; + int attributeCount = parser.getAttributeCount(); + TtmlStyle style = parseStyleAttributes(parser, null); + for (int i = 0; i < attributeCount; i++) { + String attr = parser.getAttributeName(i); + String value = parser.getAttributeValue(i); + if (ATTR_BEGIN.equals(attr)) { + startTime = parseTimeExpression(value, + DEFAULT_FRAMERATE, DEFAULT_SUBFRAMERATE, DEFAULT_TICKRATE); + } else if (ATTR_END.equals(attr)) { + endTime = parseTimeExpression(value, + DEFAULT_FRAMERATE, DEFAULT_SUBFRAMERATE, DEFAULT_TICKRATE); + } else if (ATTR_DURATION.equals(attr)) { + duration = parseTimeExpression(value, + DEFAULT_FRAMERATE, DEFAULT_SUBFRAMERATE, DEFAULT_TICKRATE); + } else if (ATTR_STYLE.equals(attr)) { + // IDREFS: potentially multiple space delimited ids + String[] ids = parseStyleIds(value); + if (ids.length > 0) { + styleIds = ids; + } + } else if (ATTR_REGION.equals(attr) && regionMap.containsKey(value)) { + // If the region has not been correctly declared or does not define a position, we use the + // anonymous region. + regionId = value; + } else { + // Do nothing. + } + } + if (parent != null && parent.startTimeUs != TtmlNode.UNDEFINED_TIME) { + if (startTime != TtmlNode.UNDEFINED_TIME) { + startTime += parent.startTimeUs; + } + if (endTime != TtmlNode.UNDEFINED_TIME) { + endTime += parent.startTimeUs; + } + } + if (endTime == TtmlNode.UNDEFINED_TIME) { + if (duration > 0) { + // Infer the end time from the duration. + endTime = startTime + duration; + } else if (parent != null && parent.endTimeUs != TtmlNode.UNDEFINED_TIME) { + // If the end time remains unspecified, then it should be inherited from the parent. + endTime = parent.endTimeUs; + } + } + return TtmlNode.buildNode(parser.getName(), startTime, endTime, style, styleIds, regionId); + } + + private static boolean isSupportedTag(String tag) { + if (tag.equals(TtmlNode.TAG_TT) + || tag.equals(TtmlNode.TAG_HEAD) + || tag.equals(TtmlNode.TAG_BODY) + || tag.equals(TtmlNode.TAG_DIV) + || tag.equals(TtmlNode.TAG_P) + || tag.equals(TtmlNode.TAG_SPAN) + || tag.equals(TtmlNode.TAG_BR) + || tag.equals(TtmlNode.TAG_STYLE) + || tag.equals(TtmlNode.TAG_STYLING) + || tag.equals(TtmlNode.TAG_LAYOUT) + || tag.equals(TtmlNode.TAG_REGION) + || tag.equals(TtmlNode.TAG_METADATA) + || tag.equals(TtmlNode.TAG_SMPTE_IMAGE) + || tag.equals(TtmlNode.TAG_SMPTE_DATA) + || tag.equals(TtmlNode.TAG_SMPTE_INFORMATION)) { + return true; + } + return false; + } + + private static void parseFontSize(String expression, TtmlStyle out) throws ParserException { + String[] expressions = expression.split("\\s+"); + Matcher matcher; + if (expressions.length == 1) { + matcher = FONT_SIZE.matcher(expression); + } else if (expressions.length == 2){ + matcher = FONT_SIZE.matcher(expressions[1]); + Log.w(TAG, "Multiple values in fontSize attribute. Picking the second value for vertical font" + + " size and ignoring the first."); + } else { + throw new ParserException("Invalid number of entries for fontSize: " + expressions.length + + "."); + } + + if (matcher.matches()) { + String unit = matcher.group(3); + switch (unit) { + case "px": + out.setFontSizeUnit(TtmlStyle.FONT_SIZE_UNIT_PIXEL); + break; + case "em": + out.setFontSizeUnit(TtmlStyle.FONT_SIZE_UNIT_EM); + break; + case "%": + out.setFontSizeUnit(TtmlStyle.FONT_SIZE_UNIT_PERCENT); + break; + default: + throw new ParserException("Invalid unit for fontSize: '" + unit + "'."); + } + out.setFontSize(Float.valueOf(matcher.group(1))); + } else { + throw new ParserException("Invalid expression for fontSize: '" + expression + "'."); + } + } + + /** + * Parses a time expression, returning the parsed timestamp. + *

+ * For the format of a time expression, see: + * timeExpression + * + * @param time A string that includes the time expression. + * @param frameRate The frame rate of the stream. + * @param subframeRate The sub-frame rate of the stream + * @param tickRate The tick rate of the stream. + * @return The parsed timestamp in microseconds. + * @throws ParserException If the given string does not contain a valid time expression. + */ + private static long parseTimeExpression(String time, int frameRate, int subframeRate, + int tickRate) throws ParserException { + Matcher matcher = CLOCK_TIME.matcher(time); + if (matcher.matches()) { + String hours = matcher.group(1); + double durationSeconds = Long.parseLong(hours) * 3600; + String minutes = matcher.group(2); + durationSeconds += Long.parseLong(minutes) * 60; + String seconds = matcher.group(3); + durationSeconds += Long.parseLong(seconds); + String fraction = matcher.group(4); + durationSeconds += (fraction != null) ? Double.parseDouble(fraction) : 0; + String frames = matcher.group(5); + durationSeconds += (frames != null) ? ((double) Long.parseLong(frames)) / frameRate : 0; + String subframes = matcher.group(6); + durationSeconds += (subframes != null) ? + ((double) Long.parseLong(subframes)) / subframeRate / frameRate : 0; + return (long) (durationSeconds * C.MICROS_PER_SECOND); + } + matcher = OFFSET_TIME.matcher(time); + if (matcher.matches()) { + String timeValue = matcher.group(1); + double offsetSeconds = Double.parseDouble(timeValue); + String unit = matcher.group(2); + if (unit.equals("h")) { + offsetSeconds *= 3600; + } else if (unit.equals("m")) { + offsetSeconds *= 60; + } else if (unit.equals("s")) { + // Do nothing. + } else if (unit.equals("ms")) { + offsetSeconds /= 1000; + } else if (unit.equals("f")) { + offsetSeconds /= frameRate; + } else if (unit.equals("t")) { + offsetSeconds /= tickRate; + } + return (long) (offsetSeconds * C.MICROS_PER_SECOND); + } + throw new ParserException("Malformed time expression: " + time); + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/ttml/TtmlRegion.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/ttml/TtmlRegion.java new file mode 100755 index 000000000..579d841c8 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/ttml/TtmlRegion.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2016 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.messenger.exoplayer.text.ttml; + +import org.telegram.messenger.exoplayer.text.Cue; + +/** + * Represents a TTML Region. + */ +/* package */ final class TtmlRegion { + + public final float position; + public final float line; + public final float width; + + public TtmlRegion() { + this(Cue.DIMEN_UNSET, Cue.DIMEN_UNSET, Cue.DIMEN_UNSET); + } + + public TtmlRegion(float position, float line, float width) { + this.position = position; + this.line = line; + this.width = width; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/ttml/TtmlRenderUtil.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/ttml/TtmlRenderUtil.java new file mode 100755 index 000000000..65c9cceb9 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/ttml/TtmlRenderUtil.java @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2015 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.messenger.exoplayer.text.ttml; + +import android.text.Spannable; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.style.AbsoluteSizeSpan; +import android.text.style.AlignmentSpan; +import android.text.style.BackgroundColorSpan; +import android.text.style.ForegroundColorSpan; +import android.text.style.RelativeSizeSpan; +import android.text.style.StrikethroughSpan; +import android.text.style.StyleSpan; +import android.text.style.TypefaceSpan; +import android.text.style.UnderlineSpan; + +import java.util.Map; + +/** + * Package internal utility class to render styled TtmlNodes. + */ +/* package */ final class TtmlRenderUtil { + + public static TtmlStyle resolveStyle(TtmlStyle style, String[] styleIds, + Map globalStyles) { + if (style == null && styleIds == null) { + // No styles at all. + return null; + } else if (style == null && styleIds.length == 1) { + // Only one single referential style present. + return globalStyles.get(styleIds[0]); + } else if (style == null && styleIds.length > 1) { + // Only multiple referential styles present. + TtmlStyle chainedStyle = new TtmlStyle(); + for (String id : styleIds) { + chainedStyle.chain(globalStyles.get(id)); + } + return chainedStyle; + } else if (style != null && styleIds != null && styleIds.length == 1) { + // Merge a single referential style into inline style. + return style.chain(globalStyles.get(styleIds[0])); + } else if (style != null && styleIds != null && styleIds.length > 1) { + // Merge multiple referential styles into inline style. + for (String id : styleIds) { + style.chain(globalStyles.get(id)); + } + return style; + } + // Only inline styles available. + return style; + } + + public static void applyStylesToSpan(SpannableStringBuilder builder, + int start, int end, TtmlStyle style) { + + if (style.getStyle() != TtmlStyle.UNSPECIFIED) { + builder.setSpan(new StyleSpan(style.getStyle()), start, end, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + if (style.isLinethrough()) { + builder.setSpan(new StrikethroughSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + if (style.isUnderline()) { + builder.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + if (style.hasFontColor()) { + builder.setSpan(new ForegroundColorSpan(style.getFontColor()), start, end, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + if (style.hasBackgroundColor()) { + builder.setSpan(new BackgroundColorSpan(style.getBackgroundColor()), start, end, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + if (style.getFontFamily() != null) { + builder.setSpan(new TypefaceSpan(style.getFontFamily()), start, end, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + if (style.getTextAlign() != null) { + builder.setSpan(new AlignmentSpan.Standard(style.getTextAlign()), start, end, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + if (style.getFontSizeUnit() != TtmlStyle.UNSPECIFIED) { + switch (style.getFontSizeUnit()) { + case TtmlStyle.FONT_SIZE_UNIT_PIXEL: + builder.setSpan(new AbsoluteSizeSpan((int) style.getFontSize(), true), start, end, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + break; + case TtmlStyle.FONT_SIZE_UNIT_EM: + builder.setSpan(new RelativeSizeSpan(style.getFontSize()), start, end, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + break; + case TtmlStyle.FONT_SIZE_UNIT_PERCENT: + builder.setSpan(new RelativeSizeSpan(style.getFontSize() / 100), start, end, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + break; + } + } + } + + /** + * Invoked when the end of a paragraph is encountered. Adds a newline if there are one or more + * non-space characters since the previous newline. + * + * @param builder The builder. + */ + /* package */ static void endParagraph(SpannableStringBuilder builder) { + int position = builder.length() - 1; + while (position >= 0 && builder.charAt(position) == ' ') { + position--; + } + if (position >= 0 && builder.charAt(position) != '\n') { + builder.append('\n'); + } + } + + /** + * Applies the appropriate space policy to the given text element. + * + * @param in The text element to which the policy should be applied. + * @return The result of applying the policy to the text element. + */ + /* package */ static String applyTextElementSpacePolicy(String in) { + // Removes carriage return followed by line feed. See: http://www.w3.org/TR/xml/#sec-line-ends + String out = in.replaceAll("\r\n", "\n"); + // Apply suppress-at-line-break="auto" and + // white-space-treatment="ignore-if-surrounding-linefeed" + out = out.replaceAll(" *\n *", "\n"); + // Apply linefeed-treatment="treat-as-space" + out = out.replaceAll("\n", " "); + // Apply white-space-collapse="true" + out = out.replaceAll("[ \t\\x0B\f\r]+", " "); + return out; + } + + private TtmlRenderUtil() {} + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/ttml/TtmlStyle.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/ttml/TtmlStyle.java new file mode 100755 index 000000000..5c523c475 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/ttml/TtmlStyle.java @@ -0,0 +1,248 @@ +/* + * Copyright (C) 2015 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.messenger.exoplayer.text.ttml; + +import org.telegram.messenger.exoplayer.util.Assertions; + +import android.graphics.Typeface; +import android.text.Layout; + +/** + * Style object of a TtmlNode + */ +/* package */ final class TtmlStyle { + + public static final int UNSPECIFIED = -1; + + public static final int STYLE_NORMAL = Typeface.NORMAL; + public static final int STYLE_BOLD = Typeface.BOLD; + public static final int STYLE_ITALIC = Typeface.ITALIC; + public static final int STYLE_BOLD_ITALIC = Typeface.BOLD_ITALIC; + + public static final int FONT_SIZE_UNIT_PIXEL = 1; + public static final int FONT_SIZE_UNIT_EM = 2; + public static final int FONT_SIZE_UNIT_PERCENT = 3; + + private static final int OFF = 0; + private static final int ON = 1; + + private String fontFamily; + private int fontColor; + private boolean hasFontColor; + private int backgroundColor; + private boolean hasBackgroundColor; + private int linethrough; + private int underline; + private int bold; + private int italic; + private int fontSizeUnit; + private float fontSize; + private String id; + private TtmlStyle inheritableStyle; + private Layout.Alignment textAlign; + + public TtmlStyle() { + linethrough = UNSPECIFIED; + underline = UNSPECIFIED; + bold = UNSPECIFIED; + italic = UNSPECIFIED; + fontSizeUnit = UNSPECIFIED; + } + + /** + * Returns the style or {@link #UNSPECIFIED} when no style information is given. + * + * @return {@link #UNSPECIFIED}, {@link #STYLE_NORMAL}, {@link #STYLE_BOLD}, {@link #STYLE_BOLD} + * or {@link #STYLE_BOLD_ITALIC}. + */ + public int getStyle() { + if (bold == UNSPECIFIED && italic == UNSPECIFIED) { + return UNSPECIFIED; + } + return (bold != UNSPECIFIED ? bold : STYLE_NORMAL) + | (italic != UNSPECIFIED ? italic : STYLE_NORMAL); + } + + public boolean isLinethrough() { + return linethrough == ON; + } + + public TtmlStyle setLinethrough(boolean linethrough) { + Assertions.checkState(inheritableStyle == null); + this.linethrough = linethrough ? ON : OFF; + return this; + } + + public boolean isUnderline() { + return underline == ON; + } + + public TtmlStyle setUnderline(boolean underline) { + Assertions.checkState(inheritableStyle == null); + this.underline = underline ? ON : OFF; + return this; + } + + public String getFontFamily() { + return fontFamily; + } + + public TtmlStyle setFontFamily(String fontFamily) { + Assertions.checkState(inheritableStyle == null); + this.fontFamily = fontFamily; + return this; + } + + public int getFontColor() { + if (!hasFontColor) { + throw new IllegalStateException("Font color has not been defined."); + } + return fontColor; + } + + public TtmlStyle setFontColor(int fontColor) { + Assertions.checkState(inheritableStyle == null); + this.fontColor = fontColor; + hasFontColor = true; + return this; + } + + public boolean hasFontColor() { + return hasFontColor; + } + + public int getBackgroundColor() { + if (!hasBackgroundColor) { + throw new IllegalStateException("Background color has not been defined."); + } + return backgroundColor; + } + + public TtmlStyle setBackgroundColor(int backgroundColor) { + this.backgroundColor = backgroundColor; + hasBackgroundColor = true; + return this; + } + + public boolean hasBackgroundColor() { + return hasBackgroundColor; + } + + public TtmlStyle setBold(boolean isBold) { + Assertions.checkState(inheritableStyle == null); + bold = isBold ? STYLE_BOLD : STYLE_NORMAL; + return this; + } + + public TtmlStyle setItalic(boolean isItalic) { + Assertions.checkState(inheritableStyle == null); + italic = isItalic ? STYLE_ITALIC : STYLE_NORMAL; + return this; + } + + /** + * Inherits from an ancestor style. Properties like tts:backgroundColor which + * are not inheritable are not inherited as well as properties which are already set locally + * are never overridden. + * + * @param ancestor the ancestor style to inherit from + */ + public TtmlStyle inherit(TtmlStyle ancestor) { + return inherit(ancestor, false); + } + + /** + * Chains this style to referential style. Local properties which are already set + * are never overridden. + * + * @param ancestor the referential style to inherit from + */ + public TtmlStyle chain(TtmlStyle ancestor) { + return inherit(ancestor, true); + } + + private TtmlStyle inherit(TtmlStyle ancestor, boolean chaining) { + if (ancestor != null) { + if (!hasFontColor && ancestor.hasFontColor) { + setFontColor(ancestor.fontColor); + } + if (bold == UNSPECIFIED) { + bold = ancestor.bold; + } + if (italic == UNSPECIFIED) { + italic = ancestor.italic; + } + if (fontFamily == null) { + fontFamily = ancestor.fontFamily; + } + if (linethrough == UNSPECIFIED) { + linethrough = ancestor.linethrough; + } + if (underline == UNSPECIFIED) { + underline = ancestor.underline; + } + if (textAlign == null) { + textAlign = ancestor.textAlign; + } + if (fontSizeUnit == UNSPECIFIED) { + fontSizeUnit = ancestor.fontSizeUnit; + fontSize = ancestor.fontSize; + } + // attributes not inherited as of http://www.w3.org/TR/ttml1/ + if (chaining && !hasBackgroundColor && ancestor.hasBackgroundColor) { + setBackgroundColor(ancestor.backgroundColor); + } + } + return this; + } + + public TtmlStyle setId(String id) { + this.id = id; + return this; + } + + public String getId() { + return id; + } + + public Layout.Alignment getTextAlign() { + return textAlign; + } + + public TtmlStyle setTextAlign(Layout.Alignment textAlign) { + this.textAlign = textAlign; + return this; + } + + public TtmlStyle setFontSize(float fontSize) { + this.fontSize = fontSize; + return this; + } + + public TtmlStyle setFontSizeUnit(int fontSizeUnit) { + this.fontSizeUnit = fontSizeUnit; + return this; + } + + public int getFontSizeUnit() { + return fontSizeUnit; + } + + public float getFontSize() { + return fontSize; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/ttml/TtmlSubtitle.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/ttml/TtmlSubtitle.java new file mode 100755 index 000000000..83131ff81 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/ttml/TtmlSubtitle.java @@ -0,0 +1,80 @@ +/* + * 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.messenger.exoplayer.text.ttml; + +import org.telegram.messenger.exoplayer.text.Cue; +import org.telegram.messenger.exoplayer.text.Subtitle; +import org.telegram.messenger.exoplayer.util.Util; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * A representation of a TTML subtitle. + */ +public final class TtmlSubtitle implements Subtitle { + + private final TtmlNode root; + private final long[] eventTimesUs; + private final Map globalStyles; + private final Map regionMap; + + public TtmlSubtitle(TtmlNode root, Map globalStyles, + Map regionMap) { + this.root = root; + this.regionMap = regionMap; + this.globalStyles = globalStyles != null + ? Collections.unmodifiableMap(globalStyles) : Collections.emptyMap(); + this.eventTimesUs = root.getEventTimesUs(); + } + + @Override + public int getNextEventTimeIndex(long timeUs) { + int index = Util.binarySearchCeil(eventTimesUs, timeUs, false, false); + return index < eventTimesUs.length ? index : -1; + } + + @Override + public int getEventTimeCount() { + return eventTimesUs.length; + } + + @Override + public long getEventTime(int index) { + return eventTimesUs[index]; + } + + @Override + public long getLastEventTime() { + return (eventTimesUs.length == 0 ? -1 : eventTimesUs[eventTimesUs.length - 1]); + } + + /* @VisibleForTesting */ + /* package */ TtmlNode getRoot() { + return root; + } + + @Override + public List getCues(long timeUs) { + return root.getCues(timeUs, globalStyles, regionMap); + } + + /* @VisibleForTesting */ + /* package */ Map getGlobalStyles() { + return globalStyles; + } +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/tx3g/Tx3gParser.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/tx3g/Tx3gParser.java new file mode 100755 index 000000000..f4c4a531c --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/tx3g/Tx3gParser.java @@ -0,0 +1,41 @@ +/* + * 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.messenger.exoplayer.text.tx3g; + +import org.telegram.messenger.exoplayer.text.Cue; +import org.telegram.messenger.exoplayer.text.Subtitle; +import org.telegram.messenger.exoplayer.text.SubtitleParser; +import org.telegram.messenger.exoplayer.util.MimeTypes; + +/** + * A {@link SubtitleParser} for tx3g. + *

+ * Currently only supports parsing of a single text track. + */ +public final class Tx3gParser implements SubtitleParser { + + @Override + public boolean canParse(String mimeType) { + return MimeTypes.APPLICATION_TX3G.equals(mimeType); + } + + @Override + public Subtitle parse(byte[] bytes, int offset, int length) { + String cueText = new String(bytes, offset, length); + return new Tx3gSubtitle(new Cue(cueText)); + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/tx3g/Tx3gSubtitle.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/tx3g/Tx3gSubtitle.java new file mode 100755 index 000000000..854268dd5 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/tx3g/Tx3gSubtitle.java @@ -0,0 +1,62 @@ +/* + * 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.messenger.exoplayer.text.tx3g; + +import org.telegram.messenger.exoplayer.text.Cue; +import org.telegram.messenger.exoplayer.text.Subtitle; +import org.telegram.messenger.exoplayer.util.Assertions; + +import java.util.Collections; +import java.util.List; + +/** + * A representation of a tx3g subtitle. + */ +/* package */ final class Tx3gSubtitle implements Subtitle { + + private final List cues; + + public Tx3gSubtitle(Cue cue) { + this.cues = Collections.singletonList(cue); + } + + @Override + public int getNextEventTimeIndex(long timeUs) { + return timeUs < 0 ? 0 : -1; + } + + @Override + public int getEventTimeCount() { + return 1; + } + + @Override + public long getEventTime(int index) { + Assertions.checkArgument(index == 0); + return 0; + } + + @Override + public long getLastEventTime() { + return 0; + } + + @Override + public List getCues(long timeUs) { + return timeUs >= 0 ? cues : Collections.emptyList(); + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/webvtt/Mp4WebvttParser.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/webvtt/Mp4WebvttParser.java new file mode 100755 index 000000000..b6e73fc91 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/webvtt/Mp4WebvttParser.java @@ -0,0 +1,100 @@ +/* + * 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.messenger.exoplayer.text.webvtt; + +import org.telegram.messenger.exoplayer.ParserException; +import org.telegram.messenger.exoplayer.text.Cue; +import org.telegram.messenger.exoplayer.text.SubtitleParser; +import org.telegram.messenger.exoplayer.util.MimeTypes; +import org.telegram.messenger.exoplayer.util.ParsableByteArray; +import org.telegram.messenger.exoplayer.util.Util; + +import java.util.ArrayList; +import java.util.List; + +/** + * A {@link SubtitleParser} for Webvtt embedded in a Mp4 container file. + */ +public final class Mp4WebvttParser implements SubtitleParser { + + private static final int BOX_HEADER_SIZE = 8; + + private static final int TYPE_payl = Util.getIntegerCodeForString("payl"); + private static final int TYPE_sttg = Util.getIntegerCodeForString("sttg"); + private static final int TYPE_vttc = Util.getIntegerCodeForString("vttc"); + + private final ParsableByteArray sampleData; + private final WebvttCue.Builder builder; + + public Mp4WebvttParser() { + sampleData = new ParsableByteArray(); + builder = new WebvttCue.Builder(); + } + + @Override + public boolean canParse(String mimeType) { + return MimeTypes.APPLICATION_MP4VTT.equals(mimeType); + } + + @Override + public Mp4WebvttSubtitle parse(byte[] bytes, int offset, int length) throws ParserException { + // Webvtt in Mp4 samples have boxes inside of them, so we have to do a traditional box parsing: + // first 4 bytes size and then 4 bytes type. + sampleData.reset(bytes, offset + length); + sampleData.setPosition(offset); + List resultingCueList = new ArrayList<>(); + while (sampleData.bytesLeft() > 0) { + if (sampleData.bytesLeft() < BOX_HEADER_SIZE) { + throw new ParserException("Incomplete Mp4Webvtt Top Level box header found."); + } + int boxSize = sampleData.readInt(); + int boxType = sampleData.readInt(); + if (boxType == TYPE_vttc) { + resultingCueList.add(parseVttCueBox(sampleData, builder, boxSize - BOX_HEADER_SIZE)); + } else { + // Peers of the VTTCueBox are still not supported and are skipped. + sampleData.skipBytes(boxSize - BOX_HEADER_SIZE); + } + } + return new Mp4WebvttSubtitle(resultingCueList); + } + + private static Cue parseVttCueBox(ParsableByteArray sampleData, WebvttCue.Builder builder, + int remainingCueBoxBytes) throws ParserException { + builder.reset(); + while (remainingCueBoxBytes > 0) { + if (remainingCueBoxBytes < BOX_HEADER_SIZE) { + throw new ParserException("Incomplete vtt cue box header found."); + } + int boxSize = sampleData.readInt(); + int boxType = sampleData.readInt(); + remainingCueBoxBytes -= BOX_HEADER_SIZE; + int payloadLength = boxSize - BOX_HEADER_SIZE; + String boxPayload = new String(sampleData.data, sampleData.getPosition(), payloadLength); + sampleData.skipBytes(payloadLength); + remainingCueBoxBytes -= payloadLength; + if (boxType == TYPE_sttg) { + WebvttCueParser.parseCueSettingsList(boxPayload, builder); + } else if (boxType == TYPE_payl) { + WebvttCueParser.parseCueText(boxPayload.trim(), builder); + } else { + // Other VTTCueBox children are still not supported and are ignored. + } + } + return builder.build(); + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/webvtt/Mp4WebvttSubtitle.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/webvtt/Mp4WebvttSubtitle.java new file mode 100755 index 000000000..586f64eee --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/webvtt/Mp4WebvttSubtitle.java @@ -0,0 +1,61 @@ +/* + * 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.messenger.exoplayer.text.webvtt; + +import org.telegram.messenger.exoplayer.text.Cue; +import org.telegram.messenger.exoplayer.text.Subtitle; +import org.telegram.messenger.exoplayer.util.Assertions; + +import java.util.Collections; +import java.util.List; + +/** + * Representation of a Webvtt subtitle embedded in a MP4 container file. + */ +/* package */ final class Mp4WebvttSubtitle implements Subtitle { + + private final List cues; + + public Mp4WebvttSubtitle(List cueList) { + cues = Collections.unmodifiableList(cueList); + } + + @Override + public int getNextEventTimeIndex(long timeUs) { + return timeUs < 0 ? 0 : -1; + } + + @Override + public int getEventTimeCount() { + return 1; + } + + @Override + public long getEventTime(int index) { + Assertions.checkArgument(index == 0); + return 0; + } + + @Override + public long getLastEventTime() { + return 0; + } + + @Override + public List getCues(long timeUs) { + return timeUs >= 0 ? cues : Collections.emptyList(); + } +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/webvtt/WebvttCue.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/webvtt/WebvttCue.java new file mode 100755 index 000000000..d3f22e4cf --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/webvtt/WebvttCue.java @@ -0,0 +1,180 @@ +/* + * 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.messenger.exoplayer.text.webvtt; + +import org.telegram.messenger.exoplayer.text.Cue; + +import android.text.Layout.Alignment; +import android.util.Log; + +/** + * A representation of a WebVTT cue. + */ +/* package */ final class WebvttCue extends Cue { + + public final long startTime; + public final long endTime; + + public WebvttCue(CharSequence text) { + this(0, 0, text); + } + + public WebvttCue(long startTime, long endTime, CharSequence text) { + this(startTime, endTime, text, null, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.TYPE_UNSET, + Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.DIMEN_UNSET); + } + + public WebvttCue(long startTime, long endTime, CharSequence text, Alignment textAlignment, + float line, int lineType, int lineAnchor, float position, int positionAnchor, float width) { + super(text, textAlignment, line, lineType, lineAnchor, position, positionAnchor, width); + this.startTime = startTime; + this.endTime = endTime; + } + + /** + * Returns whether or not this cue should be placed in the default position and rolled-up with + * the other "normal" cues. + * + * @return True if this cue should be placed in the default position; false otherwise. + */ + public boolean isNormalCue() { + return (line == DIMEN_UNSET && position == DIMEN_UNSET); + } + + /** + * Builder for WebVTT cues. + */ + @SuppressWarnings("hiding") + public static final class Builder { + + private static final String TAG = "WebvttCueBuilder"; + + private long startTime; + private long endTime; + private CharSequence text; + private Alignment textAlignment; + private float line; + private int lineType; + private int lineAnchor; + private float position; + private int positionAnchor; + private float width; + + // Initialization methods + + public Builder() { + reset(); + } + + public void reset() { + startTime = 0; + endTime = 0; + text = null; + textAlignment = null; + line = Cue.DIMEN_UNSET; + lineType = Cue.TYPE_UNSET; + lineAnchor = Cue.TYPE_UNSET; + position = Cue.DIMEN_UNSET; + positionAnchor = Cue.TYPE_UNSET; + width = Cue.DIMEN_UNSET; + } + + // Construction methods + + public WebvttCue build() { + if (position != Cue.DIMEN_UNSET && positionAnchor == Cue.TYPE_UNSET) { + derivePositionAnchorFromAlignment(); + } + return new WebvttCue(startTime, endTime, text, textAlignment, line, lineType, lineAnchor, + position, positionAnchor, width); + } + + public Builder setStartTime(long time) { + startTime = time; + return this; + } + + public Builder setEndTime(long time) { + endTime = time; + return this; + } + + public Builder setText(CharSequence aText) { + text = aText; + return this; + } + + public Builder setTextAlignment(Alignment textAlignment) { + this.textAlignment = textAlignment; + return this; + } + + public Builder setLine(float line) { + this.line = line; + return this; + } + + public Builder setLineType(int lineType) { + this.lineType = lineType; + return this; + } + + public Builder setLineAnchor(int lineAnchor) { + this.lineAnchor = lineAnchor; + return this; + } + + public Builder setPosition(float position) { + this.position = position; + return this; + } + + public Builder setPositionAnchor(int positionAnchor) { + this.positionAnchor = positionAnchor; + return this; + } + + public Builder setWidth(float width) { + this.width = width; + return this; + } + + private Builder derivePositionAnchorFromAlignment() { + if (textAlignment == null) { + positionAnchor = Cue.TYPE_UNSET; + } else { + switch (textAlignment) { + case ALIGN_NORMAL: + positionAnchor = Cue.ANCHOR_TYPE_START; + break; + case ALIGN_CENTER: + positionAnchor = Cue.ANCHOR_TYPE_MIDDLE; + break; + case ALIGN_OPPOSITE: + positionAnchor = Cue.ANCHOR_TYPE_END; + break; + default: + Log.w(TAG, "Unrecognized alignment: " + textAlignment); + positionAnchor = Cue.ANCHOR_TYPE_START; + break; + } + } + return this; + } + + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/webvtt/WebvttCueParser.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/webvtt/WebvttCueParser.java new file mode 100755 index 000000000..7f8d1815d --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/webvtt/WebvttCueParser.java @@ -0,0 +1,401 @@ +/* + * 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.messenger.exoplayer.text.webvtt; + +import org.telegram.messenger.exoplayer.text.Cue; +import org.telegram.messenger.exoplayer.util.ParsableByteArray; + +import android.graphics.Typeface; +import android.text.Layout.Alignment; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.style.StyleSpan; +import android.text.style.UnderlineSpan; +import android.util.Log; + +import java.util.Stack; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Parser for WebVTT cues. (https://w3c.github.io/webvtt/#cues) + */ +public final class WebvttCueParser { + + public static final Pattern CUE_HEADER_PATTERN = Pattern + .compile("^(\\S+)\\s+-->\\s+(\\S+)(.*)?$"); + + private static final Pattern COMMENT = Pattern.compile("^NOTE((\u0020|\u0009).*)?$"); + private static final Pattern CUE_SETTING_PATTERN = Pattern.compile("(\\S+?):(\\S+)"); + + private static final char CHAR_LESS_THAN = '<'; + private static final char CHAR_GREATER_THAN = '>'; + private static final char CHAR_SLASH = '/'; + private static final char CHAR_AMPERSAND = '&'; + private static final char CHAR_SEMI_COLON = ';'; + private static final char CHAR_SPACE = ' '; + private static final String SPACE = " "; + + private static final String ENTITY_LESS_THAN = "lt"; + private static final String ENTITY_GREATER_THAN = "gt"; + private static final String ENTITY_AMPERSAND = "amp"; + private static final String ENTITY_NON_BREAK_SPACE = "nbsp"; + + private static final String TAG_BOLD = "b"; + private static final String TAG_ITALIC = "i"; + private static final String TAG_UNDERLINE = "u"; + private static final String TAG_CLASS = "c"; + private static final String TAG_VOICE = "v"; + private static final String TAG_LANG = "lang"; + + private static final int STYLE_BOLD = Typeface.BOLD; + private static final int STYLE_ITALIC = Typeface.ITALIC; + + private static final String TAG = "WebvttCueParser"; + + private final StringBuilder textBuilder; + + public WebvttCueParser() { + textBuilder = new StringBuilder(); + } + + /** + * Parses the next valid WebVTT cue in a parsable array, including timestamps, settings and text. + * + * @param webvttData Parsable WebVTT file data. + * @param builder Builder for WebVTT Cues. + * @return True if a valid Cue was found, false otherwise. + */ + /* package */ boolean parseNextValidCue(ParsableByteArray webvttData, WebvttCue.Builder builder) { + Matcher cueHeaderMatcher; + while ((cueHeaderMatcher = findNextCueHeader(webvttData)) != null) { + if (parseCue(cueHeaderMatcher, webvttData, builder, textBuilder)) { + return true; + } + } + return false; + } + + /** + * Parses a string containing a list of cue settings. + * + * @param cueSettingsList String containing the settings for a given cue. + * @param builder The {@link WebvttCue.Builder} where incremental construction takes place. + */ + /* package */ static void parseCueSettingsList(String cueSettingsList, + WebvttCue.Builder builder) { + // Parse the cue settings list. + Matcher cueSettingMatcher = CUE_SETTING_PATTERN.matcher(cueSettingsList); + while (cueSettingMatcher.find()) { + String name = cueSettingMatcher.group(1); + String value = cueSettingMatcher.group(2); + try { + if ("line".equals(name)) { + parseLineAttribute(value, builder); + } else if ("align".equals(name)) { + builder.setTextAlignment(parseTextAlignment(value)); + } else if ("position".equals(name)) { + parsePositionAttribute(value, builder); + } else if ("size".equals(name)) { + builder.setWidth(WebvttParserUtil.parsePercentage(value)); + } else { + Log.w(TAG, "Unknown cue setting " + name + ":" + value); + } + } catch (NumberFormatException e) { + Log.w(TAG, "Skipping bad cue setting: " + cueSettingMatcher.group()); + } + } + } + + /** + * Reads lines up to and including the next WebVTT cue header. + * + * @param input The input from which lines should be read. + * @return A {@link Matcher} for the WebVTT cue header, or null if the end of the input was + * reached without a cue header being found. In the case that a cue header is found, groups 1, + * 2 and 3 of the returned matcher contain the start time, end time and settings list. + */ + public static Matcher findNextCueHeader(ParsableByteArray input) { + String line; + while ((line = input.readLine()) != null) { + if (COMMENT.matcher(line).matches()) { + // Skip until the end of the comment block. + while ((line = input.readLine()) != null && !line.isEmpty()) {} + } else { + Matcher cueHeaderMatcher = WebvttCueParser.CUE_HEADER_PATTERN.matcher(line); + if (cueHeaderMatcher.matches()) { + return cueHeaderMatcher; + } + } + } + return null; + } + + /** + * Parses the text payload of a WebVTT Cue and applies modifications on {@link WebvttCue.Builder}. + * + * @param markup The markup text to be parsed. + * @param builder Target builder. + */ + /* package */ static void parseCueText(String markup, WebvttCue.Builder builder) { + SpannableStringBuilder spannedText = new SpannableStringBuilder(); + Stack startTagStack = new Stack<>(); + String[] tagTokens; + int pos = 0; + while (pos < markup.length()) { + char curr = markup.charAt(pos); + switch (curr) { + case CHAR_LESS_THAN: + if (pos + 1 >= markup.length()) { + pos++; + break; // avoid ArrayOutOfBoundsException + } + int ltPos = pos; + boolean isClosingTag = markup.charAt(ltPos + 1) == CHAR_SLASH; + pos = findEndOfTag(markup, ltPos + 1); + boolean isVoidTag = markup.charAt(pos - 2) == CHAR_SLASH; + + tagTokens = tokenizeTag(markup.substring( + ltPos + (isClosingTag ? 2 : 1), isVoidTag ? pos - 2 : pos - 1)); + if (tagTokens == null || !isSupportedTag(tagTokens[0])) { + continue; + } + if (isClosingTag) { + StartTag startTag; + do { + if (startTagStack.isEmpty()) { + break; + } + startTag = startTagStack.pop(); + applySpansForTag(startTag, spannedText); + } while(!startTag.name.equals(tagTokens[0])); + } else if (!isVoidTag) { + startTagStack.push(new StartTag(tagTokens[0], spannedText.length())); + } + break; + case CHAR_AMPERSAND: + int semiColonEnd = markup.indexOf(CHAR_SEMI_COLON, pos + 1); + int spaceEnd = markup.indexOf(CHAR_SPACE, pos + 1); + int entityEnd = semiColonEnd == -1 ? spaceEnd + : spaceEnd == -1 ? semiColonEnd : Math.min(semiColonEnd, spaceEnd); + if (entityEnd != -1) { + applyEntity(markup.substring(pos + 1, entityEnd), spannedText); + if (entityEnd == spaceEnd) { + spannedText.append(" "); + } + pos = entityEnd + 1; + } else { + spannedText.append(curr); + pos++; + } + break; + default: + spannedText.append(curr); + pos++; + break; + } + } + // apply unclosed tags + while (!startTagStack.isEmpty()) { + applySpansForTag(startTagStack.pop(), spannedText); + } + builder.setText(spannedText); + } + + private static boolean parseCue(Matcher cueHeaderMatcher, ParsableByteArray webvttData, + WebvttCue.Builder builder, StringBuilder textBuilder) { + try { + // Parse the cue start and end times. + builder.setStartTime(WebvttParserUtil.parseTimestampUs(cueHeaderMatcher.group(1))) + .setEndTime(WebvttParserUtil.parseTimestampUs(cueHeaderMatcher.group(2))); + } catch (NumberFormatException e) { + Log.w(TAG, "Skipping cue with bad header: " + cueHeaderMatcher.group()); + return false; + } + + parseCueSettingsList(cueHeaderMatcher.group(3), builder); + + // Parse the cue text. + textBuilder.setLength(0); + String line; + while ((line = webvttData.readLine()) != null && !line.isEmpty()) { + if (textBuilder.length() > 0) { + textBuilder.append("\n"); + } + textBuilder.append(line.trim()); + } + parseCueText(textBuilder.toString(), builder); + return true; + } + + // Internal methods + + private static void parseLineAttribute(String s, WebvttCue.Builder builder) + throws NumberFormatException { + int commaPosition = s.indexOf(','); + if (commaPosition != -1) { + builder.setLineAnchor(parsePositionAnchor(s.substring(commaPosition + 1))); + s = s.substring(0, commaPosition); + } else { + builder.setLineAnchor(Cue.TYPE_UNSET); + } + if (s.endsWith("%")) { + builder.setLine(WebvttParserUtil.parsePercentage(s)).setLineType(Cue.LINE_TYPE_FRACTION); + } else { + builder.setLine(Integer.parseInt(s)).setLineType(Cue.LINE_TYPE_NUMBER); + } + } + + private static void parsePositionAttribute(String s, WebvttCue.Builder builder) + throws NumberFormatException { + int commaPosition = s.indexOf(','); + if (commaPosition != -1) { + builder.setPositionAnchor(parsePositionAnchor(s.substring(commaPosition + 1))); + s = s.substring(0, commaPosition); + } else { + builder.setPositionAnchor(Cue.TYPE_UNSET); + } + builder.setPosition(WebvttParserUtil.parsePercentage(s)); + } + + private static int parsePositionAnchor(String s) { + switch (s) { + case "start": + return Cue.ANCHOR_TYPE_START; + case "center": + case "middle": + return Cue.ANCHOR_TYPE_MIDDLE; + case "end": + return Cue.ANCHOR_TYPE_END; + default: + Log.w(TAG, "Invalid anchor value: " + s); + return Cue.TYPE_UNSET; + } + } + + private static Alignment parseTextAlignment(String s) { + switch (s) { + case "start": + case "left": + return Alignment.ALIGN_NORMAL; + case "center": + case "middle": + return Alignment.ALIGN_CENTER; + case "end": + case "right": + return Alignment.ALIGN_OPPOSITE; + default: + Log.w(TAG, "Invalid alignment value: " + s); + return null; + } + } + + /** + * Find end of tag (>). The position returned is the position of the > plus one (exclusive). + * + * @param markup The WebVTT cue markup to be parsed. + * @param startPos the position from where to start searching for the end of tag. + * @return the position of the end of tag plus 1 (one). + */ + private static int findEndOfTag(String markup, int startPos) { + int idx = markup.indexOf(CHAR_GREATER_THAN, startPos); + return idx == -1 ? markup.length() : idx + 1; + } + + private static void applyEntity(String entity, SpannableStringBuilder spannedText) { + switch (entity) { + case ENTITY_LESS_THAN: + spannedText.append('<'); + break; + case ENTITY_GREATER_THAN: + spannedText.append('>'); + break; + case ENTITY_NON_BREAK_SPACE: + spannedText.append(' '); + break; + case ENTITY_AMPERSAND: + spannedText.append('&'); + break; + default: + Log.w(TAG, "ignoring unsupported entity: '&" + entity + ";'"); + break; + } + } + + private static boolean isSupportedTag(String tagName) { + switch (tagName) { + case TAG_BOLD: + case TAG_CLASS: + case TAG_ITALIC: + case TAG_LANG: + case TAG_UNDERLINE: + case TAG_VOICE: + return true; + default: + return false; + } + } + + private static void applySpansForTag(StartTag startTag, SpannableStringBuilder spannedText) { + switch(startTag.name) { + case TAG_BOLD: + spannedText.setSpan(new StyleSpan(STYLE_BOLD), startTag.position, + spannedText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + return; + case TAG_ITALIC: + spannedText.setSpan(new StyleSpan(STYLE_ITALIC), startTag.position, + spannedText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + return; + case TAG_UNDERLINE: + spannedText.setSpan(new UnderlineSpan(), startTag.position, + spannedText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + return; + default: + break; + } + } + + /** + * Tokenizes a tag expression into tag name (pos 0) and classes (pos 1..n). + * + * @param fullTagExpression characters between &lt: and &gt; of a start or end tag + * @return an array of Strings with the tag name at pos 0 followed by style classes + * or null if it's an empty tag: '<>' + */ + private static String[] tokenizeTag(String fullTagExpression) { + fullTagExpression = fullTagExpression.replace("\\s+", " ").trim(); + if (fullTagExpression.length() == 0) { + return null; + } + if (fullTagExpression.contains(SPACE)) { + fullTagExpression = fullTagExpression.substring(0, fullTagExpression.indexOf(SPACE)); + } + return fullTagExpression.split("\\."); + } + + private static final class StartTag { + + public final String name; + public final int position; + + public StartTag(String name, int position) { + this.position = position; + this.name = name; + } + + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/webvtt/WebvttParser.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/webvtt/WebvttParser.java new file mode 100755 index 000000000..9b2b62311 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/webvtt/WebvttParser.java @@ -0,0 +1,68 @@ +/* + * 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.messenger.exoplayer.text.webvtt; + +import org.telegram.messenger.exoplayer.ParserException; +import org.telegram.messenger.exoplayer.text.SubtitleParser; +import org.telegram.messenger.exoplayer.util.MimeTypes; +import org.telegram.messenger.exoplayer.util.ParsableByteArray; + +import android.text.TextUtils; + +import java.util.ArrayList; + +/** + * A simple WebVTT parser. + *

+ * @see WebVTT specification + */ +public final class WebvttParser implements SubtitleParser { + + private final WebvttCueParser cueParser; + private final ParsableByteArray parsableWebvttData; + private final WebvttCue.Builder webvttCueBuilder; + + public WebvttParser() { + cueParser = new WebvttCueParser(); + parsableWebvttData = new ParsableByteArray(); + webvttCueBuilder = new WebvttCue.Builder(); + } + + @Override + public final boolean canParse(String mimeType) { + return MimeTypes.TEXT_VTT.equals(mimeType); + } + + @Override + public final WebvttSubtitle parse(byte[] bytes, int offset, int length) throws ParserException { + parsableWebvttData.reset(bytes, offset + length); + parsableWebvttData.setPosition(offset); + webvttCueBuilder.reset(); // In case a previous parse run failed with a ParserException. + + // Validate the first line of the header, and skip the remainder. + WebvttParserUtil.validateWebvttHeaderLine(parsableWebvttData); + while (!TextUtils.isEmpty(parsableWebvttData.readLine())) {} + + // Extract Cues + ArrayList subtitles = new ArrayList<>(); + while (cueParser.parseNextValidCue(parsableWebvttData, webvttCueBuilder)) { + subtitles.add(webvttCueBuilder.build()); + webvttCueBuilder.reset(); + } + return new WebvttSubtitle(subtitles); + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/webvtt/WebvttParserUtil.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/webvtt/WebvttParserUtil.java new file mode 100755 index 000000000..a8420d29f --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/webvtt/WebvttParserUtil.java @@ -0,0 +1,75 @@ +/* + * 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.messenger.exoplayer.text.webvtt; + +import org.telegram.messenger.exoplayer.ParserException; +import org.telegram.messenger.exoplayer.util.ParsableByteArray; + +import java.util.regex.Pattern; + +/** + * Utility methods for parsing WebVTT data. + */ +public final class WebvttParserUtil { + + private static final Pattern HEADER = Pattern.compile("^\uFEFF?WEBVTT((\u0020|\u0009).*)?$"); + + private WebvttParserUtil() {} + + /** + * Reads and validates the first line of a WebVTT file. + * + * @param input The input from which the line should be read. + * @throws ParserException If the line isn't the start of a valid WebVTT file. + */ + public static void validateWebvttHeaderLine(ParsableByteArray input) throws ParserException { + String line = input.readLine(); + if (line == null || !HEADER.matcher(line).matches()) { + throw new ParserException("Expected WEBVTT. Got " + line); + } + } + + /** + * Parses a WebVTT timestamp. + * + * @param timestamp The timestamp string. + * @return The parsed timestamp in microseconds. + * @throws NumberFormatException If the timestamp could not be parsed. + */ + public static long parseTimestampUs(String timestamp) throws NumberFormatException { + long value = 0; + String[] parts = timestamp.split("\\.", 2); + String[] subparts = parts[0].split(":"); + for (int i = 0; i < subparts.length; i++) { + value = value * 60 + Long.parseLong(subparts[i]); + } + return (value * 1000 + Long.parseLong(parts[1])) * 1000; + } + + /** + * Parses a percentage and returns a scaled float. + * @param s contains the number to parse. + * @return a float scaled number. 1.0 represents 100%. + * @throws NumberFormatException if the number format is invalid or does not end with '%'. + */ + public static float parsePercentage(String s) throws NumberFormatException { + if (!s.endsWith("%")) { + throw new NumberFormatException("Percentages must end with %"); + } + return Float.parseFloat(s.substring(0, s.length() - 1)) / 100; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/webvtt/WebvttSubtitle.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/webvtt/WebvttSubtitle.java new file mode 100755 index 000000000..e23f3a18d --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/text/webvtt/WebvttSubtitle.java @@ -0,0 +1,127 @@ +/* + * 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.messenger.exoplayer.text.webvtt; + +import org.telegram.messenger.exoplayer.text.Cue; +import org.telegram.messenger.exoplayer.text.Subtitle; +import org.telegram.messenger.exoplayer.util.Assertions; +import org.telegram.messenger.exoplayer.util.Util; + +import android.text.SpannableStringBuilder; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * A representation of a WebVTT subtitle. + */ +public final class WebvttSubtitle implements Subtitle { + + private final List cues; + private final int numCues; + private final long[] cueTimesUs; + private final long[] sortedCueTimesUs; + + /** + * @param cues A list of the cues in this subtitle. + */ + public WebvttSubtitle(List cues) { + this.cues = cues; + numCues = cues.size(); + cueTimesUs = new long[2 * numCues]; + for (int cueIndex = 0; cueIndex < numCues; cueIndex++) { + WebvttCue cue = cues.get(cueIndex); + int arrayIndex = cueIndex * 2; + cueTimesUs[arrayIndex] = cue.startTime; + cueTimesUs[arrayIndex + 1] = cue.endTime; + } + sortedCueTimesUs = Arrays.copyOf(cueTimesUs, cueTimesUs.length); + Arrays.sort(sortedCueTimesUs); + } + + @Override + public int getNextEventTimeIndex(long timeUs) { + int index = Util.binarySearchCeil(sortedCueTimesUs, timeUs, false, false); + return index < sortedCueTimesUs.length ? index : -1; + } + + @Override + public int getEventTimeCount() { + return sortedCueTimesUs.length; + } + + @Override + public long getEventTime(int index) { + Assertions.checkArgument(index >= 0); + Assertions.checkArgument(index < sortedCueTimesUs.length); + return sortedCueTimesUs[index]; + } + + @Override + public long getLastEventTime() { + if (getEventTimeCount() == 0) { + return -1; + } + return sortedCueTimesUs[sortedCueTimesUs.length - 1]; + } + + @Override + public List getCues(long timeUs) { + ArrayList list = null; + WebvttCue firstNormalCue = null; + SpannableStringBuilder normalCueTextBuilder = null; + + for (int i = 0; i < numCues; i++) { + if ((cueTimesUs[i * 2] <= timeUs) && (timeUs < cueTimesUs[i * 2 + 1])) { + if (list == null) { + list = new ArrayList<>(); + } + WebvttCue cue = cues.get(i); + if (cue.isNormalCue()) { + // we want to merge all of the normal cues into a single cue to ensure they are drawn + // correctly (i.e. don't overlap) and to emulate roll-up, but only if there are multiple + // normal cues, otherwise we can just append the single normal cue + if (firstNormalCue == null) { + firstNormalCue = cue; + } else if (normalCueTextBuilder == null) { + normalCueTextBuilder = new SpannableStringBuilder(); + normalCueTextBuilder.append(firstNormalCue.text).append("\n").append(cue.text); + } else { + normalCueTextBuilder.append("\n").append(cue.text); + } + } else { + list.add(cue); + } + } + } + if (normalCueTextBuilder != null) { + // there were multiple normal cues, so create a new cue with all of the text + list.add(new WebvttCue(normalCueTextBuilder)); + } else if (firstNormalCue != null) { + // there was only a single normal cue, so just add it to the list + list.add(firstNormalCue); + } + + if (list != null) { + return list; + } else { + return Collections.emptyList(); + } + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/Allocation.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/Allocation.java new file mode 100755 index 000000000..0d21c9a2b --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/Allocation.java @@ -0,0 +1,53 @@ +/* + * 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.messenger.exoplayer.upstream; + +/** + * An allocation within a byte array. + *

+ * The allocation's length is obtained by calling {@link Allocator#getIndividualAllocationLength()} + * on the {@link Allocator} from which it was obtained. + */ +public final class Allocation { + + /** + * The array containing the allocated space. The allocated space may not be at the start of the + * array, and so {@link #translateOffset(int)} method must be used when indexing into it. + */ + public final byte[] data; + + private final int offset; + + /** + * @param data The array containing the allocated space. + * @param offset The offset of the allocated space within the array. + */ + public Allocation(byte[] data, int offset) { + this.data = data; + this.offset = offset; + } + + /** + * Translates a zero-based offset into the allocation to the corresponding {@link #data} offset. + * + * @param offset The zero-based offset to translate. + * @return The corresponding offset in {@link #data}. + */ + public int translateOffset(int offset) { + return this.offset + offset; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/Allocator.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/Allocator.java new file mode 100755 index 000000000..f6e919f92 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/Allocator.java @@ -0,0 +1,67 @@ +/* + * 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.messenger.exoplayer.upstream; + +/** + * A source of allocations. + */ +public interface Allocator { + + /** + * Obtain an {@link Allocation}. + *

+ * When the caller has finished with the {@link Allocation}, it should be returned by calling + * {@link #release(Allocation)}. + * + * @return The {@link Allocation}. + */ + Allocation allocate(); + + /** + * Return an {@link Allocation}. + * + * @param allocation The {@link Allocation} being returned. + */ + void release(Allocation allocation); + + /** + * Hints to the {@link Allocator} that it should make a best effort to release any memory that it + * has allocated, beyond the specified target number of bytes. + * + * @param targetSize The target size in bytes. + */ + void trim(int targetSize); + + /** + * Blocks execution until the number of bytes allocated is not greater than the limit, or the + * thread is interrupted. + * + * @param limit The limit in bytes. + * @throws InterruptedException If the thread is interrupted. + */ + void blockWhileTotalBytesAllocatedExceeds(int limit) throws InterruptedException; + + /** + * Returns the total number of bytes currently allocated. + */ + int getTotalBytesAllocated(); + + /** + * Returns the length of each individual {@link Allocation}. + */ + int getIndividualAllocationLength(); + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/AssetDataSource.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/AssetDataSource.java new file mode 100755 index 000000000..8a39451d7 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/AssetDataSource.java @@ -0,0 +1,160 @@ +/* + * 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.messenger.exoplayer.upstream; + +import org.telegram.messenger.exoplayer.C; + +import android.content.Context; +import android.content.res.AssetManager; + +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; + +/** + * A local asset {@link UriDataSource}. + */ +public final class AssetDataSource implements UriDataSource { + + /** + * Thrown when an {@link IOException} is encountered reading a local asset. + */ + public static final class AssetDataSourceException extends IOException { + + public AssetDataSourceException(IOException cause) { + super(cause); + } + + } + + private final AssetManager assetManager; + private final TransferListener listener; + + private String uriString; + private InputStream inputStream; + private long bytesRemaining; + private boolean opened; + + /** + * Constructs a new {@link DataSource} that retrieves data from a local asset. + */ + public AssetDataSource(Context context) { + this(context, null); + } + + /** + * Constructs a new {@link DataSource} that retrieves data from a local asset. + * + * @param listener An optional listener. Specify {@code null} for no listener. + */ + public AssetDataSource(Context context, TransferListener listener) { + this.assetManager = context.getAssets(); + this.listener = listener; + } + + @Override + public long open(DataSpec dataSpec) throws AssetDataSourceException { + try { + uriString = dataSpec.uri.toString(); + String path = dataSpec.uri.getPath(); + if (path.startsWith("/android_asset/")) { + path = path.substring(15); + } else if (path.startsWith("/")) { + path = path.substring(1); + } + uriString = dataSpec.uri.toString(); + inputStream = assetManager.open(path, AssetManager.ACCESS_RANDOM); + long skipped = inputStream.skip(dataSpec.position); + if (skipped < dataSpec.position) { + // assetManager.open() returns an AssetInputStream, whose skip() implementation only skips + // fewer bytes than requested if the skip is beyond the end of the asset's data. + throw new EOFException(); + } + if (dataSpec.length != C.LENGTH_UNBOUNDED) { + bytesRemaining = dataSpec.length; + } else { + bytesRemaining = inputStream.available(); + if (bytesRemaining == Integer.MAX_VALUE) { + // assetManager.open() returns an AssetInputStream, whose available() implementation + // returns Integer.MAX_VALUE if the remaining length is greater than (or equal to) + // Integer.MAX_VALUE. We don't know the true length in this case, so treat as unbounded. + bytesRemaining = C.LENGTH_UNBOUNDED; + } + } + } catch (IOException e) { + throw new AssetDataSourceException(e); + } + + opened = true; + if (listener != null) { + listener.onTransferStart(); + } + return bytesRemaining; + } + + @Override + public int read(byte[] buffer, int offset, int readLength) throws AssetDataSourceException { + if (bytesRemaining == 0) { + return -1; + } else { + int bytesRead = 0; + try { + int bytesToRead = bytesRemaining == C.LENGTH_UNBOUNDED ? readLength + : (int) Math.min(bytesRemaining, readLength); + bytesRead = inputStream.read(buffer, offset, bytesToRead); + } catch (IOException e) { + throw new AssetDataSourceException(e); + } + + if (bytesRead > 0) { + if (bytesRemaining != C.LENGTH_UNBOUNDED) { + bytesRemaining -= bytesRead; + } + if (listener != null) { + listener.onBytesTransferred(bytesRead); + } + } + + return bytesRead; + } + } + + @Override + public String getUri() { + return uriString; + } + + @Override + public void close() throws AssetDataSourceException { + uriString = null; + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + throw new AssetDataSourceException(e); + } finally { + inputStream = null; + if (opened) { + opened = false; + if (listener != null) { + listener.onTransferEnd(); + } + } + } + } + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/BandwidthMeter.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/BandwidthMeter.java new file mode 100755 index 000000000..81d0b817e --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/BandwidthMeter.java @@ -0,0 +1,52 @@ +/* + * 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.messenger.exoplayer.upstream; + +/** + * Provides estimates of the currently available bandwidth. + */ +public interface BandwidthMeter extends TransferListener { + + /** + * Interface definition for a callback to be notified of {@link BandwidthMeter} events. + */ + public interface EventListener { + + /** + * Invoked periodically to indicate that bytes have been transferred. + * + * @param elapsedMs The time taken to transfer the bytes, in milliseconds. + * @param bytes The number of bytes transferred. + * @param bitrate The estimated bitrate in bits/sec, or {@link #NO_ESTIMATE} if no estimate + * is available. Note that this estimate is typically derived from more information than + * {@code bytes} and {@code elapsedMs}. + */ + void onBandwidthSample(int elapsedMs, long bytes, long bitrate); + } + + /** + * Indicates no bandwidth estimate is available. + */ + final long NO_ESTIMATE = -1; + + /** + * Gets the estimated bandwidth, in bits/sec. + * + * @return Estimated bandwidth in bits/sec, or {@link #NO_ESTIMATE} if no estimate is available. + */ + long getBitrateEstimate(); + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/ByteArrayDataSink.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/ByteArrayDataSink.java new file mode 100755 index 000000000..be8f9e9f0 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/ByteArrayDataSink.java @@ -0,0 +1,61 @@ +/* + * 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.messenger.exoplayer.upstream; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.util.Assertions; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +/** + * A {@link DataSink} for writing to a byte array. + */ +public final class ByteArrayDataSink implements DataSink { + + private ByteArrayOutputStream stream; + + @Override + public DataSink open(DataSpec dataSpec) throws IOException { + if (dataSpec.length == C.LENGTH_UNBOUNDED) { + stream = new ByteArrayOutputStream(); + } else { + Assertions.checkArgument(dataSpec.length <= Integer.MAX_VALUE); + stream = new ByteArrayOutputStream((int) dataSpec.length); + } + return this; + } + + @Override + public void close() throws IOException { + stream.close(); + } + + @Override + public void write(byte[] buffer, int offset, int length) throws IOException { + stream.write(buffer, offset, length); + } + + /** + * Returns the data written to the sink since the last call to {@link #open(DataSpec)}. + * + * @return The data, or null if {@link #open(DataSpec)} has never been called. + */ + public byte[] getData() { + return stream == null ? null : stream.toByteArray(); + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/ByteArrayDataSource.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/ByteArrayDataSource.java new file mode 100755 index 000000000..3d5cdb63e --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/ByteArrayDataSource.java @@ -0,0 +1,70 @@ +/* + * 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.messenger.exoplayer.upstream; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.util.Assertions; + +import java.io.IOException; + +/** + * A {@link DataSource} for reading from a byte array. + */ +public final class ByteArrayDataSource implements DataSource { + + private final byte[] data; + private int readPosition; + private int remainingBytes; + + /** + * @param data The data to be read. + */ + public ByteArrayDataSource(byte[] data) { + Assertions.checkNotNull(data); + Assertions.checkArgument(data.length > 0); + this.data = data; + } + + @Override + public long open(DataSpec dataSpec) throws IOException { + readPosition = (int) dataSpec.position; + remainingBytes = (int) ((dataSpec.length == C.LENGTH_UNBOUNDED) + ? (data.length - dataSpec.position) : dataSpec.length); + if (remainingBytes <= 0 || readPosition + remainingBytes > data.length) { + throw new IOException("Unsatisfiable range: [" + readPosition + ", " + dataSpec.length + + "], length: " + data.length); + } + return remainingBytes; + } + + @Override + public void close() throws IOException { + // Do nothing. + } + + @Override + public int read(byte[] buffer, int offset, int length) throws IOException { + if (remainingBytes == 0) { + return -1; + } + length = Math.min(length, remainingBytes); + System.arraycopy(data, readPosition, buffer, offset, length); + readPosition += length; + remainingBytes -= length; + return length; + } +} + diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/ContentDataSource.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/ContentDataSource.java new file mode 100755 index 000000000..ff19c8ced --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/ContentDataSource.java @@ -0,0 +1,157 @@ +/* + * 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.messenger.exoplayer.upstream; + +import org.telegram.messenger.exoplayer.C; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.res.AssetFileDescriptor; + +import java.io.EOFException; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * A content URI {@link UriDataSource}. + */ +public final class ContentDataSource implements UriDataSource { + + /** + * Thrown when an {@link IOException} is encountered reading from a content URI. + */ + public static class ContentDataSourceException extends IOException { + + public ContentDataSourceException(IOException cause) { + super(cause); + } + + } + + private final ContentResolver resolver; + private final TransferListener listener; + + private InputStream inputStream; + private String uriString; + private long bytesRemaining; + private boolean opened; + + /** + * Constructs a new {@link DataSource} that retrieves data from a content provider. + */ + public ContentDataSource(Context context) { + this(context, null); + } + + /** + * Constructs a new {@link DataSource} that retrieves data from a content provider. + * + * @param listener An optional listener. Specify {@code null} for no listener. + */ + public ContentDataSource(Context context, TransferListener listener) { + this.resolver = context.getContentResolver(); + this.listener = listener; + } + + @Override + public long open(DataSpec dataSpec) throws ContentDataSourceException { + try { + uriString = dataSpec.uri.toString(); + AssetFileDescriptor assetFd = resolver.openAssetFileDescriptor(dataSpec.uri, "r"); + inputStream = new FileInputStream(assetFd.getFileDescriptor()); + long skipped = inputStream.skip(dataSpec.position); + if (skipped < dataSpec.position) { + // We expect the skip to be satisfied in full. If it isn't then we're probably trying to + // skip beyond the end of the data. + throw new EOFException(); + } + if (dataSpec.length != C.LENGTH_UNBOUNDED) { + bytesRemaining = dataSpec.length; + } else { + bytesRemaining = inputStream.available(); + if (bytesRemaining == 0) { + // FileInputStream.available() returns 0 if the remaining length cannot be determined, or + // if it's greater than Integer.MAX_VALUE. We don't know the true length in either case, + // so treat as unbounded. + bytesRemaining = C.LENGTH_UNBOUNDED; + } + } + } catch (IOException e) { + throw new ContentDataSourceException(e); + } + + opened = true; + if (listener != null) { + listener.onTransferStart(); + } + + return bytesRemaining; + } + + @Override + public int read(byte[] buffer, int offset, int readLength) throws ContentDataSourceException { + if (bytesRemaining == 0) { + return -1; + } else { + int bytesRead = 0; + try { + int bytesToRead = bytesRemaining == C.LENGTH_UNBOUNDED ? readLength + : (int) Math.min(bytesRemaining, readLength); + bytesRead = inputStream.read(buffer, offset, bytesToRead); + } catch (IOException e) { + throw new ContentDataSourceException(e); + } + + if (bytesRead > 0) { + if (bytesRemaining != C.LENGTH_UNBOUNDED) { + bytesRemaining -= bytesRead; + } + if (listener != null) { + listener.onBytesTransferred(bytesRead); + } + } + + return bytesRead; + } + } + + @Override + public String getUri() { + return uriString; + } + + @Override + public void close() throws ContentDataSourceException { + uriString = null; + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + throw new ContentDataSourceException(e); + } finally { + inputStream = null; + if (opened) { + opened = false; + if (listener != null) { + listener.onTransferEnd(); + } + } + } + } + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/DataSink.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/DataSink.java new file mode 100755 index 000000000..ed5a5762d --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/DataSink.java @@ -0,0 +1,52 @@ +/* + * 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.messenger.exoplayer.upstream; + +import java.io.IOException; + +/** + * A component that consumes media data. + */ +public interface DataSink { + + /** + * Opens the {@link DataSink} to consume the specified data. Calls to {@link #open(DataSpec)} and + * {@link #close()} must be balanced. + * + * @param dataSpec Defines the data to be consumed. + * @return This {@link DataSink}, for convenience. + * @throws IOException + */ + public DataSink open(DataSpec dataSpec) throws IOException; + + /** + * Closes the {@link DataSink}. + * + * @throws IOException + */ + public void close() throws IOException; + + /** + * Consumes the provided data. + * + * @param buffer The buffer from which data should be consumed. + * @param offset The offset of the data to consume in {@code buffer}. + * @param length The length of the data to consume, in bytes. + * @throws IOException + */ + public void write(byte[] buffer, int offset, int length) throws IOException; + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/DataSource.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/DataSource.java new file mode 100755 index 000000000..a69857612 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/DataSource.java @@ -0,0 +1,72 @@ +/* + * 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.messenger.exoplayer.upstream; + +import org.telegram.messenger.exoplayer.C; + +import java.io.IOException; + +/** + * A component that provides media data. + */ +public interface DataSource { + + /** + * Opens the {@link DataSource} to read the specified data. Calls to {@link #open(DataSpec)} and + * {@link #close()} must be balanced. + *

+ * Note: If {@link #open(DataSpec)} throws an {@link IOException}, callers must still call + * {@link #close()} to ensure that any partial effects of the {@link #open(DataSpec)} invocation + * are cleaned up. Implementations of this class can assume that callers will call + * {@link #close()} in this case. + * + * @param dataSpec Defines the data to be read. + * @throws IOException If an error occurs opening the source. + * @return The number of bytes that can be read from the opened source. For unbounded requests + * (i.e. requests where {@link DataSpec#length} equals {@link C#LENGTH_UNBOUNDED}) this value + * is the resolved length of the request, or {@link C#LENGTH_UNBOUNDED} if the length is still + * unresolved. For all other requests, the value returned will be equal to the request's + * {@link DataSpec#length}. + */ + long open(DataSpec dataSpec) throws IOException; + + /** + * Closes the {@link DataSource}. + *

+ * Note: This method will be called even if the corresponding call to {@link #open(DataSpec)} + * threw an {@link IOException}. See {@link #open(DataSpec)} for more details. + * + * @throws IOException If an error occurs closing the source. + */ + void close() throws IOException; + + /** + * Reads up to {@code length} bytes of data and stores them into {@code buffer}, starting at + * index {@code offset}. + *

+ * This method blocks until at least one byte of data can be read, the end of the opened range is + * detected, or an exception is thrown. + * + * @param buffer The buffer into which the read data should be stored. + * @param offset The start offset into {@code buffer} at which data should be written. + * @param readLength The maximum number of bytes to read. + * @return The number of bytes read, or {@link C#RESULT_END_OF_INPUT} if the end of the opened + * range is reached. + * @throws IOException If an error occurs reading from the source. + */ + int read(byte[] buffer, int offset, int readLength) throws IOException; + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/DataSourceInputStream.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/DataSourceInputStream.java new file mode 100755 index 000000000..620dc5ba0 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/DataSourceInputStream.java @@ -0,0 +1,102 @@ +/* + * 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.messenger.exoplayer.upstream; + +import org.telegram.messenger.exoplayer.util.Assertions; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Allows data corresponding to a given {@link DataSpec} to be read from a {@link DataSource} and + * consumed as an {@link InputStream}. + */ +public final class DataSourceInputStream extends InputStream { + + private final DataSource dataSource; + private final DataSpec dataSpec; + private final byte[] singleByteArray; + + private boolean opened = false; + private boolean closed = false; + + /** + * @param dataSource The {@link DataSource} from which the data should be read. + * @param dataSpec The {@link DataSpec} defining the data to be read from {@code dataSource}. + */ + public DataSourceInputStream(DataSource dataSource, DataSpec dataSpec) { + this.dataSource = dataSource; + this.dataSpec = dataSpec; + singleByteArray = new byte[1]; + } + + /** + * Optional call to open the underlying {@link DataSource}. + *

+ * Calling this method does nothing if the {@link DataSource} is already open. Calling this + * method is optional, since the read and skip methods will automatically open the underlying + * {@link DataSource} if it's not open already. + * + * @throws IOException If an error occurs opening the {@link DataSource}. + */ + public void open() throws IOException { + checkOpened(); + } + + @Override + public int read() throws IOException { + int length = read(singleByteArray); + if (length == -1) { + return -1; + } + return singleByteArray[0] & 0xFF; + } + + @Override + public int read(byte[] buffer) throws IOException { + return read(buffer, 0, buffer.length); + } + + @Override + public int read(byte[] buffer, int offset, int length) throws IOException { + Assertions.checkState(!closed); + checkOpened(); + return dataSource.read(buffer, offset, length); + } + + @Override + public long skip(long byteCount) throws IOException { + Assertions.checkState(!closed); + checkOpened(); + return super.skip(byteCount); + } + + @Override + public void close() throws IOException { + if (!closed) { + dataSource.close(); + closed = true; + } + } + + private void checkOpened() throws IOException { + if (!opened) { + dataSource.open(dataSpec); + opened = true; + } + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/DataSpec.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/DataSpec.java new file mode 100755 index 000000000..fce9ecb22 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/DataSpec.java @@ -0,0 +1,168 @@ +/* + * 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.messenger.exoplayer.upstream; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.util.Assertions; + +import android.net.Uri; + +import java.util.Arrays; + +/** + * Defines a region of media data. + */ +public final class DataSpec { + + /** + * Permits an underlying network stack to request that the server use gzip compression. + *

+ * Should not typically be set if the data being requested is already compressed (e.g. most audio + * and video requests). May be set when requesting other data. + *

+ * When a {@link DataSource} is used to request data with this flag set, and if the + * {@link DataSource} does make a network request, then the value returned from + * {@link DataSource#open(DataSpec)} will typically be {@link C#LENGTH_UNBOUNDED}. The data read + * from {@link DataSource#read(byte[], int, int)} will be the decompressed data. + */ + public static final int FLAG_ALLOW_GZIP = 1; + + /** + * Identifies the source from which data should be read. + */ + public final Uri uri; + /** + * Body for a POST request, null otherwise. + */ + public final byte[] postBody; + /** + * The absolute position of the data in the full stream. + */ + public final long absoluteStreamPosition; + /** + * The position of the data when read from {@link #uri}. + *

+ * Always equal to {@link #absoluteStreamPosition} unless the {@link #uri} defines the location + * of a subset of the underyling data. + */ + public final long position; + /** + * The length of the data. Greater than zero, or equal to {@link C#LENGTH_UNBOUNDED}. + */ + public final long length; + /** + * A key that uniquely identifies the original stream. Used for cache indexing. May be null if the + * {@link DataSpec} is not intended to be used in conjunction with a cache. + */ + public final String key; + /** + * Request flags. Currently {@link #FLAG_ALLOW_GZIP} is the only supported flag. + */ + public final int flags; + + /** + * Construct a {@link DataSpec} for the given uri and with {@link #key} set to null. + * + * @param uri {@link #uri}. + */ + public DataSpec(Uri uri) { + this(uri, 0); + } + + /** + * Construct a {@link DataSpec} for the given uri and with {@link #key} set to null. + * + * @param uri {@link #uri}. + * @param flags {@link #flags}. + */ + public DataSpec(Uri uri, int flags) { + this(uri, 0, C.LENGTH_UNBOUNDED, null, flags); + } + + /** + * Construct a {@link DataSpec} where {@link #position} equals {@link #absoluteStreamPosition}. + * + * @param uri {@link #uri}. + * @param absoluteStreamPosition {@link #absoluteStreamPosition}, equal to {@link #position}. + * @param length {@link #length}. + * @param key {@link #key}. + */ + public DataSpec(Uri uri, long absoluteStreamPosition, long length, String key) { + this(uri, absoluteStreamPosition, absoluteStreamPosition, length, key, 0); + } + + /** + * Construct a {@link DataSpec} where {@link #position} equals {@link #absoluteStreamPosition}. + * + * @param uri {@link #uri}. + * @param absoluteStreamPosition {@link #absoluteStreamPosition}, equal to {@link #position}. + * @param length {@link #length}. + * @param key {@link #key}. + * @param flags {@link #flags}. + */ + public DataSpec(Uri uri, long absoluteStreamPosition, long length, String key, int flags) { + this(uri, absoluteStreamPosition, absoluteStreamPosition, length, key, flags); + } + + /** + * Construct a {@link DataSpec} where {@link #position} may differ from + * {@link #absoluteStreamPosition}. + * + * @param uri {@link #uri}. + * @param absoluteStreamPosition {@link #absoluteStreamPosition}. + * @param position {@link #position}. + * @param length {@link #length}. + * @param key {@link #key}. + * @param flags {@link #flags}. + */ + public DataSpec(Uri uri, long absoluteStreamPosition, long position, long length, String key, + int flags) { + this(uri, null, absoluteStreamPosition, position, length, key, flags); + } + + /** + * Construct a {@link DataSpec} where {@link #position} may differ from + * {@link #absoluteStreamPosition}. + * + * @param uri {@link #uri}. + * @param postBody {@link #postBody}. + * @param absoluteStreamPosition {@link #absoluteStreamPosition}. + * @param position {@link #position}. + * @param length {@link #length}. + * @param key {@link #key}. + * @param flags {@link #flags}. + */ + public DataSpec(Uri uri, byte[] postBody, long absoluteStreamPosition, long position, long length, + String key, int flags) { + Assertions.checkArgument(absoluteStreamPosition >= 0); + Assertions.checkArgument(position >= 0); + Assertions.checkArgument(length > 0 || length == C.LENGTH_UNBOUNDED); + this.uri = uri; + this.postBody = postBody; + this.absoluteStreamPosition = absoluteStreamPosition; + this.position = position; + this.length = length; + this.key = key; + this.flags = flags; + } + + @Override + public String toString() { + return "DataSpec[" + uri + ", " + Arrays.toString(postBody) + ", " + absoluteStreamPosition + + ", " + position + ", " + length + ", " + key + ", " + flags + "]"; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/DefaultAllocator.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/DefaultAllocator.java new file mode 100755 index 000000000..6fe69ab7f --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/DefaultAllocator.java @@ -0,0 +1,158 @@ +/* + * 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.messenger.exoplayer.upstream; + +import org.telegram.messenger.exoplayer.util.Assertions; +import org.telegram.messenger.exoplayer.util.Util; + +import java.util.Arrays; + +/** + * Default implementation of {@link Allocator}. + */ +public final class DefaultAllocator implements Allocator { + + private static final int AVAILABLE_EXTRA_CAPACITY = 100; + + private final int individualAllocationSize; + private final byte[] initialAllocationBlock; + + private int allocatedCount; + private int availableCount; + private Allocation[] availableAllocations; + + /** + * Constructs an initially empty pool. + * + * @param individualAllocationSize The length of each individual allocation. + */ + public DefaultAllocator(int individualAllocationSize) { + this(individualAllocationSize, 0); + } + + /** + * Constructs a pool with some {@link Allocation}s created up front. + *

+ * Note: Initial {@link Allocation}s will never be discarded by {@link #trim(int)}. + * + * @param individualAllocationSize The length of each individual allocation. + * @param initialAllocationCount The number of allocations to create up front. + */ + public DefaultAllocator(int individualAllocationSize, int initialAllocationCount) { + Assertions.checkArgument(individualAllocationSize > 0); + Assertions.checkArgument(initialAllocationCount >= 0); + this.individualAllocationSize = individualAllocationSize; + this.availableCount = initialAllocationCount; + this.availableAllocations = new Allocation[initialAllocationCount + AVAILABLE_EXTRA_CAPACITY]; + if (initialAllocationCount > 0) { + initialAllocationBlock = new byte[initialAllocationCount * individualAllocationSize]; + for (int i = 0; i < initialAllocationCount; i++) { + int allocationOffset = i * individualAllocationSize; + availableAllocations[i] = new Allocation(initialAllocationBlock, allocationOffset); + } + } else { + initialAllocationBlock = null; + } + } + + @Override + public synchronized Allocation allocate() { + allocatedCount++; + Allocation allocation; + if (availableCount > 0) { + allocation = availableAllocations[--availableCount]; + availableAllocations[availableCount] = null; + } else { + allocation = new Allocation(new byte[individualAllocationSize], 0); + } + return allocation; + } + + @Override + public synchronized void release(Allocation allocation) { + // Weak sanity check that the allocation probably originated from this pool. + Assertions.checkArgument(allocation.data == initialAllocationBlock + || allocation.data.length == individualAllocationSize); + allocatedCount--; + if (availableCount == availableAllocations.length) { + availableAllocations = Arrays.copyOf(availableAllocations, availableAllocations.length * 2); + } + availableAllocations[availableCount++] = allocation; + // Wake up threads waiting for the allocated size to drop. + notifyAll(); + } + + @Override + public synchronized void trim(int targetSize) { + int targetAllocationCount = Util.ceilDivide(targetSize, individualAllocationSize); + int targetAvailableCount = Math.max(0, targetAllocationCount - allocatedCount); + if (targetAvailableCount >= availableCount) { + // We're already at or below the target. + return; + } + + if (initialAllocationBlock != null) { + // Some allocations are backed by an initial block. We need to make sure that we hold onto all + // such allocations. Re-order the available allocations so that the ones backed by the initial + // block come first. + int lowIndex = 0; + int highIndex = availableCount - 1; + while (lowIndex <= highIndex) { + Allocation lowAllocation = availableAllocations[lowIndex]; + if (lowAllocation.data == initialAllocationBlock) { + lowIndex++; + } else { + Allocation highAllocation = availableAllocations[lowIndex]; + if (highAllocation.data != initialAllocationBlock) { + highIndex--; + } else { + availableAllocations[lowIndex++] = highAllocation; + availableAllocations[highIndex--] = lowAllocation; + } + } + } + // lowIndex is the index of the first allocation not backed by an initial block. + targetAvailableCount = Math.max(targetAvailableCount, lowIndex); + if (targetAvailableCount >= availableCount) { + // We're already at or below the target. + return; + } + } + + // Discard allocations beyond the target. + Arrays.fill(availableAllocations, targetAvailableCount, availableCount, null); + availableCount = targetAvailableCount; + } + + @Override + public synchronized int getTotalBytesAllocated() { + return allocatedCount * individualAllocationSize; + } + + @Override + public synchronized void blockWhileTotalBytesAllocatedExceeds(int limit) + throws InterruptedException { + while (getTotalBytesAllocated() > limit) { + wait(); + } + } + + @Override + public int getIndividualAllocationLength() { + return individualAllocationSize; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/DefaultBandwidthMeter.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/DefaultBandwidthMeter.java new file mode 100755 index 000000000..73a8dfeed --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/DefaultBandwidthMeter.java @@ -0,0 +1,117 @@ +/* + * 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.messenger.exoplayer.upstream; + +import org.telegram.messenger.exoplayer.util.Assertions; +import org.telegram.messenger.exoplayer.util.Clock; +import org.telegram.messenger.exoplayer.util.SlidingPercentile; +import org.telegram.messenger.exoplayer.util.SystemClock; + +import android.os.Handler; + +/** + * Counts transferred bytes while transfers are open and creates a bandwidth sample and updated + * bandwidth estimate each time a transfer ends. + */ +public final class DefaultBandwidthMeter implements BandwidthMeter { + + public static final int DEFAULT_MAX_WEIGHT = 2000; + + private final Handler eventHandler; + private final EventListener eventListener; + private final Clock clock; + private final SlidingPercentile slidingPercentile; + + private long bytesAccumulator; + private long startTimeMs; + private long bitrateEstimate; + private int streamCount; + + public DefaultBandwidthMeter() { + this(null, null); + } + + public DefaultBandwidthMeter(Handler eventHandler, EventListener eventListener) { + this(eventHandler, eventListener, new SystemClock()); + } + + public DefaultBandwidthMeter(Handler eventHandler, EventListener eventListener, Clock clock) { + this(eventHandler, eventListener, clock, DEFAULT_MAX_WEIGHT); + } + + public DefaultBandwidthMeter(Handler eventHandler, EventListener eventListener, int maxWeight) { + this(eventHandler, eventListener, new SystemClock(), maxWeight); + } + + public DefaultBandwidthMeter(Handler eventHandler, EventListener eventListener, Clock clock, + int maxWeight) { + this.eventHandler = eventHandler; + this.eventListener = eventListener; + this.clock = clock; + this.slidingPercentile = new SlidingPercentile(maxWeight); + bitrateEstimate = NO_ESTIMATE; + } + + @Override + public synchronized long getBitrateEstimate() { + return bitrateEstimate; + } + + @Override + public synchronized void onTransferStart() { + if (streamCount == 0) { + startTimeMs = clock.elapsedRealtime(); + } + streamCount++; + } + + @Override + public synchronized void onBytesTransferred(int bytes) { + bytesAccumulator += bytes; + } + + @Override + public synchronized void onTransferEnd() { + Assertions.checkState(streamCount > 0); + long nowMs = clock.elapsedRealtime(); + int elapsedMs = (int) (nowMs - startTimeMs); + if (elapsedMs > 0) { + float bitsPerSecond = (bytesAccumulator * 8000) / elapsedMs; + slidingPercentile.addSample((int) Math.sqrt(bytesAccumulator), bitsPerSecond); + float bandwidthEstimateFloat = slidingPercentile.getPercentile(0.5f); + bitrateEstimate = Float.isNaN(bandwidthEstimateFloat) ? NO_ESTIMATE + : (long) bandwidthEstimateFloat; + notifyBandwidthSample(elapsedMs, bytesAccumulator, bitrateEstimate); + } + streamCount--; + if (streamCount > 0) { + startTimeMs = nowMs; + } + bytesAccumulator = 0; + } + + private void notifyBandwidthSample(final int elapsedMs, final long bytes, final long bitrate) { + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onBandwidthSample(elapsedMs, bytes, bitrate); + } + }); + } + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/DefaultHttpDataSource.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/DefaultHttpDataSource.java new file mode 100755 index 000000000..5898f6a08 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/DefaultHttpDataSource.java @@ -0,0 +1,582 @@ +/* + * 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.messenger.exoplayer.upstream; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.util.Assertions; +import org.telegram.messenger.exoplayer.util.Predicate; +import org.telegram.messenger.exoplayer.util.Util; + +import android.text.TextUtils; +import android.util.Log; + +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InterruptedIOException; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.NoRouteToHostException; +import java.net.ProtocolException; +import java.net.URL; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A {@link HttpDataSource} that uses Android's {@link HttpURLConnection}. + *

+ * By default this implementation will not follow cross-protocol redirects (i.e. redirects from + * HTTP to HTTPS or vice versa). Cross-protocol redirects can be enabled by using the + * {@link #DefaultHttpDataSource(String, Predicate, TransferListener, int, int, boolean)} + * constructor and passing {@code true} as the final argument. + */ +public class DefaultHttpDataSource implements HttpDataSource { + + /** + * The default connection timeout, in milliseconds. + */ + public static final int DEFAULT_CONNECT_TIMEOUT_MILLIS = 8 * 1000; + /** + * The default read timeout, in milliseconds. + */ + public static final int DEFAULT_READ_TIMEOUT_MILLIS = 8 * 1000; + + private static final int MAX_REDIRECTS = 20; // Same limit as okhttp. + private static final String TAG = "DefaultHttpDataSource"; + private static final Pattern CONTENT_RANGE_HEADER = + Pattern.compile("^bytes (\\d+)-(\\d+)/(\\d+)$"); + private static final AtomicReference skipBufferReference = new AtomicReference<>(); + + private final boolean allowCrossProtocolRedirects; + private final int connectTimeoutMillis; + private final int readTimeoutMillis; + private final String userAgent; + private final Predicate contentTypePredicate; + private final HashMap requestProperties; + private final TransferListener listener; + + private DataSpec dataSpec; + private HttpURLConnection connection; + private InputStream inputStream; + private boolean opened; + + private long bytesToSkip; + private long bytesToRead; + + private long bytesSkipped; + private long bytesRead; + + /** + * @param userAgent The User-Agent string that should be used. + * @param contentTypePredicate An optional {@link Predicate}. If a content type is + * rejected by the predicate then a {@link HttpDataSource.InvalidContentTypeException} is + * thrown from {@link #open(DataSpec)}. + */ + public DefaultHttpDataSource(String userAgent, Predicate contentTypePredicate) { + this(userAgent, contentTypePredicate, null); + } + + /** + * @param userAgent The User-Agent string that should be used. + * @param contentTypePredicate An optional {@link Predicate}. If a content type is + * rejected by the predicate then a {@link HttpDataSource.InvalidContentTypeException} is + * thrown from {@link #open(DataSpec)}. + * @param listener An optional listener. + */ + public DefaultHttpDataSource(String userAgent, Predicate contentTypePredicate, + TransferListener listener) { + this(userAgent, contentTypePredicate, listener, DEFAULT_CONNECT_TIMEOUT_MILLIS, + DEFAULT_READ_TIMEOUT_MILLIS); + } + + /** + * @param userAgent The User-Agent string that should be used. + * @param contentTypePredicate An optional {@link Predicate}. If a content type is + * rejected by the predicate then a {@link HttpDataSource.InvalidContentTypeException} is + * thrown from {@link #open(DataSpec)}. + * @param listener An optional listener. + * @param connectTimeoutMillis The connection timeout, in milliseconds. A timeout of zero is + * interpreted as an infinite timeout. + * @param readTimeoutMillis The read timeout, in milliseconds. A timeout of zero is interpreted + * as an infinite timeout. + */ + public DefaultHttpDataSource(String userAgent, Predicate contentTypePredicate, + TransferListener listener, int connectTimeoutMillis, int readTimeoutMillis) { + this(userAgent, contentTypePredicate, listener, connectTimeoutMillis, readTimeoutMillis, false); + } + + /** + * @param userAgent The User-Agent string that should be used. + * @param contentTypePredicate An optional {@link Predicate}. If a content type is + * rejected by the predicate then a {@link HttpDataSource.InvalidContentTypeException} is + * thrown from {@link #open(DataSpec)}. + * @param listener An optional listener. + * @param connectTimeoutMillis The connection timeout, in milliseconds. A timeout of zero is + * interpreted as an infinite timeout. Pass {@link #DEFAULT_CONNECT_TIMEOUT_MILLIS} to use + * the default value. + * @param readTimeoutMillis The read timeout, in milliseconds. A timeout of zero is interpreted + * as an infinite timeout. Pass {@link #DEFAULT_READ_TIMEOUT_MILLIS} to use the default value. + * @param allowCrossProtocolRedirects Whether cross-protocol redirects (i.e. redirects from HTTP + * to HTTPS and vice versa) are enabled. + */ + public DefaultHttpDataSource(String userAgent, Predicate contentTypePredicate, + TransferListener listener, int connectTimeoutMillis, int readTimeoutMillis, + boolean allowCrossProtocolRedirects) { + this.userAgent = Assertions.checkNotEmpty(userAgent); + this.contentTypePredicate = contentTypePredicate; + this.listener = listener; + this.requestProperties = new HashMap<>(); + this.connectTimeoutMillis = connectTimeoutMillis; + this.readTimeoutMillis = readTimeoutMillis; + this.allowCrossProtocolRedirects = allowCrossProtocolRedirects; + } + + @Override + public String getUri() { + return connection == null ? null : connection.getURL().toString(); + } + + @Override + public Map> getResponseHeaders() { + return connection == null ? null : connection.getHeaderFields(); + } + + @Override + public void setRequestProperty(String name, String value) { + Assertions.checkNotNull(name); + Assertions.checkNotNull(value); + synchronized (requestProperties) { + requestProperties.put(name, value); + } + } + + @Override + public void clearRequestProperty(String name) { + Assertions.checkNotNull(name); + synchronized (requestProperties) { + requestProperties.remove(name); + } + } + + @Override + public void clearAllRequestProperties() { + synchronized (requestProperties) { + requestProperties.clear(); + } + } + + @Override + public long open(DataSpec dataSpec) throws HttpDataSourceException { + this.dataSpec = dataSpec; + this.bytesRead = 0; + this.bytesSkipped = 0; + try { + connection = makeConnection(dataSpec); + } catch (IOException e) { + throw new HttpDataSourceException("Unable to connect to " + dataSpec.uri.toString(), e, + dataSpec, HttpDataSourceException.TYPE_OPEN); + } + + int responseCode; + try { + responseCode = connection.getResponseCode(); + } catch (IOException e) { + closeConnectionQuietly(); + throw new HttpDataSourceException("Unable to connect to " + dataSpec.uri.toString(), e, + dataSpec, HttpDataSourceException.TYPE_OPEN); + } + + // Check for a valid response code. + if (responseCode < 200 || responseCode > 299) { + Map> headers = connection.getHeaderFields(); + closeConnectionQuietly(); + throw new InvalidResponseCodeException(responseCode, headers, dataSpec); + } + + // Check for a valid content type. + String contentType = connection.getContentType(); + if (contentTypePredicate != null && !contentTypePredicate.evaluate(contentType)) { + closeConnectionQuietly(); + throw new InvalidContentTypeException(contentType, dataSpec); + } + + // If we requested a range starting from a non-zero position and received a 200 rather than a + // 206, then the server does not support partial requests. We'll need to manually skip to the + // requested position. + bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0; + + // Determine the length of the data to be read, after skipping. + if ((dataSpec.flags & DataSpec.FLAG_ALLOW_GZIP) == 0) { + long contentLength = getContentLength(connection); + bytesToRead = dataSpec.length != C.LENGTH_UNBOUNDED ? dataSpec.length + : contentLength != C.LENGTH_UNBOUNDED ? contentLength - bytesToSkip + : C.LENGTH_UNBOUNDED; + } else { + // Gzip is enabled. If the server opts to use gzip then the content length in the response + // will be that of the compressed data, which isn't what we want. Furthermore, there isn't a + // reliable way to determine whether the gzip was used or not. Always use the dataSpec length + // in this case. + bytesToRead = dataSpec.length; + } + + try { + inputStream = connection.getInputStream(); + } catch (IOException e) { + closeConnectionQuietly(); + throw new HttpDataSourceException(e, dataSpec, HttpDataSourceException.TYPE_OPEN); + } + + opened = true; + if (listener != null) { + listener.onTransferStart(); + } + + return bytesToRead; + } + + @Override + public int read(byte[] buffer, int offset, int readLength) throws HttpDataSourceException { + try { + skipInternal(); + return readInternal(buffer, offset, readLength); + } catch (IOException e) { + throw new HttpDataSourceException(e, dataSpec, HttpDataSourceException.TYPE_READ); + } + } + + @Override + public void close() throws HttpDataSourceException { + try { + if (inputStream != null) { + Util.maybeTerminateInputStream(connection, bytesRemaining()); + try { + inputStream.close(); + } catch (IOException e) { + throw new HttpDataSourceException(e, dataSpec, HttpDataSourceException.TYPE_CLOSE); + } + } + } finally { + inputStream = null; + closeConnectionQuietly(); + if (opened) { + opened = false; + if (listener != null) { + listener.onTransferEnd(); + } + } + } + } + + /** + * Returns the current connection, or null if the source is not currently opened. + * + * @return The current open connection, or null. + */ + protected final HttpURLConnection getConnection() { + return connection; + } + + /** + * Returns the number of bytes that have been skipped since the most recent call to + * {@link #open(DataSpec)}. + * + * @return The number of bytes skipped. + */ + protected final long bytesSkipped() { + return bytesSkipped; + } + + /** + * Returns the number of bytes that have been read since the most recent call to + * {@link #open(DataSpec)}. + * + * @return The number of bytes read. + */ + protected final long bytesRead() { + return bytesRead; + } + + /** + * Returns the number of bytes that are still to be read for the current {@link DataSpec}. + *

+ * If the total length of the data being read is known, then this length minus {@code bytesRead()} + * is returned. If the total length is unknown, {@link C#LENGTH_UNBOUNDED} is returned. + * + * @return The remaining length, or {@link C#LENGTH_UNBOUNDED}. + */ + protected final long bytesRemaining() { + return bytesToRead == C.LENGTH_UNBOUNDED ? bytesToRead : bytesToRead - bytesRead; + } + + /** + * Establishes a connection, following redirects to do so where permitted. + */ + private HttpURLConnection makeConnection(DataSpec dataSpec) throws IOException { + URL url = new URL(dataSpec.uri.toString()); + byte[] postBody = dataSpec.postBody; + long position = dataSpec.position; + long length = dataSpec.length; + boolean allowGzip = (dataSpec.flags & DataSpec.FLAG_ALLOW_GZIP) != 0; + + if (!allowCrossProtocolRedirects) { + // HttpURLConnection disallows cross-protocol redirects, but otherwise performs redirection + // automatically. This is the behavior we want, so use it. + HttpURLConnection connection = makeConnection( + url, postBody, position, length, allowGzip, true /* followRedirects */); + return connection; + } + + // We need to handle redirects ourselves to allow cross-protocol redirects. + int redirectCount = 0; + while (redirectCount++ <= MAX_REDIRECTS) { + HttpURLConnection connection = makeConnection( + url, postBody, position, length, allowGzip, false /* followRedirects */); + int responseCode = connection.getResponseCode(); + if (responseCode == HttpURLConnection.HTTP_MULT_CHOICE + || responseCode == HttpURLConnection.HTTP_MOVED_PERM + || responseCode == HttpURLConnection.HTTP_MOVED_TEMP + || responseCode == HttpURLConnection.HTTP_SEE_OTHER + || (postBody == null + && (responseCode == 307 /* HTTP_TEMP_REDIRECT */ + || responseCode == 308 /* HTTP_PERM_REDIRECT */))) { + // For 300, 301, 302, and 303 POST requests follow the redirect and are transformed into + // GET requests. For 307 and 308 POST requests are not redirected. + postBody = null; + String location = connection.getHeaderField("Location"); + connection.disconnect(); + url = handleRedirect(url, location); + } else { + return connection; + } + } + + // If we get here we've been redirected more times than are permitted. + throw new NoRouteToHostException("Too many redirects: " + redirectCount); + } + + /** + * Configures a connection and opens it. + * + * @param url The url to connect to. + * @param postBody The body data for a POST request. + * @param position The byte offset of the requested data. + * @param length The length of the requested data, or {@link C#LENGTH_UNBOUNDED}. + * @param allowGzip Whether to allow the use of gzip. + * @param followRedirects Whether to follow redirects. + */ + private HttpURLConnection makeConnection(URL url, byte[] postBody, long position, + long length, boolean allowGzip, boolean followRedirects) throws IOException { + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setConnectTimeout(connectTimeoutMillis); + connection.setReadTimeout(readTimeoutMillis); + synchronized (requestProperties) { + for (Map.Entry property : requestProperties.entrySet()) { + connection.setRequestProperty(property.getKey(), property.getValue()); + } + } + if (!(position == 0 && length == C.LENGTH_UNBOUNDED)) { + String rangeRequest = "bytes=" + position + "-"; + if (length != C.LENGTH_UNBOUNDED) { + rangeRequest += (position + length - 1); + } + connection.setRequestProperty("Range", rangeRequest); + } + connection.setRequestProperty("User-Agent", userAgent); + if (!allowGzip) { + connection.setRequestProperty("Accept-Encoding", "identity"); + } + connection.setInstanceFollowRedirects(followRedirects); + connection.setDoOutput(postBody != null); + if (postBody != null) { + connection.setFixedLengthStreamingMode(postBody.length); + connection.connect(); + OutputStream os = connection.getOutputStream(); + os.write(postBody); + os.close(); + } else { + connection.connect(); + } + return connection; + } + + /** + * Handles a redirect. + * + * @param originalUrl The original URL. + * @param location The Location header in the response. + * @return The next URL. + * @throws IOException If redirection isn't possible. + */ + private static URL handleRedirect(URL originalUrl, String location) throws IOException { + if (location == null) { + throw new ProtocolException("Null location redirect"); + } + // Form the new url. + URL url = new URL(originalUrl, location); + // Check that the protocol of the new url is supported. + String protocol = url.getProtocol(); + if (!"https".equals(protocol) && !"http".equals(protocol)) { + throw new ProtocolException("Unsupported protocol redirect: " + protocol); + } + // Currently this method is only called if allowCrossProtocolRedirects is true, and so the code + // below isn't required. If we ever decide to handle redirects ourselves when cross-protocol + // redirects are disabled, we'll need to uncomment this block of code. + // if (!allowCrossProtocolRedirects && !protocol.equals(originalUrl.getProtocol())) { + // throw new ProtocolException("Disallowed cross-protocol redirect (" + // + originalUrl.getProtocol() + " to " + protocol + ")"); + // } + return url; + } + + /** + * Attempts to extract the length of the content from the response headers of an open connection. + * + * @param connection The open connection. + * @return The extracted length, or {@link C#LENGTH_UNBOUNDED}. + */ + private static long getContentLength(HttpURLConnection connection) { + long contentLength = C.LENGTH_UNBOUNDED; + String contentLengthHeader = connection.getHeaderField("Content-Length"); + if (!TextUtils.isEmpty(contentLengthHeader)) { + try { + contentLength = Long.parseLong(contentLengthHeader); + } catch (NumberFormatException e) { + Log.e(TAG, "Unexpected Content-Length [" + contentLengthHeader + "]"); + } + } + String contentRangeHeader = connection.getHeaderField("Content-Range"); + if (!TextUtils.isEmpty(contentRangeHeader)) { + Matcher matcher = CONTENT_RANGE_HEADER.matcher(contentRangeHeader); + if (matcher.find()) { + try { + long contentLengthFromRange = + Long.parseLong(matcher.group(2)) - Long.parseLong(matcher.group(1)) + 1; + if (contentLength < 0) { + // Some proxy servers strip the Content-Length header. Fall back to the length + // calculated here in this case. + contentLength = contentLengthFromRange; + } else if (contentLength != contentLengthFromRange) { + // If there is a discrepancy between the Content-Length and Content-Range headers, + // assume the one with the larger value is correct. We have seen cases where carrier + // change one of them to reduce the size of a request, but it is unlikely anybody would + // increase it. + Log.w(TAG, "Inconsistent headers [" + contentLengthHeader + "] [" + contentRangeHeader + + "]"); + contentLength = Math.max(contentLength, contentLengthFromRange); + } + } catch (NumberFormatException e) { + Log.e(TAG, "Unexpected Content-Range [" + contentRangeHeader + "]"); + } + } + } + return contentLength; + } + + /** + * Skips any bytes that need skipping. Else does nothing. + *

+ * This implementation is based roughly on {@code libcore.io.Streams.skipByReading()}. + * + * @throws InterruptedIOException If the thread is interrupted during the operation. + * @throws EOFException If the end of the input stream is reached before the bytes are skipped. + */ + private void skipInternal() throws IOException { + if (bytesSkipped == bytesToSkip) { + return; + } + + // Acquire the shared skip buffer. + byte[] skipBuffer = skipBufferReference.getAndSet(null); + if (skipBuffer == null) { + skipBuffer = new byte[4096]; + } + + while (bytesSkipped != bytesToSkip) { + int readLength = (int) Math.min(bytesToSkip - bytesSkipped, skipBuffer.length); + int read = inputStream.read(skipBuffer, 0, readLength); + if (Thread.interrupted()) { + throw new InterruptedIOException(); + } + if (read == -1) { + throw new EOFException(); + } + bytesSkipped += read; + if (listener != null) { + listener.onBytesTransferred(read); + } + } + + // Release the shared skip buffer. + skipBufferReference.set(skipBuffer); + } + + /** + * Reads up to {@code length} bytes of data and stores them into {@code buffer}, starting at + * index {@code offset}. + *

+ * This method blocks until at least one byte of data can be read, the end of the opened range is + * detected, or an exception is thrown. + * + * @param buffer The buffer into which the read data should be stored. + * @param offset The start offset into {@code buffer} at which data should be written. + * @param readLength The maximum number of bytes to read. + * @return The number of bytes read, or {@link C#RESULT_END_OF_INPUT} if the end of the opened + * range is reached. + * @throws IOException If an error occurs reading from the source. + */ + private int readInternal(byte[] buffer, int offset, int readLength) throws IOException { + readLength = bytesToRead == C.LENGTH_UNBOUNDED ? readLength + : (int) Math.min(readLength, bytesToRead - bytesRead); + if (readLength == 0) { + // We've read all of the requested data. + return C.RESULT_END_OF_INPUT; + } + + int read = inputStream.read(buffer, offset, readLength); + if (read == -1) { + if (bytesToRead != C.LENGTH_UNBOUNDED && bytesToRead != bytesRead) { + // The server closed the connection having not sent sufficient data. + throw new EOFException(); + } + return C.RESULT_END_OF_INPUT; + } + + bytesRead += read; + if (listener != null) { + listener.onBytesTransferred(read); + } + return read; + } + + /** + * Closes the current connection quietly, if there is one. + */ + private void closeConnectionQuietly() { + if (connection != null) { + try { + connection.disconnect(); + } catch (Exception e) { + Log.e(TAG, "Unexpected error while disconnecting", e); + } + connection = null; + } + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/DefaultUriDataSource.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/DefaultUriDataSource.java new file mode 100755 index 000000000..2c93ed98e --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/DefaultUriDataSource.java @@ -0,0 +1,159 @@ +/* + * 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.messenger.exoplayer.upstream; + +import org.telegram.messenger.exoplayer.util.Assertions; +import org.telegram.messenger.exoplayer.util.Util; + +import android.content.Context; + +import java.io.IOException; + +/** + * A {@link UriDataSource} that supports multiple URI schemes. The supported schemes are: + * + *

    + *
  • http(s): For fetching data over HTTP and HTTPS (e.g. https://www.something.com/media.mp4). + *
  • file: For fetching data from a local file (e.g. file:///path/to/media/media.mp4, or just + * /path/to/media/media.mp4 because the implementation assumes that a URI without a scheme is a + * local file URI). + *
  • asset: For fetching data from an asset in the application's apk (e.g. asset:///media.mp4). + *
  • content: For fetching data from a content URI (e.g. content://authority/path/123). + *
+ */ +public final class DefaultUriDataSource implements UriDataSource { + + private static final String SCHEME_ASSET = "asset"; + private static final String SCHEME_CONTENT = "content"; + + private final UriDataSource httpDataSource; + private final UriDataSource fileDataSource; + private final UriDataSource assetDataSource; + private final UriDataSource contentDataSource; + + /** + * {@code null} if no data source is open. Otherwise, equal to {@link #fileDataSource} if the open + * data source is a file, or {@link #httpDataSource} otherwise. + */ + private UriDataSource dataSource; + + /** + * Constructs a new instance. + *

+ * The constructed instance will not follow cross-protocol redirects (i.e. redirects from HTTP to + * HTTPS or vice versa) when fetching remote data. Cross-protocol redirects can be enabled by + * using {@link #DefaultUriDataSource(Context, TransferListener, String, boolean)} and passing + * {@code true} as the final argument. + * + * @param context A context. + * @param userAgent The User-Agent string that should be used when requesting remote data. + */ + public DefaultUriDataSource(Context context, String userAgent) { + this(context, null, userAgent, false); + } + + /** + * Constructs a new instance. + *

+ * The constructed instance will not follow cross-protocol redirects (i.e. redirects from HTTP to + * HTTPS or vice versa) when fetching remote data. Cross-protocol redirects can be enabled by + * using {@link #DefaultUriDataSource(Context, TransferListener, String, boolean)} and passing + * {@code true} as the final argument. + * + * @param context A context. + * @param listener An optional {@link TransferListener}. + * @param userAgent The User-Agent string that should be used when requesting remote data. + */ + public DefaultUriDataSource(Context context, TransferListener listener, String userAgent) { + this(context, listener, userAgent, false); + } + + /** + * Constructs a new instance, optionally configured to follow cross-protocol redirects. + * + * @param context A context. + * @param listener An optional {@link TransferListener}. + * @param userAgent The User-Agent string that should be used when requesting remote data. + * @param allowCrossProtocolRedirects Whether cross-protocol redirects (i.e. redirects from HTTP + * to HTTPS and vice versa) are enabled when fetching remote data.. + */ + public DefaultUriDataSource(Context context, TransferListener listener, String userAgent, + boolean allowCrossProtocolRedirects) { + this(context, listener, + new DefaultHttpDataSource(userAgent, null, listener, + DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS, + DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS, allowCrossProtocolRedirects)); + } + + /** + * Constructs a new instance, using a provided {@link HttpDataSource} for fetching remote data. + * + * @param context A context. + * @param listener An optional {@link TransferListener}. + * @param httpDataSource {@link UriDataSource} to use for non-file URIs. + */ + public DefaultUriDataSource(Context context, TransferListener listener, + UriDataSource httpDataSource) { + this.httpDataSource = Assertions.checkNotNull(httpDataSource); + this.fileDataSource = new FileDataSource(listener); + this.assetDataSource = new AssetDataSource(context, listener); + this.contentDataSource = new ContentDataSource(context, listener); + } + + @Override + public long open(DataSpec dataSpec) throws IOException { + Assertions.checkState(dataSource == null); + // Choose the correct source for the scheme. + String scheme = dataSpec.uri.getScheme(); + if (Util.isLocalFileUri(dataSpec.uri)) { + if (dataSpec.uri.getPath().startsWith("/android_asset/")) { + dataSource = assetDataSource; + } else { + dataSource = fileDataSource; + } + } else if (SCHEME_ASSET.equals(scheme)) { + dataSource = assetDataSource; + } else if (SCHEME_CONTENT.equals(scheme)) { + dataSource = contentDataSource; + } else { + dataSource = httpDataSource; + } + // Open the source and return. + return dataSource.open(dataSpec); + } + + @Override + public int read(byte[] buffer, int offset, int readLength) throws IOException { + return dataSource.read(buffer, offset, readLength); + } + + @Override + public String getUri() { + return dataSource == null ? null : dataSource.getUri(); + } + + @Override + public void close() throws IOException { + if (dataSource != null) { + try { + dataSource.close(); + } finally { + dataSource = null; + } + } + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/FileDataSource.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/FileDataSource.java new file mode 100755 index 000000000..cf28babc7 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/FileDataSource.java @@ -0,0 +1,134 @@ +/* + * 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.messenger.exoplayer.upstream; + +import org.telegram.messenger.exoplayer.C; + +import java.io.EOFException; +import java.io.IOException; +import java.io.RandomAccessFile; + +/** + * A local file {@link UriDataSource}. + */ +public final class FileDataSource implements UriDataSource { + + /** + * Thrown when IOException is encountered during local file read operation. + */ + public static class FileDataSourceException extends IOException { + + public FileDataSourceException(IOException cause) { + super(cause); + } + + } + + private final TransferListener listener; + + private RandomAccessFile file; + private String uriString; + private long bytesRemaining; + private boolean opened; + + /** + * Constructs a new {@link DataSource} that retrieves data from a file. + */ + public FileDataSource() { + this(null); + } + + /** + * Constructs a new {@link DataSource} that retrieves data from a file. + * + * @param listener An optional listener. Specify {@code null} for no listener. + */ + public FileDataSource(TransferListener listener) { + this.listener = listener; + } + + @Override + public long open(DataSpec dataSpec) throws FileDataSourceException { + try { + uriString = dataSpec.uri.toString(); + file = new RandomAccessFile(dataSpec.uri.getPath(), "r"); + file.seek(dataSpec.position); + bytesRemaining = dataSpec.length == C.LENGTH_UNBOUNDED ? file.length() - dataSpec.position + : dataSpec.length; + if (bytesRemaining < 0) { + throw new EOFException(); + } + } catch (IOException e) { + throw new FileDataSourceException(e); + } + + opened = true; + if (listener != null) { + listener.onTransferStart(); + } + + return bytesRemaining; + } + + @Override + public int read(byte[] buffer, int offset, int readLength) throws FileDataSourceException { + if (bytesRemaining == 0) { + return -1; + } else { + int bytesRead = 0; + try { + bytesRead = file.read(buffer, offset, (int) Math.min(bytesRemaining, readLength)); + } catch (IOException e) { + throw new FileDataSourceException(e); + } + + if (bytesRead > 0) { + bytesRemaining -= bytesRead; + if (listener != null) { + listener.onBytesTransferred(bytesRead); + } + } + + return bytesRead; + } + } + + @Override + public String getUri() { + return uriString; + } + + @Override + public void close() throws FileDataSourceException { + uriString = null; + if (file != null) { + try { + file.close(); + } catch (IOException e) { + throw new FileDataSourceException(e); + } finally { + file = null; + if (opened) { + opened = false; + if (listener != null) { + listener.onTransferEnd(); + } + } + } + } + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/HttpDataSource.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/HttpDataSource.java new file mode 100755 index 000000000..808ee355c --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/HttpDataSource.java @@ -0,0 +1,165 @@ +/* + * 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.messenger.exoplayer.upstream; + +import org.telegram.messenger.exoplayer.util.Predicate; +import org.telegram.messenger.exoplayer.util.Util; + +import android.text.TextUtils; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +/** + * An HTTP specific extension to {@link UriDataSource}. + */ +public interface HttpDataSource extends UriDataSource { + + /** + * A {@link Predicate} that rejects content types often used for pay-walls. + */ + Predicate REJECT_PAYWALL_TYPES = new Predicate() { + + @Override + public boolean evaluate(String contentType) { + contentType = Util.toLowerInvariant(contentType); + return !TextUtils.isEmpty(contentType) + && (!contentType.contains("text") || contentType.contains("text/vtt")) + && !contentType.contains("html") && !contentType.contains("xml"); + } + + }; + + /** + * Thrown when an error is encountered when trying to read from a {@link HttpDataSource}. + */ + class HttpDataSourceException extends IOException { + + public static final int TYPE_OPEN = 1; + public static final int TYPE_READ = 2; + public static final int TYPE_CLOSE = 3; + + public final int type; + + /** + * The {@link DataSpec} associated with the current connection. + */ + public final DataSpec dataSpec; + + public HttpDataSourceException(DataSpec dataSpec, int type) { + super(); + this.dataSpec = dataSpec; + this.type = type; + } + + public HttpDataSourceException(String message, DataSpec dataSpec, int type) { + super(message); + this.dataSpec = dataSpec; + this.type = type; + } + + public HttpDataSourceException(IOException cause, DataSpec dataSpec, int type) { + super(cause); + this.dataSpec = dataSpec; + this.type = type; + } + + public HttpDataSourceException(String message, IOException cause, DataSpec dataSpec, int type) { + super(message, cause); + this.dataSpec = dataSpec; + this.type = type; + } + + } + + /** + * Thrown when the content type is invalid. + */ + final class InvalidContentTypeException extends HttpDataSourceException { + + public final String contentType; + + public InvalidContentTypeException(String contentType, DataSpec dataSpec) { + super("Invalid content type: " + contentType, dataSpec, TYPE_OPEN); + this.contentType = contentType; + } + + } + + /** + * Thrown when an attempt to open a connection results in a response code not in the 2xx range. + */ + final class InvalidResponseCodeException extends HttpDataSourceException { + + /** + * The response code that was outside of the 2xx range. + */ + public final int responseCode; + + /** + * An unmodifiable map of the response header fields and values. + */ + public final Map> headerFields; + + public InvalidResponseCodeException(int responseCode, Map> headerFields, + DataSpec dataSpec) { + super("Response code: " + responseCode, dataSpec, TYPE_OPEN); + this.responseCode = responseCode; + this.headerFields = headerFields; + } + + } + + @Override + long open(DataSpec dataSpec) throws HttpDataSourceException; + + @Override + void close() throws HttpDataSourceException; + + @Override + int read(byte[] buffer, int offset, int readLength) throws HttpDataSourceException; + + /** + * Sets the value of a request header field. The value will be used for subsequent connections + * established by the source. + * + * @param name The name of the header field. + * @param value The value of the field. + */ + void setRequestProperty(String name, String value); + + /** + * Clears the value of a request header field. The change will apply to subsequent connections + * established by the source. + * + * @param name The name of the header field. + */ + void clearRequestProperty(String name); + + /** + * Clears all request header fields that were set by {@link #setRequestProperty(String, String)}. + */ + void clearAllRequestProperties(); + + /** + * Gets the headers provided in the response. + * + * @return The response headers, or {@code null} if response headers are unavailable. + */ + Map> getResponseHeaders(); + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/Loader.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/Loader.java new file mode 100755 index 000000000..fd2b4bd29 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/Loader.java @@ -0,0 +1,260 @@ +/* + * 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.messenger.exoplayer.upstream; + +import org.telegram.messenger.exoplayer.util.Assertions; +import org.telegram.messenger.exoplayer.util.TraceUtil; +import org.telegram.messenger.exoplayer.util.Util; + +import android.annotation.SuppressLint; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.util.Log; + +import java.io.IOException; +import java.util.concurrent.ExecutorService; + +/** + * Manages the background loading of {@link Loadable}s. + */ +public final class Loader { + + /** + * Thrown when an unexpected exception is encountered during loading. + */ + public static final class UnexpectedLoaderException extends IOException { + + public UnexpectedLoaderException(Exception cause) { + super("Unexpected " + cause.getClass().getSimpleName() + ": " + cause.getMessage(), cause); + } + + } + + /** + * Interface definition of an object that can be loaded using a {@link Loader}. + */ + public interface Loadable { + + /** + * Cancels the load. + */ + void cancelLoad(); + + /** + * Whether the load has been canceled. + * + * @return True if the load has been canceled. False otherwise. + */ + boolean isLoadCanceled(); + + /** + * Performs the load, returning on completion or cancelation. + * + * @throws IOException + * @throws InterruptedException + */ + void load() throws IOException, InterruptedException; + + } + + /** + * Interface definition for a callback to be notified of {@link Loader} events. + */ + public interface Callback { + + /** + * Invoked when loading has been canceled. + * + * @param loadable The loadable whose load has been canceled. + */ + void onLoadCanceled(Loadable loadable); + + /** + * Invoked when the data source has been fully loaded. + * + * @param loadable The loadable whose load has completed. + */ + void onLoadCompleted(Loadable loadable); + + /** + * Invoked when the data source is stopped due to an error. + * + * @param loadable The loadable whose load has failed. + */ + void onLoadError(Loadable loadable, IOException exception); + + } + + private static final int MSG_END_OF_SOURCE = 0; + private static final int MSG_IO_EXCEPTION = 1; + private static final int MSG_FATAL_ERROR = 2; + + private final ExecutorService downloadExecutorService; + + private LoadTask currentTask; + private boolean loading; + + /** + * @param threadName A name for the loader's thread. + */ + public Loader(String threadName) { + this.downloadExecutorService = Util.newSingleThreadExecutor(threadName); + } + + /** + * Invokes {@link #startLoading(Looper, Loadable, Callback)}, using the {@link Looper} + * associated with the calling thread. + * + * @param loadable The {@link Loadable} to load. + * @param callback A callback to invoke when the load ends. + * @throws IllegalStateException If the calling thread does not have an associated {@link Looper}. + */ + public void startLoading(Loadable loadable, Callback callback) { + Looper myLooper = Looper.myLooper(); + Assertions.checkState(myLooper != null); + startLoading(myLooper, loadable, callback); + } + + /** + * Start loading a {@link Loadable}. + *

+ * A {@link Loader} instance can only load one {@link Loadable} at a time, and so this method + * must not be called when another load is in progress. + * + * @param looper The looper of the thread on which the callback should be invoked. + * @param loadable The {@link Loadable} to load. + * @param callback A callback to invoke when the load ends. + */ + public void startLoading(Looper looper, Loadable loadable, Callback callback) { + Assertions.checkState(!loading); + loading = true; + currentTask = new LoadTask(looper, loadable, callback); + downloadExecutorService.submit(currentTask); + } + + /** + * Whether the {@link Loader} is currently loading a {@link Loadable}. + * + * @return Whether the {@link Loader} is currently loading a {@link Loadable}. + */ + public boolean isLoading() { + return loading; + } + + /** + * Cancels the current load. + *

+ * This method should only be called when a load is in progress. + */ + public void cancelLoading() { + Assertions.checkState(loading); + currentTask.quit(); + } + + /** + * Releases the {@link Loader}. + *

+ * This method should be called when the {@link Loader} is no longer required. + */ + public void release() { + if (loading) { + cancelLoading(); + } + downloadExecutorService.shutdown(); + } + + @SuppressLint("HandlerLeak") + private final class LoadTask extends Handler implements Runnable { + + private static final String TAG = "LoadTask"; + + private final Loadable loadable; + private final Loader.Callback callback; + + private volatile Thread executorThread; + + public LoadTask(Looper looper, Loadable loadable, Loader.Callback callback) { + super(looper); + this.loadable = loadable; + this.callback = callback; + } + + public void quit() { + loadable.cancelLoad(); + if (executorThread != null) { + executorThread.interrupt(); + } + } + + @Override + public void run() { + try { + executorThread = Thread.currentThread(); + if (!loadable.isLoadCanceled()) { + TraceUtil.beginSection(loadable.getClass().getSimpleName() + ".load()"); + loadable.load(); + TraceUtil.endSection(); + } + sendEmptyMessage(MSG_END_OF_SOURCE); + } catch (IOException e) { + obtainMessage(MSG_IO_EXCEPTION, e).sendToTarget(); + } catch (InterruptedException e) { + // The load was canceled. + Assertions.checkState(loadable.isLoadCanceled()); + sendEmptyMessage(MSG_END_OF_SOURCE); + } catch (Exception e) { + // This should never happen, but handle it anyway. + Log.e(TAG, "Unexpected exception loading stream", e); + obtainMessage(MSG_IO_EXCEPTION, new UnexpectedLoaderException(e)).sendToTarget(); + } catch (Error e) { + // We'd hope that the platform would kill the process if an Error is thrown here, but the + // executor may catch the error (b/20616433). Throw it here, but also pass and throw it from + // the handler thread so that the process dies even if the executor behaves in this way. + Log.e(TAG, "Unexpected error loading stream", e); + obtainMessage(MSG_FATAL_ERROR, e).sendToTarget(); + throw e; + } + } + + @Override + public void handleMessage(Message msg) { + if (msg.what == MSG_FATAL_ERROR) { + throw (Error) msg.obj; + } + onFinished(); + if (loadable.isLoadCanceled()) { + callback.onLoadCanceled(loadable); + return; + } + switch (msg.what) { + case MSG_END_OF_SOURCE: + callback.onLoadCompleted(loadable); + break; + case MSG_IO_EXCEPTION: + callback.onLoadError(loadable, (IOException) msg.obj); + break; + } + } + + private void onFinished() { + loading = false; + currentTask = null; + } + + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/NetworkLock.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/NetworkLock.java new file mode 100755 index 000000000..83e310d83 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/NetworkLock.java @@ -0,0 +1,135 @@ +/* + * 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.messenger.exoplayer.upstream; + +import java.io.IOException; +import java.util.PriorityQueue; + +/** + * A network task prioritization mechanism. + *

+ * Manages different priority network tasks. A network task that wishes to have its priority + * respected, and respect the priority of other tasks, should register itself with the lock prior + * to making network requests. It should then call one of the lock's proceed methods frequently + * during execution, so as to ensure that it continues only if it is the highest (or equally + * highest) priority task. + *

+ * Note that lower integer values correspond to higher priorities. + */ +public final class NetworkLock { + + /** + * Thrown when a task is attempts to proceed when it does not have the highest priority. + */ + public static class PriorityTooLowException extends IOException { + + public PriorityTooLowException(int priority, int highestPriority) { + super("Priority too low [priority=" + priority + ", highest=" + highestPriority + "]"); + } + + } + + public static final NetworkLock instance = new NetworkLock(); + + /** + * Priority for network tasks associated with media streaming. + */ + public static final int STREAMING_PRIORITY = 0; + /** + * Priority for network tasks associated with background downloads. + */ + public static final int DOWNLOAD_PRIORITY = 10; + + private final Object lock = new Object(); + + /** Guarded by {@link #lock}. */ + private final PriorityQueue queue; + + /** Guarded by {@link #lock}. */ + private int highestPriority; + + private NetworkLock() { + queue = new PriorityQueue<>(); + highestPriority = Integer.MAX_VALUE; + } + + /** + * Blocks until the passed priority is the lowest one (i.e. highest priority). + * + * @param priority The priority of the task that would like to proceed. + */ + public void proceed(int priority) throws InterruptedException { + synchronized (lock) { + while (highestPriority < priority) { + lock.wait(); + } + } + } + + /** + * A non-blocking variant of {@link #proceed(int)}. + * + * @param priority The priority of the task that would like to proceed. + * @return Whether the passed priority is allowed to proceed. + */ + public boolean proceedNonBlocking(int priority) { + synchronized (lock) { + return highestPriority >= priority; + } + } + + /** + * A throwing variant of {@link #proceed(int)}. + * + * @param priority The priority of the task that would like to proceed. + * @throws PriorityTooLowException If the passed priority is not high enough to proceed. + */ + public void proceedOrThrow(int priority) throws PriorityTooLowException { + synchronized (lock) { + if (highestPriority < priority) { + throw new PriorityTooLowException(priority, highestPriority); + } + } + } + + /** + * Register a new task. + *

+ * The task must call {@link #remove(int)} when done. + * + * @param priority The priority of the task. + */ + public void add(int priority) { + synchronized (lock) { + queue.add(priority); + highestPriority = Math.min(highestPriority, priority); + } + } + + /** + * Unregister a task. + * + * @param priority The priority of the task. + */ + public void remove(int priority) { + synchronized (lock) { + queue.remove(priority); + highestPriority = queue.isEmpty() ? Integer.MAX_VALUE : queue.peek(); + lock.notifyAll(); + } + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/PriorityDataSource.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/PriorityDataSource.java new file mode 100755 index 000000000..f9db9d8f7 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/PriorityDataSource.java @@ -0,0 +1,58 @@ +/* + * 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.messenger.exoplayer.upstream; + +import org.telegram.messenger.exoplayer.util.Assertions; + +import java.io.IOException; + +/** + * Allows {@link #open(DataSpec)} and {@link #read(byte[], int, int)} calls only if the specified + * priority is the highest priority of any task. {@link NetworkLock.PriorityTooLowException} is + * thrown when this condition does not hold. + */ +public final class PriorityDataSource implements DataSource { + + private final DataSource upstream; + private final int priority; + + /** + * @param priority The priority of the source. + * @param upstream The upstream {@link DataSource}. + */ + public PriorityDataSource(int priority, DataSource upstream) { + this.priority = priority; + this.upstream = Assertions.checkNotNull(upstream); + } + + @Override + public long open(DataSpec dataSpec) throws IOException { + NetworkLock.instance.proceedOrThrow(priority); + return upstream.open(dataSpec); + } + + @Override + public int read(byte[] buffer, int offset, int max) throws IOException { + NetworkLock.instance.proceedOrThrow(priority); + return upstream.read(buffer, offset, max); + } + + @Override + public void close() throws IOException { + upstream.close(); + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/TeeDataSource.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/TeeDataSource.java new file mode 100755 index 000000000..2d4995649 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/TeeDataSource.java @@ -0,0 +1,71 @@ +/* + * 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.messenger.exoplayer.upstream; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.util.Assertions; + +import java.io.IOException; + +/** + * Tees data into a {@link DataSink} as the data is read. + */ +public final class TeeDataSource implements DataSource { + + private final DataSource upstream; + private final DataSink dataSink; + + /** + * @param upstream The upstream {@link DataSource}. + * @param dataSink The {@link DataSink} into which data is written. + */ + public TeeDataSource(DataSource upstream, DataSink dataSink) { + this.upstream = Assertions.checkNotNull(upstream); + this.dataSink = Assertions.checkNotNull(dataSink); + } + + @Override + public long open(DataSpec dataSpec) throws IOException { + long dataLength = upstream.open(dataSpec); + if (dataSpec.length == C.LENGTH_UNBOUNDED && dataLength != C.LENGTH_UNBOUNDED) { + // Reconstruct dataSpec in order to provide the resolved length to the sink. + dataSpec = new DataSpec(dataSpec.uri, dataSpec.absoluteStreamPosition, dataSpec.position, + dataLength, dataSpec.key, dataSpec.flags); + } + dataSink.open(dataSpec); + return dataLength; + } + + @Override + public int read(byte[] buffer, int offset, int max) throws IOException { + int num = upstream.read(buffer, offset, max); + if (num > 0) { + // TODO: Consider continuing even if disk writes fail. + dataSink.write(buffer, offset, num); + } + return num; + } + + @Override + public void close() throws IOException { + try { + upstream.close(); + } finally { + dataSink.close(); + } + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/TransferListener.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/TransferListener.java new file mode 100755 index 000000000..391754205 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/TransferListener.java @@ -0,0 +1,41 @@ +/* + * 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.messenger.exoplayer.upstream; + +/** + * Interface definition for a callback to be notified of data transfer events. + */ +public interface TransferListener { + + /** + * Invoked when a transfer starts. + */ + void onTransferStart(); + + /** + * Called incrementally during a transfer. + * + * @param bytesTransferred The number of bytes transferred since the previous call to this + * method (or if the first call, since the transfer was started). + */ + void onBytesTransferred(int bytesTransferred); + + /** + * Invoked when a transfer ends. + */ + void onTransferEnd(); + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/UdpDataSource.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/UdpDataSource.java new file mode 100755 index 000000000..458c9ec2f --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/UdpDataSource.java @@ -0,0 +1,186 @@ +/* + * 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.messenger.exoplayer.upstream; + +import org.telegram.messenger.exoplayer.C; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.MulticastSocket; +import java.net.SocketException; + +/** + * A UDP {@link DataSource}. + */ +public final class UdpDataSource implements UriDataSource { + + /** + * Thrown when an error is encountered when trying to read from a {@link UdpDataSource}. + */ + public static final class UdpDataSourceException extends IOException { + + public UdpDataSourceException(String message) { + super(message); + } + + public UdpDataSourceException(IOException cause) { + super(cause); + } + + } + + /** + * The default maximum datagram packet size, in bytes. + */ + public static final int DEFAULT_MAX_PACKET_SIZE = 2000; + + /** + * The default socket timeout, in milliseconds. + */ + public static final int DEAFULT_SOCKET_TIMEOUT_MILLIS = 8 * 1000; + + private final TransferListener listener; + private final DatagramPacket packet; + private final int socketTimeoutMillis; + + private DataSpec dataSpec; + private DatagramSocket socket; + private MulticastSocket multicastSocket; + private InetAddress address; + private InetSocketAddress socketAddress; + private boolean opened; + + private byte[] packetBuffer; + private int packetRemaining; + + /** + * @param listener An optional listener. + */ + public UdpDataSource(TransferListener listener) { + this(listener, DEFAULT_MAX_PACKET_SIZE); + } + + /** + * @param listener An optional listener. + * @param maxPacketSize The maximum datagram packet size, in bytes. + */ + public UdpDataSource(TransferListener listener, int maxPacketSize) { + this(listener, maxPacketSize, DEAFULT_SOCKET_TIMEOUT_MILLIS); + } + + /** + * @param listener An optional listener. + * @param maxPacketSize The maximum datagram packet size, in bytes. + * @param socketTimeoutMillis The socket timeout in milliseconds. A timeout of zero is interpreted + * as an infinite timeout. + */ + public UdpDataSource(TransferListener listener, int maxPacketSize, int socketTimeoutMillis) { + this.listener = listener; + this.socketTimeoutMillis = socketTimeoutMillis; + packetBuffer = new byte[maxPacketSize]; + packet = new DatagramPacket(packetBuffer, 0, maxPacketSize); + } + + @Override + public long open(DataSpec dataSpec) throws UdpDataSourceException { + this.dataSpec = dataSpec; + String host = dataSpec.uri.getHost(); + int port = dataSpec.uri.getPort(); + + try { + address = InetAddress.getByName(host); + socketAddress = new InetSocketAddress(address, port); + if (address.isMulticastAddress()) { + multicastSocket = new MulticastSocket(socketAddress); + multicastSocket.joinGroup(address); + socket = multicastSocket; + } else { + socket = new DatagramSocket(socketAddress); + } + } catch (IOException e) { + throw new UdpDataSourceException(e); + } + + try { + socket.setSoTimeout(socketTimeoutMillis); + } catch (SocketException e) { + throw new UdpDataSourceException(e); + } + + opened = true; + if (listener != null) { + listener.onTransferStart(); + } + return C.LENGTH_UNBOUNDED; + } + + @Override + public int read(byte[] buffer, int offset, int readLength) throws UdpDataSourceException { + if (packetRemaining == 0) { + // We've read all of the data from the current packet. Get another. + try { + socket.receive(packet); + } catch (IOException e) { + throw new UdpDataSourceException(e); + } + + packetRemaining = packet.getLength(); + if (listener != null) { + listener.onBytesTransferred(packetRemaining); + } + } + + int packetOffset = packet.getLength() - packetRemaining; + int bytesToRead = Math.min(packetRemaining, readLength); + System.arraycopy(packetBuffer, packetOffset, buffer, offset, bytesToRead); + packetRemaining -= bytesToRead; + return bytesToRead; + } + + @Override + public void close() { + if (multicastSocket != null) { + try { + multicastSocket.leaveGroup(address); + } catch (IOException e) { + // Do nothing. + } + multicastSocket = null; + } + if (socket != null) { + socket.close(); + socket = null; + } + address = null; + socketAddress = null; + packetRemaining = 0; + if (opened) { + opened = false; + if (listener != null) { + listener.onTransferEnd(); + } + } + } + + @Override + public String getUri() { + return dataSpec == null ? null : dataSpec.uri.toString(); + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/UriDataSource.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/UriDataSource.java new file mode 100755 index 000000000..b10431b3c --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/UriDataSource.java @@ -0,0 +1,32 @@ +/* + * 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.messenger.exoplayer.upstream; + +/** + * A component that provides media data from a URI. + */ +public interface UriDataSource extends DataSource { + + /** + * When the source is open, returns the URI from which data is being read. + *

+ * If redirection occurred, the URI after redirection is the one returned. + * + * @return When the source is open, the URI from which data is being read. Null otherwise. + */ + String getUri(); + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/UriLoadable.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/UriLoadable.java new file mode 100755 index 000000000..7c169206c --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/UriLoadable.java @@ -0,0 +1,99 @@ +/* + * 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.messenger.exoplayer.upstream; + +import org.telegram.messenger.exoplayer.ParserException; +import org.telegram.messenger.exoplayer.upstream.Loader.Loadable; + +import android.net.Uri; + +import java.io.IOException; +import java.io.InputStream; + +/** + * A {@link Loadable} for loading an object from a URI. + * + * @param The type of the object being loaded. + */ +public final class UriLoadable implements Loadable { + + /** + * Parses an object from loaded data. + */ + public interface Parser { + + /** + * Parses an object from a response. + * + * @param connectionUrl The source of the response, after any redirection. + * @param inputStream An {@link InputStream} from which the response data can be read. + * @return The parsed object. + * @throws ParserException If an error occurs parsing the data. + * @throws IOException If an error occurs reading data from the stream. + */ + T parse(String connectionUrl, InputStream inputStream) throws ParserException, IOException; + + } + + private final DataSpec dataSpec; + private final UriDataSource uriDataSource; + private final Parser parser; + + private volatile T result; + private volatile boolean isCanceled; + + /** + * @param url The url from which the object should be loaded. + * @param uriDataSource A {@link UriDataSource} to use when loading the data. + * @param parser Parses the object from the response. + */ + public UriLoadable(String url, UriDataSource uriDataSource, Parser parser) { + this.uriDataSource = uriDataSource; + this.parser = parser; + dataSpec = new DataSpec(Uri.parse(url), DataSpec.FLAG_ALLOW_GZIP); + } + + /** + * Returns the loaded object, or null if an object has not been loaded. + */ + public final T getResult() { + return result; + } + + @Override + public final void cancelLoad() { + // We don't actually cancel anything, but we need to record the cancellation so that + // isLoadCanceled can return the correct value. + isCanceled = true; + } + + @Override + public final boolean isLoadCanceled() { + return isCanceled; + } + + @Override + public final void load() throws IOException, InterruptedException { + DataSourceInputStream inputStream = new DataSourceInputStream(uriDataSource, dataSpec); + try { + inputStream.open(); + result = parser.parse(uriDataSource.getUri(), inputStream); + } finally { + inputStream.close(); + } + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/cache/Cache.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/cache/Cache.java new file mode 100755 index 000000000..0cf4b365e --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/cache/Cache.java @@ -0,0 +1,185 @@ +/* + * 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.messenger.exoplayer.upstream.cache; + +import java.io.File; +import java.util.NavigableSet; +import java.util.Set; + +/** + * An interface for cache. + */ +public interface Cache { + + /** + * Interface definition for a callback to be notified of {@link Cache} events. + */ + public interface Listener { + + /** + * Invoked when a {@link CacheSpan} is added to the cache. + * + * @param cache The source of the event. + * @param span The added {@link CacheSpan}. + */ + void onSpanAdded(Cache cache, CacheSpan span); + + /** + * Invoked when a {@link CacheSpan} is removed from the cache. + * + * @param cache The source of the event. + * @param span The removed {@link CacheSpan}. + */ + void onSpanRemoved(Cache cache, CacheSpan span); + + /** + * Invoked when an existing {@link CacheSpan} is accessed, causing it to be replaced. The new + * {@link CacheSpan} is guaranteed to represent the same data as the one it replaces, however + * {@link CacheSpan#file} and {@link CacheSpan#lastAccessTimestamp} may have changed. + *

+ * Note that for span replacement, {@link #onSpanAdded(Cache, CacheSpan)} and + * {@link #onSpanRemoved(Cache, CacheSpan)} are not invoked in addition to this method. + * + * @param cache The source of the event. + * @param oldSpan The old {@link CacheSpan}, which has been removed from the cache. + * @param newSpan The new {@link CacheSpan}, which has been added to the cache. + */ + void onSpanTouched(Cache cache, CacheSpan oldSpan, CacheSpan newSpan); + + } + + /** + * Registers a listener to listen for changes to a given key. + *

+ * No guarantees are made about the thread or threads on which the listener is invoked, but it + * is guaranteed that listener methods will be invoked in a serial fashion (i.e. one at a time) + * and in the same order as events occurred. + * + * @param key The key to listen to. + * @param listener The listener to add. + * @return The current spans for the key. + */ + NavigableSet addListener(String key, Listener listener); + + /** + * Unregisters a listener. + * + * @param key The key to stop listening to. + * @param listener The listener to remove. + */ + void removeListener(String key, Listener listener); + + /** + * Returns the cached spans for a given cache key. + * + * @param key The key for which spans should be returned. + * @return The spans for the key. May be null if there are no such spans. + */ + NavigableSet getCachedSpans(String key); + + /** + * Returns all keys in the cache. + * + * @return All the keys in the cache. + */ + Set getKeys(); + + /** + * Returns the total disk space in bytes used by the cache. + * + * @return The total disk space in bytes. + */ + long getCacheSpace(); + + /** + * A caller should invoke this method when they require data from a given position for a given + * key. + *

+ * If there is a cache entry that overlaps the position, then the returned {@link CacheSpan} + * defines the file in which the data is stored. {@link CacheSpan#isCached} is true. The caller + * may read from the cache file, but does not acquire any locks. + *

+ * If there is no cache entry overlapping {@code offset}, then the returned {@link CacheSpan} + * defines a hole in the cache starting at {@code position} into which the caller may write as it + * obtains the data from some other source. The returned {@link CacheSpan} serves as a lock. + * Whilst the caller holds the lock it may write data into the hole. It may split data into + * multiple files. When the caller has finished writing a file it should commit it to the cache + * by calling {@link #commitFile(File)}. When the caller has finished writing, it must release + * the lock by calling {@link #releaseHoleSpan}. + * + * @param key The key of the data being requested. + * @param position The position of the data being requested. + * @return The {@link CacheSpan}. + * @throws InterruptedException + */ + CacheSpan startReadWrite(String key, long position) throws InterruptedException; + + /** + * Same as {@link #startReadWrite(String, long)}. However, if the cache entry is locked, then + * instead of blocking, this method will return null as the {@link CacheSpan}. + * + * @param key The key of the data being requested. + * @param position The position of the data being requested. + * @return The {@link CacheSpan}. Or null if the cache entry is locked. + */ + CacheSpan startReadWriteNonBlocking(String key, long position); + + /** + * Obtains a cache file into which data can be written. Must only be called when holding a + * corresponding hole {@link CacheSpan} obtained from {@link #startReadWrite(String, long)}. + * + * @param key The cache key for the data. + * @param position The starting position of the data. + * @param length The length of the data to be written. Used only to ensure that there is enough + * space in the cache. + * @return The file into which data should be written. + */ + File startFile(String key, long position, long length); + + /** + * Commits a file into the cache. Must only be called when holding a corresponding hole + * {@link CacheSpan} obtained from {@link #startReadWrite(String, long)} + * + * @param file A newly written cache file. + */ + void commitFile(File file); + + /** + * Releases a {@link CacheSpan} obtained from {@link #startReadWrite(String, long)} which + * corresponded to a hole in the cache. + * + * @param holeSpan The {@link CacheSpan} being released. + */ + void releaseHoleSpan(CacheSpan holeSpan); + + /** + * Removes a cached {@link CacheSpan} from the cache, deleting the underlying file. + * + * @param span The {@link CacheSpan} to remove. + */ + void removeSpan(CacheSpan span); + + /** + * Queries if a range is entirely available in the cache. + * + * @param key The cache key for the data. + * @param position The starting position of the data. + * @param length The length of the data. + * @return true if the data is available in the Cache otherwise false; + */ + boolean isCached(String key, long position, long length); + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/cache/CacheDataSink.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/cache/CacheDataSink.java new file mode 100755 index 000000000..accac24ec --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/cache/CacheDataSink.java @@ -0,0 +1,140 @@ +/* + * 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.messenger.exoplayer.upstream.cache; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.upstream.DataSink; +import org.telegram.messenger.exoplayer.upstream.DataSpec; +import org.telegram.messenger.exoplayer.util.Assertions; +import org.telegram.messenger.exoplayer.util.Util; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; + +/** + * Writes data into a cache. + */ +public final class CacheDataSink implements DataSink { + + private final Cache cache; + private final long maxCacheFileSize; + + private DataSpec dataSpec; + private File file; + private FileOutputStream outputStream; + private long outputStreamBytesWritten; + private long dataSpecBytesWritten; + + /** + * Thrown when IOException is encountered when writing data into sink. + */ + public static class CacheDataSinkException extends IOException { + + public CacheDataSinkException(IOException cause) { + super(cause); + } + + } + + + /** + * @param cache The cache into which data should be written. + * @param maxCacheFileSize The maximum size of a cache file, in bytes. If the sink is opened for + * a {@link DataSpec} whose size exceeds this value, then the data will be fragmented into + * multiple cache files. + */ + public CacheDataSink(Cache cache, long maxCacheFileSize) { + this.cache = Assertions.checkNotNull(cache); + this.maxCacheFileSize = maxCacheFileSize; + } + + @Override + public DataSink open(DataSpec dataSpec) throws CacheDataSinkException { + // TODO: Support caching for unbounded requests. See TODO in {@link CacheDataSource} for + // more details. + Assertions.checkState(dataSpec.length != C.LENGTH_UNBOUNDED); + try { + this.dataSpec = dataSpec; + dataSpecBytesWritten = 0; + openNextOutputStream(); + return this; + } catch (FileNotFoundException e) { + throw new CacheDataSinkException(e); + } + } + + @Override + public void write(byte[] buffer, int offset, int length) throws CacheDataSinkException { + try { + int bytesWritten = 0; + while (bytesWritten < length) { + if (outputStreamBytesWritten == maxCacheFileSize) { + closeCurrentOutputStream(); + openNextOutputStream(); + } + int bytesToWrite = (int) Math.min(length - bytesWritten, + maxCacheFileSize - outputStreamBytesWritten); + outputStream.write(buffer, offset + bytesWritten, bytesToWrite); + bytesWritten += bytesToWrite; + outputStreamBytesWritten += bytesToWrite; + dataSpecBytesWritten += bytesToWrite; + } + } catch (IOException e) { + throw new CacheDataSinkException(e); + } + } + + @Override + public void close() throws CacheDataSinkException { + try { + closeCurrentOutputStream(); + } catch (IOException e) { + throw new CacheDataSinkException(e); + } + } + + private void openNextOutputStream() throws FileNotFoundException { + file = cache.startFile(dataSpec.key, dataSpec.absoluteStreamPosition + dataSpecBytesWritten, + Math.min(dataSpec.length - dataSpecBytesWritten, maxCacheFileSize)); + outputStream = new FileOutputStream(file); + outputStreamBytesWritten = 0; + } + + private void closeCurrentOutputStream() throws IOException { + if (outputStream == null) { + return; + } + + boolean success = false; + try { + outputStream.flush(); + outputStream.getFD().sync(); + success = true; + } finally { + Util.closeQuietly(outputStream); + if (success) { + cache.commitFile(file); + } else { + file.delete(); + } + outputStream = null; + file = null; + } + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/cache/CacheDataSource.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/cache/CacheDataSource.java new file mode 100755 index 000000000..af3ee50ac --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/cache/CacheDataSource.java @@ -0,0 +1,257 @@ +/* + * 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.messenger.exoplayer.upstream.cache; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.upstream.DataSink; +import org.telegram.messenger.exoplayer.upstream.DataSource; +import org.telegram.messenger.exoplayer.upstream.DataSpec; +import org.telegram.messenger.exoplayer.upstream.FileDataSource; +import org.telegram.messenger.exoplayer.upstream.TeeDataSource; +import org.telegram.messenger.exoplayer.upstream.cache.CacheDataSink.CacheDataSinkException; + +import android.net.Uri; +import android.util.Log; + +import java.io.IOException; +import java.io.InterruptedIOException; + +/** + * A {@link DataSource} that reads and writes a {@link Cache}. Requests are fulfilled from the cache + * when possible. When data is not cached it is requested from an upstream {@link DataSource} and + * written into the cache. + */ +public final class CacheDataSource implements DataSource { + + /** + * Interface definition for a callback to be notified of {@link CacheDataSource} events. + */ + public interface EventListener { + + /** + * Invoked when bytes have been read from the cache. + * + * @param cacheSizeBytes Current cache size in bytes. + * @param cachedBytesRead Total bytes read from the cache since this method was last invoked. + */ + void onCachedBytesRead(long cacheSizeBytes, long cachedBytesRead); + + } + + private static final String TAG = "CacheDataSource"; + + private final Cache cache; + private final DataSource cacheReadDataSource; + private final DataSource cacheWriteDataSource; + private final DataSource upstreamDataSource; + private final EventListener eventListener; + + private final boolean blockOnCache; + private final boolean ignoreCacheOnError; + + private DataSource currentDataSource; + private Uri uri; + private int flags; + private String key; + private long readPosition; + private long bytesRemaining; + private CacheSpan lockedSpan; + private boolean ignoreCache; + private long totalCachedBytesRead; + + /** + * Constructs an instance with default {@link DataSource} and {@link DataSink} instances for + * reading and writing the cache. + */ + public CacheDataSource(Cache cache, DataSource upstream, boolean blockOnCache, + boolean ignoreCacheOnError) { + this(cache, upstream, blockOnCache, ignoreCacheOnError, Long.MAX_VALUE); + } + + /** + * Constructs an instance with default {@link DataSource} and {@link DataSink} instances for + * reading and writing the cache. The sink is configured to fragment data such that no single + * cache file is greater than maxCacheFileSize bytes. + */ + public CacheDataSource(Cache cache, DataSource upstream, boolean blockOnCache, + boolean ignoreCacheOnError, long maxCacheFileSize) { + this(cache, upstream, new FileDataSource(), new CacheDataSink(cache, maxCacheFileSize), + blockOnCache, ignoreCacheOnError, null); + } + + /** + * Constructs an instance with arbitrary {@link DataSource} and {@link DataSink} instances for + * reading and writing the cache. One use of this constructor is to allow data to be transformed + * before it is written to disk. + * + * @param cache The cache. + * @param upstream A {@link DataSource} for reading data not in the cache. + * @param cacheReadDataSource A {@link DataSource} for reading data from the cache. + * @param cacheWriteDataSink A {@link DataSink} for writing data to the cache. + * @param blockOnCache A flag indicating whether we will block reads if the cache key is locked. + * If this flag is false, then we will read from upstream if the cache key is locked. + * @param ignoreCacheOnError Whether the cache is bypassed following any cache related error. If + * true, then cache related exceptions may be thrown for one cycle of open, read and close + * calls. Subsequent cycles of these calls will then bypass the cache. + * @param eventListener An optional {@link EventListener} to receive events. + */ + public CacheDataSource(Cache cache, DataSource upstream, DataSource cacheReadDataSource, + DataSink cacheWriteDataSink, boolean blockOnCache, boolean ignoreCacheOnError, + EventListener eventListener) { + this.cache = cache; + this.cacheReadDataSource = cacheReadDataSource; + this.blockOnCache = blockOnCache; + this.ignoreCacheOnError = ignoreCacheOnError; + this.upstreamDataSource = upstream; + if (cacheWriteDataSink != null) { + this.cacheWriteDataSource = new TeeDataSource(upstream, cacheWriteDataSink); + } else { + this.cacheWriteDataSource = null; + } + this.eventListener = eventListener; + } + + @Override + public long open(DataSpec dataSpec) throws IOException { + try { + uri = dataSpec.uri; + flags = dataSpec.flags; + key = dataSpec.key; + readPosition = dataSpec.position; + bytesRemaining = dataSpec.length; + openNextSource(); + return dataSpec.length; + } catch (IOException e) { + handleBeforeThrow(e); + throw e; + } + } + + @Override + public int read(byte[] buffer, int offset, int max) throws IOException { + try { + int bytesRead = currentDataSource.read(buffer, offset, max); + if (bytesRead >= 0) { + if (currentDataSource == cacheReadDataSource) { + totalCachedBytesRead += bytesRead; + } + readPosition += bytesRead; + if (bytesRemaining != C.LENGTH_UNBOUNDED) { + bytesRemaining -= bytesRead; + } + } else { + closeCurrentSource(); + if (bytesRemaining > 0 && bytesRemaining != C.LENGTH_UNBOUNDED) { + openNextSource(); + return read(buffer, offset, max); + } + } + return bytesRead; + } catch (IOException e) { + handleBeforeThrow(e); + throw e; + } + } + + @Override + public void close() throws IOException { + notifyBytesRead(); + try { + closeCurrentSource(); + } catch (IOException e) { + handleBeforeThrow(e); + throw e; + } + } + + /** + * Opens the next source. If the cache contains data spanning the current read position then + * {@link #cacheReadDataSource} is opened to read from it. Else {@link #upstreamDataSource} is + * opened to read from the upstream source and write into the cache. + */ + private void openNextSource() throws IOException { + DataSpec dataSpec; + CacheSpan span; + if (ignoreCache) { + span = null; + } else if (bytesRemaining == C.LENGTH_UNBOUNDED) { + // TODO: Support caching for unbounded requests. This requires storing the source length + // into the cache (the simplest approach is to incorporate it into each cache file's name). + Log.w(TAG, "Cache bypassed due to unbounded length."); + span = null; + } else if (blockOnCache) { + try { + span = cache.startReadWrite(key, readPosition); + } catch (InterruptedException e) { + throw new InterruptedIOException(); + } + } else { + span = cache.startReadWriteNonBlocking(key, readPosition); + } + if (span == null) { + // The data is locked in the cache, or we're ignoring the cache. Bypass the cache and read + // from upstream. + currentDataSource = upstreamDataSource; + dataSpec = new DataSpec(uri, readPosition, bytesRemaining, key, flags); + } else if (span.isCached) { + // Data is cached, read from cache. + Uri fileUri = Uri.fromFile(span.file); + long filePosition = readPosition - span.position; + long length = Math.min(span.length - filePosition, bytesRemaining); + dataSpec = new DataSpec(fileUri, readPosition, filePosition, length, key, flags); + currentDataSource = cacheReadDataSource; + } else { + // Data is not cached, and data is not locked, read from upstream with cache backing. + lockedSpan = span; + long length = span.isOpenEnded() ? bytesRemaining : Math.min(span.length, bytesRemaining); + dataSpec = new DataSpec(uri, readPosition, length, key, flags); + currentDataSource = cacheWriteDataSource != null ? cacheWriteDataSource + : upstreamDataSource; + } + currentDataSource.open(dataSpec); + } + + private void closeCurrentSource() throws IOException { + if (currentDataSource == null) { + return; + } + try { + currentDataSource.close(); + currentDataSource = null; + } finally { + if (lockedSpan != null) { + cache.releaseHoleSpan(lockedSpan); + lockedSpan = null; + } + } + } + + private void handleBeforeThrow(IOException exception) { + if (ignoreCacheOnError && (currentDataSource == cacheReadDataSource + || exception instanceof CacheDataSinkException)) { + // Ignore the cache from now on. + ignoreCache = true; + } + } + + private void notifyBytesRead() { + if (eventListener != null && totalCachedBytesRead > 0) { + eventListener.onCachedBytesRead(cache.getCacheSpace(), totalCachedBytesRead); + totalCachedBytesRead = 0; + } + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/cache/CacheEvictor.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/cache/CacheEvictor.java new file mode 100755 index 000000000..d8da430fd --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/cache/CacheEvictor.java @@ -0,0 +1,34 @@ +/* + * 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.messenger.exoplayer.upstream.cache; + +/** + * Evicts data from a {@link Cache}. Implementations should call {@link Cache#removeSpan(CacheSpan)} + * to evict cache entries based on their eviction policies. + */ +public interface CacheEvictor extends Cache.Listener { + + /** + * Invoked when a writer starts writing to the cache. + * + * @param cache The source of the event. + * @param key The key being written. + * @param position The starting position of the data being written. + * @param length The maximum length of the data being written. + */ + void onStartFile(Cache cache, String key, long position, long length); + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/cache/CacheSpan.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/cache/CacheSpan.java new file mode 100755 index 000000000..5c3a4abf9 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/cache/CacheSpan.java @@ -0,0 +1,150 @@ +/* + * 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.messenger.exoplayer.upstream.cache; + +import org.telegram.messenger.exoplayer.util.Util; + +import java.io.File; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Defines a span of data that may or may not be cached (as indicated by {@link #isCached}). + */ +public final class CacheSpan implements Comparable { + + private static final String SUFFIX = ".v2.exo"; + private static final Pattern CACHE_FILE_PATTERN_V1 = + Pattern.compile("^(.+)\\.(\\d+)\\.(\\d+)\\.v1\\.exo$"); + private static final Pattern CACHE_FILE_PATTERN_V2 = + Pattern.compile("^(.+)\\.(\\d+)\\.(\\d+)\\.v2\\.exo$"); + + /** + * The cache key that uniquely identifies the original stream. + */ + public final String key; + /** + * The position of the {@link CacheSpan} in the original stream. + */ + public final long position; + /** + * The length of the {@link CacheSpan}, or -1 if this is an open-ended hole. + */ + public final long length; + /** + * Whether the {@link CacheSpan} is cached. + */ + public final boolean isCached; + /** + * The file corresponding to this {@link CacheSpan}, or null if {@link #isCached} is false. + */ + public final File file; + /** + * The last access timestamp, or -1 if {@link #isCached} is false. + */ + public final long lastAccessTimestamp; + + public static File getCacheFileName(File cacheDir, String key, long offset, + long lastAccessTimestamp) { + return new File(cacheDir, + Util.escapeFileName(key) + "." + offset + "." + lastAccessTimestamp + SUFFIX); + } + + public static CacheSpan createLookup(String key, long position) { + return new CacheSpan(key, position, -1, false, -1, null); + } + + public static CacheSpan createOpenHole(String key, long position) { + return new CacheSpan(key, position, -1, false, -1, null); + } + + public static CacheSpan createClosedHole(String key, long position, long length) { + return new CacheSpan(key, position, length, false, -1, null); + } + + /** + * Creates a cache span from an underlying cache file. + * + * @param file The cache file. + * @return The span, or null if the file name is not correctly formatted. + */ + public static CacheSpan createCacheEntry(File file) { + Matcher matcher = CACHE_FILE_PATTERN_V2.matcher(file.getName()); + if (!matcher.matches()) { + return null; + } + String key = Util.unescapeFileName(matcher.group(1)); + return key == null ? null : createCacheEntry( + key, Long.parseLong(matcher.group(2)), Long.parseLong(matcher.group(3)), file); + } + + static File upgradeIfNeeded(File file) { + Matcher matcher = CACHE_FILE_PATTERN_V1.matcher(file.getName()); + if (!matcher.matches()) { + return file; + } + String key = matcher.group(1); // Keys were not escaped in version 1. + File newCacheFile = getCacheFileName(file.getParentFile(), key, + Long.parseLong(matcher.group(2)), Long.parseLong(matcher.group(3))); + file.renameTo(newCacheFile); + return newCacheFile; + } + + private static CacheSpan createCacheEntry(String key, long position, long lastAccessTimestamp, + File file) { + return new CacheSpan(key, position, file.length(), true, lastAccessTimestamp, file); + } + + // Visible for testing. + CacheSpan(String key, long position, long length, boolean isCached, + long lastAccessTimestamp, File file) { + this.key = key; + this.position = position; + this.length = length; + this.isCached = isCached; + this.file = file; + this.lastAccessTimestamp = lastAccessTimestamp; + } + + /** + * @return True if this is an open-ended {@link CacheSpan}. False otherwise. + */ + public boolean isOpenEnded() { + return length == -1; + } + + /** + * Renames the file underlying this cache span to update its last access time. + * + * @return A {@link CacheSpan} representing the updated cache file. + */ + public CacheSpan touch() { + long now = System.currentTimeMillis(); + File newCacheFile = getCacheFileName(file.getParentFile(), key, position, now); + file.renameTo(newCacheFile); + return CacheSpan.createCacheEntry(key, position, now, newCacheFile); + } + + @Override + public int compareTo(CacheSpan another) { + if (!key.equals(another.key)) { + return key.compareTo(another.key); + } + long startOffsetDiff = position - another.position; + return startOffsetDiff == 0 ? 0 : ((startOffsetDiff < 0) ? -1 : 1); + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/cache/LeastRecentlyUsedCacheEvictor.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/cache/LeastRecentlyUsedCacheEvictor.java new file mode 100755 index 000000000..b60eb09e7 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/cache/LeastRecentlyUsedCacheEvictor.java @@ -0,0 +1,76 @@ +/* + * 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.messenger.exoplayer.upstream.cache; + +import java.util.Comparator; +import java.util.TreeSet; + +/** + * Evicts least recently used cache files first. + */ +public final class LeastRecentlyUsedCacheEvictor implements CacheEvictor, Comparator { + + private final long maxBytes; + private final TreeSet leastRecentlyUsed; + + private long currentSize; + + public LeastRecentlyUsedCacheEvictor(long maxBytes) { + this.maxBytes = maxBytes; + this.leastRecentlyUsed = new TreeSet<>(this); + } + + @Override + public void onStartFile(Cache cache, String key, long position, long length) { + evictCache(cache, length); + } + + @Override + public void onSpanAdded(Cache cache, CacheSpan span) { + leastRecentlyUsed.add(span); + currentSize += span.length; + evictCache(cache, 0); + } + + @Override + public void onSpanRemoved(Cache cache, CacheSpan span) { + leastRecentlyUsed.remove(span); + currentSize -= span.length; + } + + @Override + public void onSpanTouched(Cache cache, CacheSpan oldSpan, CacheSpan newSpan) { + onSpanRemoved(cache, oldSpan); + onSpanAdded(cache, newSpan); + } + + @Override + public int compare(CacheSpan lhs, CacheSpan rhs) { + long lastAccessTimestampDelta = lhs.lastAccessTimestamp - rhs.lastAccessTimestamp; + if (lastAccessTimestampDelta == 0) { + // Use the standard compareTo method as a tie-break. + return lhs.compareTo(rhs); + } + return lhs.lastAccessTimestamp < rhs.lastAccessTimestamp ? -1 : 1; + } + + private void evictCache(Cache cache, long requiredSpace) { + while (currentSize + requiredSpace > maxBytes) { + cache.removeSpan(leastRecentlyUsed.first()); + } + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/cache/NoOpCacheEvictor.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/cache/NoOpCacheEvictor.java new file mode 100755 index 000000000..3eb70f6e8 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/cache/NoOpCacheEvictor.java @@ -0,0 +1,47 @@ +/* + * 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.messenger.exoplayer.upstream.cache; + + +/** + * Evictor that doesn't ever evict cache files. + * + * Warning: Using this evictor might have unforeseeable consequences if cache + * size is not managed elsewhere. + */ +public final class NoOpCacheEvictor implements CacheEvictor { + + @Override + public void onStartFile(Cache cache, String key, long position, long length) { + // Do nothing. + } + + @Override + public void onSpanAdded(Cache cache, CacheSpan span) { + // Do nothing. + } + + @Override + public void onSpanRemoved(Cache cache, CacheSpan span) { + // Do nothing. + } + + @Override + public void onSpanTouched(Cache cache, CacheSpan oldSpan, CacheSpan newSpan) { + // Do nothing. + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/cache/SimpleCache.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/cache/SimpleCache.java new file mode 100755 index 000000000..fadd0894a --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/upstream/cache/SimpleCache.java @@ -0,0 +1,382 @@ +/* + * 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.messenger.exoplayer.upstream.cache; + +import org.telegram.messenger.exoplayer.util.Assertions; + +import android.os.ConditionVariable; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map.Entry; +import java.util.NavigableSet; +import java.util.Set; +import java.util.TreeSet; + +/** + * A {@link Cache} implementation that maintains an in-memory representation. + */ +public final class SimpleCache implements Cache { + + private final File cacheDir; + private final CacheEvictor evictor; + private final HashMap lockedSpans; + private final HashMap> cachedSpans; + private final HashMap> listeners; + private long totalSpace = 0; + + /** + * Constructs the cache. The cache will delete any unrecognized files from the directory. Hence + * the directory cannot be used to store other files. + * + * @param cacheDir A dedicated cache directory. + */ + public SimpleCache(File cacheDir, CacheEvictor evictor) { + this.cacheDir = cacheDir; + this.evictor = evictor; + this.lockedSpans = new HashMap<>(); + this.cachedSpans = new HashMap<>(); + this.listeners = new HashMap<>(); + // Start cache initialization. + final ConditionVariable conditionVariable = new ConditionVariable(); + new Thread("SimpleCache.initialize()") { + @Override + public void run() { + synchronized (SimpleCache.this) { + conditionVariable.open(); + initialize(); + } + } + }.start(); + conditionVariable.block(); + } + + @Override + public synchronized NavigableSet addListener(String key, Listener listener) { + ArrayList listenersForKey = listeners.get(key); + if (listenersForKey == null) { + listenersForKey = new ArrayList<>(); + listeners.put(key, listenersForKey); + } + listenersForKey.add(listener); + return getCachedSpans(key); + } + + @Override + public synchronized void removeListener(String key, Listener listener) { + ArrayList listenersForKey = listeners.get(key); + if (listenersForKey != null) { + listenersForKey.remove(listener); + if (listenersForKey.isEmpty()) { + listeners.remove(key); + } + } + } + + @Override + public synchronized NavigableSet getCachedSpans(String key) { + TreeSet spansForKey = cachedSpans.get(key); + return spansForKey == null ? null : new TreeSet<>(spansForKey); + } + + @Override + public synchronized Set getKeys() { + return new HashSet<>(cachedSpans.keySet()); + } + + @Override + public synchronized long getCacheSpace() { + return totalSpace; + } + + @Override + public synchronized CacheSpan startReadWrite(String key, long position) + throws InterruptedException { + CacheSpan lookupSpan = CacheSpan.createLookup(key, position); + while (true) { + CacheSpan span = startReadWriteNonBlocking(lookupSpan); + if (span != null) { + return span; + } else { + // Write case, lock not available. We'll be woken up when a locked span is released (if the + // released lock is for the requested key then we'll be able to make progress) or when a + // span is added to the cache (if the span is for the requested key and covers the requested + // position, then we'll become a read and be able to make progress). + wait(); + } + } + } + + @Override + public synchronized CacheSpan startReadWriteNonBlocking(String key, long position) { + return startReadWriteNonBlocking(CacheSpan.createLookup(key, position)); + } + + private synchronized CacheSpan startReadWriteNonBlocking(CacheSpan lookupSpan) { + CacheSpan spanningRegion = getSpan(lookupSpan); + + // Read case. + if (spanningRegion.isCached) { + CacheSpan oldCacheSpan = spanningRegion; + // Remove the old span from the in-memory representation. + TreeSet spansForKey = cachedSpans.get(oldCacheSpan.key); + Assertions.checkState(spansForKey.remove(oldCacheSpan)); + // Obtain a new span with updated last access timestamp. + spanningRegion = oldCacheSpan.touch(); + // Add the updated span back into the in-memory representation. + spansForKey.add(spanningRegion); + notifySpanTouched(oldCacheSpan, spanningRegion); + return spanningRegion; + } + + // Write case, lock available. + if (!lockedSpans.containsKey(lookupSpan.key)) { + lockedSpans.put(lookupSpan.key, spanningRegion); + return spanningRegion; + } + + // Write case, lock not available. + return null; + } + + @Override + public synchronized File startFile(String key, long position, long length) { + Assertions.checkState(lockedSpans.containsKey(key)); + if (!cacheDir.exists()) { + // For some reason the cache directory doesn't exist. Make a best effort to create it. + removeStaleSpans(); + cacheDir.mkdirs(); + } + evictor.onStartFile(this, key, position, length); + return CacheSpan.getCacheFileName(cacheDir, key, position, System.currentTimeMillis()); + } + + @Override + public synchronized void commitFile(File file) { + CacheSpan span = CacheSpan.createCacheEntry(file); + Assertions.checkState(span != null); + Assertions.checkState(lockedSpans.containsKey(span.key)); + // If the file doesn't exist, don't add it to the in-memory representation. + if (!file.exists()) { + return; + } + // If the file has length 0, delete it and don't add it to the in-memory representation. + long length = file.length(); + if (length == 0) { + file.delete(); + return; + } + addSpan(span); + notifyAll(); + } + + @Override + public synchronized void releaseHoleSpan(CacheSpan holeSpan) { + Assertions.checkState(holeSpan == lockedSpans.remove(holeSpan.key)); + notifyAll(); + } + + /** + * Returns the cache {@link CacheSpan} corresponding to the provided lookup {@link CacheSpan}. + *

+ * If the lookup position is contained by an existing entry in the cache, then the returned + * {@link CacheSpan} defines the file in which the data is stored. If the lookup position is not + * contained by an existing entry, then the returned {@link CacheSpan} defines the maximum extents + * of the hole in the cache. + * + * @param lookupSpan A lookup {@link CacheSpan} specifying a key and position. + * @return The corresponding cache {@link CacheSpan}. + */ + private CacheSpan getSpan(CacheSpan lookupSpan) { + String key = lookupSpan.key; + long offset = lookupSpan.position; + TreeSet entries = cachedSpans.get(key); + if (entries == null) { + return CacheSpan.createOpenHole(key, lookupSpan.position); + } + CacheSpan floorSpan = entries.floor(lookupSpan); + if (floorSpan != null && + floorSpan.position <= offset && offset < floorSpan.position + floorSpan.length) { + // The lookup position is contained within floorSpan. + if (floorSpan.file.exists()) { + return floorSpan; + } else { + // The file has been deleted from under us. It's likely that other files will have been + // deleted too, so scan the whole in-memory representation. + removeStaleSpans(); + return getSpan(lookupSpan); + } + } + CacheSpan ceilEntry = entries.ceiling(lookupSpan); + return ceilEntry == null ? CacheSpan.createOpenHole(key, lookupSpan.position) : + CacheSpan.createClosedHole(key, lookupSpan.position, + ceilEntry.position - lookupSpan.position); + } + + /** + * Ensures that the cache's in-memory representation has been initialized. + */ + private void initialize() { + if (!cacheDir.exists()) { + cacheDir.mkdirs(); + } + File[] files = cacheDir.listFiles(); + if (files == null) { + return; + } + for (int i = 0; i < files.length; i++) { + File file = files[i]; + if (file.length() == 0) { + file.delete(); + } else { + file = CacheSpan.upgradeIfNeeded(file); + CacheSpan span = CacheSpan.createCacheEntry(file); + if (span == null) { + file.delete(); + } else { + addSpan(span); + } + } + } + } + + /** + * Adds a cached span to the in-memory representation. + * + * @param span The span to be added. + */ + private void addSpan(CacheSpan span) { + TreeSet spansForKey = cachedSpans.get(span.key); + if (spansForKey == null) { + spansForKey = new TreeSet<>(); + cachedSpans.put(span.key, spansForKey); + } + spansForKey.add(span); + totalSpace += span.length; + notifySpanAdded(span); + } + + @Override + public synchronized void removeSpan(CacheSpan span) { + TreeSet spansForKey = cachedSpans.get(span.key); + totalSpace -= span.length; + Assertions.checkState(spansForKey.remove(span)); + span.file.delete(); + if (spansForKey.isEmpty()) { + cachedSpans.remove(span.key); + } + notifySpanRemoved(span); + } + + /** + * Scans all of the cached spans in the in-memory representation, removing any for which files + * no longer exist. + */ + private void removeStaleSpans() { + Iterator>> iterator = cachedSpans.entrySet().iterator(); + while (iterator.hasNext()) { + Entry> next = iterator.next(); + Iterator spanIterator = next.getValue().iterator(); + boolean isEmpty = true; + while (spanIterator.hasNext()) { + CacheSpan span = spanIterator.next(); + if (!span.file.exists()) { + spanIterator.remove(); + if (span.isCached) { + totalSpace -= span.length; + } + notifySpanRemoved(span); + } else { + isEmpty = false; + } + } + if (isEmpty) { + iterator.remove(); + } + } + } + + private void notifySpanRemoved(CacheSpan span) { + ArrayList keyListeners = listeners.get(span.key); + if (keyListeners != null) { + for (int i = keyListeners.size() - 1; i >= 0; i--) { + keyListeners.get(i).onSpanRemoved(this, span); + } + } + evictor.onSpanRemoved(this, span); + } + + private void notifySpanAdded(CacheSpan span) { + ArrayList keyListeners = listeners.get(span.key); + if (keyListeners != null) { + for (int i = keyListeners.size() - 1; i >= 0; i--) { + keyListeners.get(i).onSpanAdded(this, span); + } + } + evictor.onSpanAdded(this, span); + } + + private void notifySpanTouched(CacheSpan oldSpan, CacheSpan newSpan) { + ArrayList keyListeners = listeners.get(oldSpan.key); + if (keyListeners != null) { + for (int i = keyListeners.size() - 1; i >= 0; i--) { + keyListeners.get(i).onSpanTouched(this, oldSpan, newSpan); + } + } + evictor.onSpanTouched(this, oldSpan, newSpan); + } + + @Override + public synchronized boolean isCached(String key, long position, long length) { + TreeSet entries = cachedSpans.get(key); + if (entries == null) { + return false; + } + CacheSpan lookupSpan = CacheSpan.createLookup(key, position); + CacheSpan floorSpan = entries.floor(lookupSpan); + if (floorSpan == null || floorSpan.position + floorSpan.length <= position) { + // We don't have a span covering the start of the queried region. + return false; + } + long queryEndPosition = position + length; + long currentEndPosition = floorSpan.position + floorSpan.length; + if (currentEndPosition >= queryEndPosition) { + // floorSpan covers the queried region. + return true; + } + Iterator iterator = entries.tailSet(floorSpan, false).iterator(); + while (iterator.hasNext()) { + CacheSpan next = iterator.next(); + if (next.position > currentEndPosition) { + // There's a hole in the cache within the queried region. + return false; + } + // We expect currentEndPosition to always equal (next.position + next.length), but + // perform a max check anyway to guard against the existence of overlapping spans. + currentEndPosition = Math.max(currentEndPosition, next.position + next.length); + if (currentEndPosition >= queryEndPosition) { + // We've found spans covering the queried region. + return true; + } + } + // We ran out of spans before covering the queried region. + return false; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/Ac3Util.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/Ac3Util.java new file mode 100755 index 000000000..ec868a1cb --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/Ac3Util.java @@ -0,0 +1,240 @@ +/* + * 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.messenger.exoplayer.util; + +import org.telegram.messenger.exoplayer.MediaFormat; + +import java.nio.ByteBuffer; + +/** + * Utility methods for parsing (E-)AC-3 syncframes, which are access units in (E-)AC-3 bitstreams. + */ +public final class Ac3Util { + + /** + * The number of new samples per (E-)AC-3 audio block. + */ + private static final int AUDIO_SAMPLES_PER_AUDIO_BLOCK = 256; + /** + * Each syncframe has 6 blocks that provide 256 new audio samples. See ETSI TS 102 366 4.1. + */ + private static final int AC3_SYNCFRAME_AUDIO_SAMPLE_COUNT = 6 * AUDIO_SAMPLES_PER_AUDIO_BLOCK; + /** + * Number of audio blocks per E-AC-3 syncframe, indexed by numblkscod. + */ + private static final int[] BLOCKS_PER_SYNCFRAME_BY_NUMBLKSCOD = new int[] {1, 2, 3, 6}; + /** + * Sample rates, indexed by fscod. + */ + private static final int[] SAMPLE_RATE_BY_FSCOD = new int[] {48000, 44100, 32000}; + /** + * Sample rates, indexed by fscod2 (E-AC-3). + */ + private static final int[] SAMPLE_RATE_BY_FSCOD2 = new int[] {24000, 22050, 16000}; + /** + * Channel counts, indexed by acmod. + */ + private static final int[] CHANNEL_COUNT_BY_ACMOD = new int[] {2, 1, 2, 3, 3, 4, 4, 5}; + /** + * Nominal bitrates in kbps, indexed by frmsizecod / 2. (See ETSI TS 102 366 table 4.13.) + */ + private static final int[] BITRATE_BY_HALF_FRMSIZECOD = new int[] {32, 40, 48, 56, 64, 80, 96, + 112, 128, 160, 192, 224, 256, 320, 384, 448, 512, 576, 640}; + /** + * 16-bit words per syncframe, indexed by frmsizecod / 2. (See ETSI TS 102 366 table 4.13.) + */ + private static final int[] SYNCFRAME_SIZE_WORDS_BY_HALF_FRMSIZECOD_44_1 = new int[] {69, 87, 104, + 121, 139, 174, 208, 243, 278, 348, 417, 487, 557, 696, 835, 975, 1114, 1253, 1393}; + + /** + * Returns the AC-3 format given {@code data} containing the AC3SpecificBox according to + * ETSI TS 102 366 Annex F. The reading position of {@code data} will be modified. + * + * @param data The AC3SpecificBox to parse. + * @param trackId The track identifier to set on the format, or null. + * @param durationUs The duration to set on the format, in microseconds. + * @param language The language to set on the format. + * @return The AC-3 format parsed from data in the header. + */ + public static MediaFormat parseAc3AnnexFFormat(ParsableByteArray data, String trackId, + long durationUs, String language) { + int fscod = (data.readUnsignedByte() & 0xC0) >> 6; + int sampleRate = SAMPLE_RATE_BY_FSCOD[fscod]; + int nextByte = data.readUnsignedByte(); + int channelCount = CHANNEL_COUNT_BY_ACMOD[(nextByte & 0x38) >> 3]; + if ((nextByte & 0x04) != 0) { // lfeon + channelCount++; + } + return MediaFormat.createAudioFormat(trackId, MimeTypes.AUDIO_AC3, MediaFormat.NO_VALUE, + MediaFormat.NO_VALUE, durationUs, channelCount, sampleRate, null, language); + } + + /** + * Returns the E-AC-3 format given {@code data} containing the EC3SpecificBox according to + * ETSI TS 102 366 Annex F. The reading position of {@code data} will be modified. + * + * @param data The EC3SpecificBox to parse. + * @param trackId The track identifier to set on the format, or null. + * @param durationUs The duration to set on the format, in microseconds. + * @param language The language to set on the format. + * @return The E-AC-3 format parsed from data in the header. + */ + public static MediaFormat parseEAc3AnnexFFormat(ParsableByteArray data, String trackId, + long durationUs, String language) { + data.skipBytes(2); // data_rate, num_ind_sub + + // Read only the first substream. + // TODO: Read later substreams? + int fscod = (data.readUnsignedByte() & 0xC0) >> 6; + int sampleRate = SAMPLE_RATE_BY_FSCOD[fscod]; + int nextByte = data.readUnsignedByte(); + int channelCount = CHANNEL_COUNT_BY_ACMOD[(nextByte & 0x0E) >> 1]; + if ((nextByte & 0x01) != 0) { // lfeon + channelCount++; + } + return MediaFormat.createAudioFormat(trackId, MimeTypes.AUDIO_E_AC3, MediaFormat.NO_VALUE, + MediaFormat.NO_VALUE, durationUs, channelCount, sampleRate, null, language); + } + + /** + * Returns the AC-3 format given {@code data} containing a syncframe. The reading position of + * {@code data} will be modified. + * + * @param data The data to parse, positioned at the start of the syncframe. + * @param trackId The track identifier to set on the format, or null. + * @param durationUs The duration to set on the format, in microseconds. + * @param language The language to set on the format. + * @return The AC-3 format parsed from data in the header. + */ + public static MediaFormat parseAc3SyncframeFormat(ParsableBitArray data, String trackId, + long durationUs, String language) { + data.skipBits(16 + 16); // syncword, crc1 + int fscod = data.readBits(2); + data.skipBits(6 + 5 + 3); // frmsizecod, bsid, bsmod + int acmod = data.readBits(3); + if ((acmod & 0x01) != 0 && acmod != 1) { + data.skipBits(2); // cmixlev + } + if ((acmod & 0x04) != 0) { + data.skipBits(2); // surmixlev + } + if (acmod == 2) { + data.skipBits(2); // dsurmod + } + boolean lfeon = data.readBit(); + return MediaFormat.createAudioFormat(trackId, MimeTypes.AUDIO_AC3, MediaFormat.NO_VALUE, + MediaFormat.NO_VALUE, durationUs, CHANNEL_COUNT_BY_ACMOD[acmod] + (lfeon ? 1 : 0), + SAMPLE_RATE_BY_FSCOD[fscod], null, language); + } + + /** + * Returns the E-AC-3 format given {@code data} containing a syncframe. The reading position of + * {@code data} will be modified. + * + * @param data The data to parse, positioned at the start of the syncframe. + * @param trackId The track identifier to set on the format, or null. + * @param durationUs The duration to set on the format, in microseconds. + * @param language The language to set on the format. + * @return The E-AC-3 format parsed from data in the header. + */ + public static MediaFormat parseEac3SyncframeFormat(ParsableBitArray data, String trackId, + long durationUs, String language) { + data.skipBits(16 + 2 + 3 + 11); // syncword, strmtype, substreamid, frmsiz + int sampleRate; + int fscod = data.readBits(2); + if (fscod == 3) { + sampleRate = SAMPLE_RATE_BY_FSCOD2[data.readBits(2)]; + } else { + data.skipBits(2); // numblkscod + sampleRate = SAMPLE_RATE_BY_FSCOD[fscod]; + } + int acmod = data.readBits(3); + boolean lfeon = data.readBit(); + return MediaFormat.createAudioFormat(trackId, MimeTypes.AUDIO_E_AC3, MediaFormat.NO_VALUE, + MediaFormat.NO_VALUE, durationUs, CHANNEL_COUNT_BY_ACMOD[acmod] + (lfeon ? 1 : 0), + sampleRate, null, language); + } + + /** + * Returns the size in bytes of the given AC-3 syncframe. + * + * @param data The syncframe to parse. + * @return The syncframe size in bytes. + */ + public static int parseAc3SyncframeSize(byte[] data) { + int fscod = (data[4] & 0xC0) >> 6; + int frmsizecod = data[4] & 0x3F; + return getAc3SyncframeSize(fscod, frmsizecod); + } + + /** + * Returns the size in bytes of the given E-AC-3 syncframe. + * + * @param data The syncframe to parse. + * @return The syncframe size in bytes. + */ + public static int parseEAc3SyncframeSize(byte[] data) { + return 2 * (((data[2] & 0x07) << 8) + (data[3] & 0xFF) + 1); // frmsiz + } + + /** + * Returns the number of audio samples in an AC-3 syncframe. + */ + public static int getAc3SyncframeAudioSampleCount() { + return AC3_SYNCFRAME_AUDIO_SAMPLE_COUNT; + } + + /** + * Returns the number of audio samples represented by the given E-AC-3 syncframe. + * + * @param data The syncframe to parse. + * @return The number of audio samples represented by the syncframe. + */ + public static int parseEAc3SyncframeAudioSampleCount(byte[] data) { + // See ETSI TS 102 366 subsection E.1.2.2. + return AUDIO_SAMPLES_PER_AUDIO_BLOCK * (((data[4] & 0xC0) >> 6) == 0x03 ? 6 // fscod + : BLOCKS_PER_SYNCFRAME_BY_NUMBLKSCOD[(data[4] & 0x30) >> 4]); + } + + /** + * Like {@link #parseEAc3SyncframeAudioSampleCount(byte[])} but reads from a byte buffer. The + * buffer position is not modified. + * + * @see #parseEAc3SyncframeAudioSampleCount(byte[]) + */ + public static int parseEAc3SyncframeAudioSampleCount(ByteBuffer buffer) { + // See ETSI TS 102 366 subsection E.1.2.2. + int fscod = (buffer.get(buffer.position() + 4) & 0xC0) >> 6; + return AUDIO_SAMPLES_PER_AUDIO_BLOCK * (fscod == 0x03 ? 6 + : BLOCKS_PER_SYNCFRAME_BY_NUMBLKSCOD[(buffer.get(buffer.position() + 4) & 0x30) >> 4]); + } + + private static int getAc3SyncframeSize(int fscod, int frmsizecod) { + int sampleRate = SAMPLE_RATE_BY_FSCOD[fscod]; + if (sampleRate == 44100) { + return 2 * (SYNCFRAME_SIZE_WORDS_BY_HALF_FRMSIZECOD_44_1[frmsizecod / 2] + (frmsizecod % 2)); + } + int bitrate = BITRATE_BY_HALF_FRMSIZECOD[frmsizecod / 2]; + if (sampleRate == 32000) { + return 6 * bitrate; + } else { // sampleRate == 48000 + return 4 * bitrate; + } + } + + private Ac3Util() {} + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/Assertions.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/Assertions.java new file mode 100755 index 000000000..a6fed82f1 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/Assertions.java @@ -0,0 +1,155 @@ +/* + * 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.messenger.exoplayer.util; + +import org.telegram.messenger.exoplayer.ExoPlayerLibraryInfo; + +import android.os.Looper; +import android.text.TextUtils; + +/** + * Provides methods for asserting the truth of expressions and properties. + */ +public final class Assertions { + + private Assertions() {} + + /** + * Ensures the truth of an expression involving one or more arguments passed to the calling + * method. + * + * @param expression A boolean expression. + * @throws IllegalArgumentException If {@code expression} is false. + */ + public static void checkArgument(boolean expression) { + if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && !expression) { + throw new IllegalArgumentException(); + } + } + + /** + * Ensures the truth of an expression involving one or more arguments passed to the calling + * method. + * + * @param expression A boolean expression. + * @param errorMessage The exception message to use if the check fails. The message is converted + * to a {@link String} using {@link String#valueOf(Object)}. + * @throws IllegalArgumentException If {@code expression} is false. + */ + public static void checkArgument(boolean expression, Object errorMessage) { + if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && !expression) { + throw new IllegalArgumentException(String.valueOf(errorMessage)); + } + } + + /** + * Ensures the truth of an expression involving the state of the calling instance. + * + * @param expression A boolean expression. + * @throws IllegalStateException If {@code expression} is false. + */ + public static void checkState(boolean expression) { + if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && !expression) { + throw new IllegalStateException(); + } + } + + /** + * Ensures the truth of an expression involving the state of the calling instance. + * + * @param expression A boolean expression. + * @param errorMessage The exception message to use if the check fails. The message is converted + * to a string using {@link String#valueOf(Object)}. + * @throws IllegalStateException If {@code expression} is false. + */ + public static void checkState(boolean expression, Object errorMessage) { + if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && !expression) { + throw new IllegalStateException(String.valueOf(errorMessage)); + } + } + + /** + * Ensures that an object reference is not null. + * + * @param reference An object reference. + * @return The non-null reference that was validated. + * @throws NullPointerException If {@code reference} is null. + */ + public static T checkNotNull(T reference) { + if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && reference == null) { + throw new NullPointerException(); + } + return reference; + } + + /** + * Ensures that an object reference is not null. + * + * @param reference An object reference. + * @param errorMessage The exception message to use if the check fails. The message is converted + * to a string using {@link String#valueOf(Object)}. + * @return The non-null reference that was validated. + * @throws NullPointerException If {@code reference} is null. + */ + public static T checkNotNull(T reference, Object errorMessage) { + if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && reference == null) { + throw new NullPointerException(String.valueOf(errorMessage)); + } + return reference; + } + + /** + * Ensures that a string passed as an argument to the calling method is not null or 0-length. + * + * @param string A string. + * @return The non-null, non-empty string that was validated. + * @throws IllegalArgumentException If {@code string} is null or 0-length. + */ + public static String checkNotEmpty(String string) { + if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && TextUtils.isEmpty(string)) { + throw new IllegalArgumentException(); + } + return string; + } + + /** + * Ensures that a string passed as an argument to the calling method is not null or 0-length. + * + * @param string A string. + * @param errorMessage The exception message to use if the check fails. The message is converted + * to a string using {@link String#valueOf(Object)}. + * @return The non-null, non-empty string that was validated. + * @throws IllegalArgumentException If {@code string} is null or 0-length. + */ + public static String checkNotEmpty(String string, Object errorMessage) { + if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && TextUtils.isEmpty(string)) { + throw new IllegalArgumentException(String.valueOf(errorMessage)); + } + return string; + } + + /** + * Ensures that the calling thread is the application's main thread. + * + * @throws IllegalStateException If the calling thread is not the application's main thread. + */ + public static void checkMainThread() { + if (ExoPlayerLibraryInfo.ASSERTIONS_ENABLED && Looper.myLooper() != Looper.getMainLooper()) { + throw new IllegalStateException("Not in applications main thread"); + } + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/IntProperty.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/Clock.java similarity index 57% rename from TMessagesProj/src/main/java/org/telegram/messenger/Animation/IntProperty.java rename to TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/Clock.java index 175704cf4..be6b968f0 100755 --- a/TMessagesProj/src/main/java/org/telegram/messenger/Animation/IntProperty.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/Clock.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011 The Android Open Source Project + * 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. @@ -13,18 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.telegram.messenger.Animation; +package org.telegram.messenger.exoplayer.util; -public abstract class IntProperty extends Property { +/** + * An interface through which system clocks can be read. The {@link SystemClock} implementation + * must be used for all non-test cases. + */ +public interface Clock { - public IntProperty(String name) { - super(Integer.class, name); - } + /** + * Returns {@link android.os.SystemClock#elapsedRealtime}. + * + * @return Elapsed milliseconds since boot. + */ + long elapsedRealtime(); - public abstract void setValue(T object, int value); - - @Override - final public void set(T object, Integer value) { - setValue(object, value.intValue()); - } -} \ No newline at end of file +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/CodecSpecificDataUtil.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/CodecSpecificDataUtil.java new file mode 100755 index 000000000..a2f3bace6 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/CodecSpecificDataUtil.java @@ -0,0 +1,250 @@ +/* + * 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.messenger.exoplayer.util; + +import android.util.Pair; + +import java.util.ArrayList; +import java.util.List; + +/** + * Provides static utility methods for manipulating various types of codec specific data. + */ +public final class CodecSpecificDataUtil { + + private static final byte[] NAL_START_CODE = new byte[] {0, 0, 0, 1}; + + private static final int AUDIO_SPECIFIC_CONFIG_FREQUENCY_INDEX_ARBITRARY = 0xF; + + private static final int[] AUDIO_SPECIFIC_CONFIG_SAMPLING_RATE_TABLE = new int[] { + 96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350 + }; + + private static final int AUDIO_SPECIFIC_CONFIG_CHANNEL_CONFIGURATION_INVALID = -1; + /** + * In the channel configurations below, indicates a single channel element; (A, B) indicates a + * channel pair element; and [A] indicates a low-frequency effects element. + * The speaker mapping short forms used are: + * - FC: front center + * - BC: back center + * - FL/FR: front left/right + * - FCL/FCR: front center left/right + * - FTL/FTR: front top left/right + * - SL/SR: back surround left/right + * - BL/BR: back left/right + * - LFE: low frequency effects + */ + private static final int[] AUDIO_SPECIFIC_CONFIG_CHANNEL_COUNT_TABLE = + new int[] { + 0, + 1, /* mono: */ + 2, /* stereo: (FL, FR) */ + 3, /* 3.0: , (FL, FR) */ + 4, /* 4.0: , (FL, FR), */ + 5, /* 5.0 back: , (FL, FR), (SL, SR) */ + 6, /* 5.1 back: , (FL, FR), (SL, SR), , [LFE] */ + 8, /* 7.1 wide back: , (FCL, FCR), (FL, FR), (SL, SR), [LFE] */ + AUDIO_SPECIFIC_CONFIG_CHANNEL_CONFIGURATION_INVALID, + AUDIO_SPECIFIC_CONFIG_CHANNEL_CONFIGURATION_INVALID, + AUDIO_SPECIFIC_CONFIG_CHANNEL_CONFIGURATION_INVALID, + 7, /* 6.1: , (FL, FR), (SL, SR), , [LFE] */ + 8, /* 7.1: , (FL, FR), (SL, SR), (BL, BR), [LFE] */ + AUDIO_SPECIFIC_CONFIG_CHANNEL_CONFIGURATION_INVALID, + 8, /* 7.1 top: , (FL, FR), (SL, SR), [LFE], (FTL, FTR) */ + AUDIO_SPECIFIC_CONFIG_CHANNEL_CONFIGURATION_INVALID + }; + + // Advanced Audio Coding Low-Complexity profile. + private static final int AUDIO_OBJECT_TYPE_AAC_LC = 2; + // Spectral Band Replication. + private static final int AUDIO_OBJECT_TYPE_SBR = 5; + // Error Resilient Bit-Sliced Arithmetic Coding. + private static final int AUDIO_OBJECT_TYPE_ER_BSAC = 22; + // Parametric Stereo. + private static final int AUDIO_OBJECT_TYPE_PS = 29; + + private CodecSpecificDataUtil() {} + + /** + * Parses an AudioSpecificConfig, as defined in ISO 14496-3 1.6.2.1 + * + * @param audioSpecificConfig The AudioSpecificConfig to parse. + * @return A pair consisting of the sample rate in Hz and the channel count. + */ + public static Pair parseAacAudioSpecificConfig(byte[] audioSpecificConfig) { + ParsableBitArray bitArray = new ParsableBitArray(audioSpecificConfig); + int audioObjectType = bitArray.readBits(5); + int frequencyIndex = bitArray.readBits(4); + int sampleRate; + if (frequencyIndex == AUDIO_SPECIFIC_CONFIG_FREQUENCY_INDEX_ARBITRARY) { + sampleRate = bitArray.readBits(24); + } else { + Assertions.checkArgument(frequencyIndex < 13); + sampleRate = AUDIO_SPECIFIC_CONFIG_SAMPLING_RATE_TABLE[frequencyIndex]; + } + int channelConfiguration = bitArray.readBits(4); + if (audioObjectType == AUDIO_OBJECT_TYPE_SBR || audioObjectType == AUDIO_OBJECT_TYPE_PS) { + // For an AAC bitstream using spectral band replication (SBR) or parametric stereo (PS) with + // explicit signaling, we return the extension sampling frequency as the sample rate of the + // content; this is identical to the sample rate of the decoded output but may differ from + // the sample rate set above. + // Use the extensionSamplingFrequencyIndex. + frequencyIndex = bitArray.readBits(4); + if (frequencyIndex == AUDIO_SPECIFIC_CONFIG_FREQUENCY_INDEX_ARBITRARY) { + sampleRate = bitArray.readBits(24); + } else { + Assertions.checkArgument(frequencyIndex < 13); + sampleRate = AUDIO_SPECIFIC_CONFIG_SAMPLING_RATE_TABLE[frequencyIndex]; + } + audioObjectType = bitArray.readBits(5); + if (audioObjectType == AUDIO_OBJECT_TYPE_ER_BSAC) { + // Use the extensionChannelConfiguration. + channelConfiguration = bitArray.readBits(4); + } + } + int channelCount = AUDIO_SPECIFIC_CONFIG_CHANNEL_COUNT_TABLE[channelConfiguration]; + Assertions.checkArgument(channelCount != AUDIO_SPECIFIC_CONFIG_CHANNEL_CONFIGURATION_INVALID); + return Pair.create(sampleRate, channelCount); + } + + /** + * Builds a simple AudioSpecificConfig, as defined in ISO 14496-3 1.6.2.1 + * + * @param audioObjectType The audio object type. + * @param sampleRateIndex The sample rate index. + * @param channelConfig The channel configuration. + * @return The AudioSpecificConfig. + */ + public static byte[] buildAacAudioSpecificConfig(int audioObjectType, int sampleRateIndex, + int channelConfig) { + byte[] audioSpecificConfig = new byte[2]; + audioSpecificConfig[0] = (byte) ((audioObjectType << 3) & 0xF8 | (sampleRateIndex >> 1) & 0x07); + audioSpecificConfig[1] = (byte) ((sampleRateIndex << 7) & 0x80 | (channelConfig << 3) & 0x78); + return audioSpecificConfig; + } + + /** + * Builds a simple HE-AAC LC AudioSpecificConfig, as defined in ISO 14496-3 1.6.2.1 + * + * @param sampleRate The sample rate in Hz. + * @param numChannels The number of channels. + * @return The AudioSpecificConfig. + */ + public static byte[] buildAacAudioSpecificConfig(int sampleRate, int numChannels) { + int sampleRateIndex = -1; + for (int i = 0; i < AUDIO_SPECIFIC_CONFIG_SAMPLING_RATE_TABLE.length; ++i) { + if (sampleRate == AUDIO_SPECIFIC_CONFIG_SAMPLING_RATE_TABLE[i]) { + sampleRateIndex = i; + } + } + int channelConfig = -1; + for (int i = 0; i < AUDIO_SPECIFIC_CONFIG_CHANNEL_COUNT_TABLE.length; ++i) { + if (numChannels == AUDIO_SPECIFIC_CONFIG_CHANNEL_COUNT_TABLE[i]) { + channelConfig = i; + } + } + // The full specification for AudioSpecificConfig is stated in ISO 14496-3 Section 1.6.2.1 + byte[] csd = new byte[2]; + csd[0] = (byte) ((AUDIO_OBJECT_TYPE_AAC_LC << 3) | (sampleRateIndex >> 1)); + csd[1] = (byte) (((sampleRateIndex & 0x1) << 7) | (channelConfig << 3)); + return csd; + } + + /** + * Constructs a NAL unit consisting of the NAL start code followed by the specified data. + * + * @param data An array containing the data that should follow the NAL start code. + * @param offset The start offset into {@code data}. + * @param length The number of bytes to copy from {@code data} + * @return The constructed NAL unit. + */ + public static byte[] buildNalUnit(byte[] data, int offset, int length) { + byte[] nalUnit = new byte[length + NAL_START_CODE.length]; + System.arraycopy(NAL_START_CODE, 0, nalUnit, 0, NAL_START_CODE.length); + System.arraycopy(data, offset, nalUnit, NAL_START_CODE.length, length); + return nalUnit; + } + + /** + * Splits an array of NAL units. + *

+ * If the input consists of NAL start code delimited units, then the returned array consists of + * the split NAL units, each of which is still prefixed with the NAL start code. For any other + * input, null is returned. + * + * @param data An array of data. + * @return The individual NAL units, or null if the input did not consist of NAL start code + * delimited units. + */ + public static byte[][] splitNalUnits(byte[] data) { + if (!isNalStartCode(data, 0)) { + // data does not consist of NAL start code delimited units. + return null; + } + List starts = new ArrayList<>(); + int nalUnitIndex = 0; + do { + starts.add(nalUnitIndex); + nalUnitIndex = findNalStartCode(data, nalUnitIndex + NAL_START_CODE.length); + } while (nalUnitIndex != -1); + byte[][] split = new byte[starts.size()][]; + for (int i = 0; i < starts.size(); i++) { + int startIndex = starts.get(i); + int endIndex = i < starts.size() - 1 ? starts.get(i + 1) : data.length; + byte[] nal = new byte[endIndex - startIndex]; + System.arraycopy(data, startIndex, nal, 0, nal.length); + split[i] = nal; + } + return split; + } + + /** + * Finds the next occurrence of the NAL start code from a given index. + * + * @param data The data in which to search. + * @param index The first index to test. + * @return The index of the first byte of the found start code, or -1. + */ + private static int findNalStartCode(byte[] data, int index) { + int endIndex = data.length - NAL_START_CODE.length; + for (int i = index; i <= endIndex; i++) { + if (isNalStartCode(data, i)) { + return i; + } + } + return -1; + } + + /** + * Tests whether there exists a NAL start code at a given index. + * + * @param data The data. + * @param index The index to test. + * @return Whether there exists a start code that begins at {@code index}. + */ + private static boolean isNalStartCode(byte[] data, int index) { + if (data.length - index <= NAL_START_CODE.length) { + return false; + } + for (int j = 0; j < NAL_START_CODE.length; j++) { + if (data[index + j] != NAL_START_CODE[j]) { + return false; + } + } + return true; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/DebugTextViewHelper.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/DebugTextViewHelper.java new file mode 100755 index 000000000..ffe97f954 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/DebugTextViewHelper.java @@ -0,0 +1,125 @@ +/* + * 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.messenger.exoplayer.util; + +import org.telegram.messenger.exoplayer.CodecCounters; +import org.telegram.messenger.exoplayer.chunk.Format; +import org.telegram.messenger.exoplayer.upstream.BandwidthMeter; + +import android.widget.TextView; + +/** + * A helper class for periodically updating debug information displayed by a {@link TextView}. + */ +public final class DebugTextViewHelper implements Runnable { + + /** + * Provides debug information about an ongoing playback. + */ + public interface Provider { + + /** + * Returns the current playback position, in milliseconds. + */ + long getCurrentPosition(); + + /** + * Returns a format whose information should be displayed, or null. + */ + Format getFormat(); + + /** + * Returns a {@link BandwidthMeter} whose estimate should be displayed, or null. + */ + BandwidthMeter getBandwidthMeter(); + + /** + * Returns a {@link CodecCounters} whose information should be displayed, or null. + */ + CodecCounters getCodecCounters(); + + } + + private static final int REFRESH_INTERVAL_MS = 1000; + + private final TextView textView; + private final Provider debuggable; + + /** + * @param debuggable The {@link Provider} from which debug information should be obtained. + * @param textView The {@link TextView} that should be updated to display the information. + */ + public DebugTextViewHelper(Provider debuggable, TextView textView) { + this.debuggable = debuggable; + this.textView = textView; + } + + /** + * Starts periodic updates of the {@link TextView}. + *

+ * Should be called from the application's main thread. + */ + public void start() { + stop(); + run(); + } + + /** + * Stops periodic updates of the {@link TextView}. + *

+ * Should be called from the application's main thread. + */ + public void stop() { + textView.removeCallbacks(this); + } + + @Override + public void run() { + textView.setText(getRenderString()); + textView.postDelayed(this, REFRESH_INTERVAL_MS); + } + + private String getRenderString() { + return getTimeString() + " " + getQualityString() + " " + getBandwidthString() + " " + + getVideoCodecCountersString(); + } + + private String getTimeString() { + return "ms(" + debuggable.getCurrentPosition() + ")"; + } + + private String getQualityString() { + Format format = debuggable.getFormat(); + return format == null ? "id:? br:? h:?" + : "id:" + format.id + " br:" + format.bitrate + " h:" + format.height; + } + + private String getBandwidthString() { + BandwidthMeter bandwidthMeter = debuggable.getBandwidthMeter(); + if (bandwidthMeter == null + || bandwidthMeter.getBitrateEstimate() == BandwidthMeter.NO_ESTIMATE) { + return "bw:?"; + } else { + return "bw:" + (bandwidthMeter.getBitrateEstimate() / 1000); + } + } + + private String getVideoCodecCountersString() { + CodecCounters codecCounters = debuggable.getCodecCounters(); + return codecCounters == null ? "" : codecCounters.getDebugString(); + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/DtsUtil.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/DtsUtil.java new file mode 100755 index 000000000..49f1aa439 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/DtsUtil.java @@ -0,0 +1,116 @@ +/* + * 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.messenger.exoplayer.util; + +import org.telegram.messenger.exoplayer.MediaFormat; + +import java.nio.ByteBuffer; + +/** + * Utility methods for parsing DTS frames. + */ +public final class DtsUtil { + + /** + * Maps AMODE to the number of channels. See ETSI TS 102 114 table 5.4. + */ + private static final int[] CHANNELS_BY_AMODE = new int[] {1, 2, 2, 2, 2, 3, 3, 4, 4, 5, 6, 6, 6, + 7, 8, 8}; + + /** + * Maps SFREQ to the sampling frequency in Hz. See ETSI TS 102 144 table 5.5. + */ + private static final int[] SAMPLE_RATE_BY_SFREQ = new int[] {-1, 8000, 16000, 32000, -1, -1, + 11025, 22050, 44100, -1, -1, 12000, 24000, 48000, -1, -1}; + + /** + * Maps RATE to 2 * bitrate in kbit/s. See ETSI TS 102 144 table 5.7. + */ + private static final int[] TWICE_BITRATE_KBPS_BY_RATE = new int[] {64, 112, 128, 192, 224, 256, + 384, 448, 512, 640, 768, 896, 1024, 1152, 1280, 1536, 1920, 2048, 2304, 2560, 2688, 2816, + 2823, 2944, 3072, 3840, 4096, 6144, 7680}; + + private static final ParsableBitArray SCRATCH_BITS = new ParsableBitArray(); + + /** + * Returns the DTS format given {@code data} containing the DTS frame according to ETSI TS 102 114 + * subsections 5.3/5.4. + *

+ * This method may only be called from one thread at a time. + * + * @param frame The DTS frame to parse. + * @param trackId The track identifier to set on the format, or null. + * @param durationUs The duration to set on the format, in microseconds. + * @param language The language to set on the format. + * @return The DTS format parsed from data in the header. + */ + public static MediaFormat parseDtsFormat(byte[] frame, String trackId, long durationUs, + String language) { + ParsableBitArray frameBits = SCRATCH_BITS; + frameBits.reset(frame); + frameBits.skipBits(4 * 8 + 1 + 5 + 1 + 7 + 14); // SYNC, FTYPE, SHORT, CPF, NBLKS, FSIZE + int amode = frameBits.readBits(6); + int channelCount = CHANNELS_BY_AMODE[amode]; + int sfreq = frameBits.readBits(4); + int sampleRate = SAMPLE_RATE_BY_SFREQ[sfreq]; + int rate = frameBits.readBits(5); + int bitrate = rate >= TWICE_BITRATE_KBPS_BY_RATE.length ? MediaFormat.NO_VALUE + : TWICE_BITRATE_KBPS_BY_RATE[rate] * 1000 / 2; + frameBits.skipBits(10); // MIX, DYNF, TIMEF, AUXF, HDCD, EXT_AUDIO_ID, EXT_AUDIO, ASPF + channelCount += frameBits.readBits(2) > 0 ? 1 : 0; // LFF + return MediaFormat.createAudioFormat(trackId, MimeTypes.AUDIO_DTS, bitrate, + MediaFormat.NO_VALUE, durationUs, channelCount, sampleRate, null, language); + } + + /** + * Returns the number of audio samples represented by the given DTS frame. + * + * @param data The frame to parse. + * @return The number of audio samples represented by the frame. + */ + public static int parseDtsAudioSampleCount(byte[] data) { + // See ETSI TS 102 114 subsection 5.4.1. + int nblks = ((data[4] & 0x01) << 6) | ((data[5] & 0xFC) >> 2); + return (nblks + 1) * 32; + } + + /** + * Like {@link #parseDtsAudioSampleCount(byte[])} but reads from a byte buffer. The buffer + * position is not modified. + */ + public static int parseDtsAudioSampleCount(ByteBuffer data) { + // See ETSI TS 102 114 subsection 5.4.1. + int position = data.position(); + int nblks = ((data.get(position + 4) & 0x01) << 6) + | ((data.get(position + 5) & 0xFC) >> 2); + return (nblks + 1) * 32; + } + + /** + * Returns the size in bytes of the given DTS frame. + * + * @param data The frame to parse. + * @return The frame's size in bytes. + */ + public static int getDtsFrameSize(byte[] data) { + return (((data[5] & 0x02) << 12) + | ((data[6] & 0xFF) << 4) + | ((data[7] & 0xF0) >> 4)) + 1; + } + + private DtsUtil() {} + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/FlacSeekTable.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/FlacSeekTable.java new file mode 100755 index 000000000..13ed1cc7a --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/FlacSeekTable.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2016 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.messenger.exoplayer.util; + +import org.telegram.messenger.exoplayer.extractor.SeekMap; + +/** + * FLAC seek table class + */ +public final class FlacSeekTable { + + private static final int METADATA_LENGTH_OFFSET = 1; + private static final int SEEK_POINT_SIZE = 18; + + private final long[] sampleNumbers; + private final long[] offsets; + + /** + * Parses a FLAC file seek table metadata structure and creates a FlacSeekTable instance. + * + * @param data A ParsableByteArray including whole seek table metadata block. Its position should + * be set to the beginning of the block. + * @return A FlacSeekTable instance keeping seek table data + * @see FLAC format + * METADATA_BLOCK_SEEKTABLE + */ + public static FlacSeekTable parseSeekTable(ParsableByteArray data) { + data.skipBytes(METADATA_LENGTH_OFFSET); + int length = data.readUnsignedInt24(); + int numberOfSeekPoints = length / SEEK_POINT_SIZE; + + long[] sampleNumbers = new long[numberOfSeekPoints]; + long[] offsets = new long[numberOfSeekPoints]; + + for (int i = 0; i < numberOfSeekPoints; i++) { + sampleNumbers[i] = data.readLong(); + offsets[i] = data.readLong(); + data.skipBytes(2); // Skip "Number of samples in the target frame." + } + + return new FlacSeekTable(sampleNumbers, offsets); + } + + private FlacSeekTable(long[] sampleNumbers, long[] offsets) { + this.sampleNumbers = sampleNumbers; + this.offsets = offsets; + } + + /** + * Creates a {@link SeekMap} wrapper for this FlacSeekTable. + * + * @param firstFrameOffset Offset of the first FLAC frame + * @param sampleRate Sample rate of the FLAC file. + * @return A SeekMap wrapper for this FlacSeekTable. + */ + public SeekMap createSeekMap(final long firstFrameOffset, final long sampleRate) { + return new SeekMap() { + @Override + public boolean isSeekable() { + return true; + } + + @Override + public long getPosition(long timeUs) { + long sample = (timeUs * sampleRate) / 1000000L; + + int index = Util.binarySearchFloor(sampleNumbers, sample, true, true); + return firstFrameOffset + offsets[index]; + } + }; + } +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/FlacStreamInfo.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/FlacStreamInfo.java new file mode 100755 index 000000000..cad3e90d1 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/FlacStreamInfo.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2016 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.messenger.exoplayer.util; + +/** + * Holder for FLAC stream info. + */ +public final class FlacStreamInfo { + + public final int minBlockSize; + public final int maxBlockSize; + public final int minFrameSize; + public final int maxFrameSize; + public final int sampleRate; + public final int channels; + public final int bitsPerSample; + public final long totalSamples; + + /** + * Constructs a FlacStreamInfo parsing the given binary FLAC stream info metadata structure. + * + * @param data An array holding FLAC stream info metadata structure + * @param offset Offset of the structure in the array + * @see FLAC format + * METADATA_BLOCK_STREAMINFO + */ + public FlacStreamInfo(byte[] data, int offset) { + ParsableBitArray scratch = new ParsableBitArray(data); + scratch.setPosition(offset * 8); + this.minBlockSize = scratch.readBits(16); + this.maxBlockSize = scratch.readBits(16); + this.minFrameSize = scratch.readBits(24); + this.maxFrameSize = scratch.readBits(24); + this.sampleRate = scratch.readBits(20); + this.channels = scratch.readBits(3) + 1; + this.bitsPerSample = scratch.readBits(5) + 1; + this.totalSamples = scratch.readBits(36); + // Remaining 16 bytes is md5 value + } + + public FlacStreamInfo(int minBlockSize, int maxBlockSize, int minFrameSize, int maxFrameSize, + int sampleRate, int channels, int bitsPerSample, long totalSamples) { + this.minBlockSize = minBlockSize; + this.maxBlockSize = maxBlockSize; + this.minFrameSize = minFrameSize; + this.maxFrameSize = maxFrameSize; + this.sampleRate = sampleRate; + this.channels = channels; + this.bitsPerSample = bitsPerSample; + this.totalSamples = totalSamples; + } + + public int maxDecodedFrameSize() { + return maxBlockSize * channels * 2; + } + + public int bitRate() { + return bitsPerSample * sampleRate; + } + + public long durationUs() { + return (totalSamples * 1000000L) / sampleRate; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/FlacUtil.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/FlacUtil.java new file mode 100755 index 000000000..5a48a2560 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/FlacUtil.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2016 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.messenger.exoplayer.util; + +/** + * Utility functions for FLAC + */ +public final class FlacUtil { + + private static final int FRAME_HEADER_SAMPLE_NUMBER_OFFSET = 4; + + /** + * Prevents initialization. + */ + private FlacUtil() { + } + + /** + * Extracts sample timestamp from the given binary FLAC frame header data structure. + * + * @param streamInfo A {@link FlacStreamInfo} instance + * @param frameData A {@link ParsableByteArray} including binary FLAC frame header data structure. + * Its position should be set to the beginning of the structure. + * @return Sample timestamp + * @see FLAC format FRAME_HEADER + */ + public static long extractSampleTimestamp(FlacStreamInfo streamInfo, + ParsableByteArray frameData) { + frameData.skipBytes(FRAME_HEADER_SAMPLE_NUMBER_OFFSET); + long sampleNumber = frameData.readUTF8EncodedLong(); + if (streamInfo.minBlockSize == streamInfo.maxBlockSize) { + // if fixed block size then sampleNumber is frame number + sampleNumber *= streamInfo.minBlockSize; + } + return (sampleNumber * 1000000L) / streamInfo.sampleRate; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/LongArray.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/LongArray.java new file mode 100755 index 000000000..e93a9b4df --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/LongArray.java @@ -0,0 +1,86 @@ +/* + * 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.messenger.exoplayer.util; + +import java.util.Arrays; + +/** + * An append-only, auto-growing {@code long[]}. + */ +public final class LongArray { + + private static final int DEFAULT_INITIAL_CAPACITY = 32; + + private int size; + private long[] values; + + public LongArray() { + this(DEFAULT_INITIAL_CAPACITY); + } + + /** + * @param initialCapacity The initial capacity of the array. + */ + public LongArray(int initialCapacity) { + values = new long[initialCapacity]; + } + + /** + * Appends a value. + * + * @param value The value to append. + */ + public void add(long value) { + if (size == values.length) { + values = Arrays.copyOf(values, size * 2); + } + values[size++] = value; + } + + /** + * Gets a value. + * + * @param index The index. + * @return The corresponding value. + * @throws IndexOutOfBoundsException If the index is less than zero, or greater than or equal to + * {@link #size()} + */ + public long get(int index) { + if (index < 0 || index >= size) { + throw new IndexOutOfBoundsException("Invalid size " + index + ", size is " + size); + } + return values[index]; + } + + /** + * Gets the current size of the array. + * + * @return The current size of the array. + */ + public int size() { + return size; + } + + /** + * Copies the current values into a newly allocated primitive array. + * + * @return The primitive array containing the copied values. + */ + public long[] toArray() { + return Arrays.copyOf(values, size); + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/ManifestFetcher.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/ManifestFetcher.java new file mode 100755 index 000000000..365269476 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/ManifestFetcher.java @@ -0,0 +1,410 @@ +/* + * 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.messenger.exoplayer.util; + +import org.telegram.messenger.exoplayer.upstream.Loader; +import org.telegram.messenger.exoplayer.upstream.Loader.Loadable; +import org.telegram.messenger.exoplayer.upstream.UriDataSource; +import org.telegram.messenger.exoplayer.upstream.UriLoadable; + +import android.os.Handler; +import android.os.Looper; +import android.os.SystemClock; +import android.text.TextUtils; +import android.util.Pair; + +import java.io.IOException; +import java.util.concurrent.CancellationException; + +/** + * Performs both single and repeated loads of media manifests. + *

+ * Client code is responsible for ensuring that only one load is taking place at any one time. + * Typical usage of this class is as follows: + *

    + *
  1. Create an instance.
  2. + *
  3. Obtain an initial manifest by calling {@link #singleLoad(Looper, ManifestCallback)} and + * waiting for the callback to be invoked.
  4. + *
  5. For on-demand playbacks, the loader is no longer required. For live playbacks, the loader + * may be required to periodically refresh the manifest. In this case it is injected into any + * components that require it. These components will call {@link #requestRefresh()} on the + * loader whenever a refresh is required.
  6. + *
+ * + * @param The type of manifest. + */ +public class ManifestFetcher implements Loader.Callback { + + /** + * Thrown when an error occurs trying to fetch a manifest. + */ + public static final class ManifestIOException extends IOException{ + public ManifestIOException(Throwable cause) { super(cause); } + + } + + /** + * Interface definition for a callback to be notified of {@link ManifestFetcher} events. + */ + public interface EventListener { + + public void onManifestRefreshStarted(); + + public void onManifestRefreshed(); + + public void onManifestError(IOException e); + + } + + /** + * Callback for the result of a single load. + * + * @param The type of manifest. + */ + public interface ManifestCallback { + + /** + * Invoked when the load has successfully completed. + * + * @param manifest The loaded manifest. + */ + void onSingleManifest(T manifest); + + /** + * Invoked when the load has failed. + * + * @param e The cause of the failure. + */ + void onSingleManifestError(IOException e); + + } + + /** + * Interface for manifests that are able to specify that subsequent loads should use a different + * URI. + */ + public interface RedirectingManifest { + + /** + * Returns the URI from which subsequent manifests should be requested, or null to continue + * using the current URI. + */ + public String getNextManifestUri(); + + } + + private final UriLoadable.Parser parser; + private final UriDataSource uriDataSource; + private final Handler eventHandler; + private final EventListener eventListener; + + /* package */ volatile String manifestUri; + + private int enabledCount; + private Loader loader; + private UriLoadable currentLoadable; + private long currentLoadStartTimestamp; + + private int loadExceptionCount; + private long loadExceptionTimestamp; + private ManifestIOException loadException; + + private volatile T manifest; + private volatile long manifestLoadStartTimestamp; + private volatile long manifestLoadCompleteTimestamp; + + /** + * @param manifestUri The manifest location. + * @param uriDataSource The {@link UriDataSource} to use when loading the manifest. + * @param parser A parser to parse the loaded manifest data. + */ + public ManifestFetcher(String manifestUri, UriDataSource uriDataSource, + UriLoadable.Parser parser) { + this(manifestUri, uriDataSource, parser, null, null); + } + + /** + * @param manifestUri The manifest location. + * @param uriDataSource The {@link UriDataSource} to use when loading the manifest. + * @param parser A parser to parse the loaded manifest data. + * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be + * null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + */ + public ManifestFetcher(String manifestUri, UriDataSource uriDataSource, + UriLoadable.Parser parser, Handler eventHandler, EventListener eventListener) { + this.parser = parser; + this.manifestUri = manifestUri; + this.uriDataSource = uriDataSource; + this.eventHandler = eventHandler; + this.eventListener = eventListener; + } + + /** + * Updates the manifest location. + * + * @param manifestUri The manifest location. + */ + public void updateManifestUri(String manifestUri) { + this.manifestUri = manifestUri; + } + + /** + * Performs a single manifest load. + * + * @param callbackLooper The looper associated with the thread on which the callback should be + * invoked. + * @param callback The callback to receive the result. + */ + public void singleLoad(Looper callbackLooper, final ManifestCallback callback) { + SingleFetchHelper fetchHelper = new SingleFetchHelper( + new UriLoadable<>(manifestUri, uriDataSource, parser), callbackLooper, callback); + fetchHelper.startLoading(); + } + + /** + * Gets a {@link Pair} containing the most recently loaded manifest together with the timestamp + * at which the load completed. + * + * @return The most recently loaded manifest and the timestamp at which the load completed, or + * null if no manifest has loaded. + */ + public T getManifest() { + return manifest; + } + + /** + * Gets the value of {@link SystemClock#elapsedRealtime()} when the last completed load started. + * + * @return The value of {@link SystemClock#elapsedRealtime()} when the last completed load + * started. + */ + public long getManifestLoadStartTimestamp() { + return manifestLoadStartTimestamp; + } + + /** + * Gets the value of {@link SystemClock#elapsedRealtime()} when the last load completed. + * + * @return The value of {@link SystemClock#elapsedRealtime()} when the last load completed. + */ + public long getManifestLoadCompleteTimestamp() { + return manifestLoadCompleteTimestamp; + } + + /** + * Throws the error that affected the most recent attempt to load the manifest. Does nothing if + * the most recent attempt was successful. + * + * @throws ManifestIOException The error that affected the most recent attempt to load the + * manifest. + */ + public void maybeThrowError() throws ManifestIOException { + // Don't throw an exception until at least 1 retry attempt has been made. + if (loadException == null || loadExceptionCount <= 1) { + return; + } + throw loadException; + } + + /** + * Enables refresh functionality. + */ + public void enable() { + if (enabledCount++ == 0) { + loadExceptionCount = 0; + loadException = null; + } + } + + /** + * Disables refresh functionality. + */ + public void disable() { + if (--enabledCount == 0) { + if (loader != null) { + loader.release(); + loader = null; + } + } + } + + /** + * Should be invoked repeatedly by callers who require an updated manifest. + */ + public void requestRefresh() { + if (loadException != null && SystemClock.elapsedRealtime() + < (loadExceptionTimestamp + getRetryDelayMillis(loadExceptionCount))) { + // The previous load failed, and it's too soon to try again. + return; + } + if (loader == null) { + loader = new Loader("manifestLoader"); + } + if (!loader.isLoading()) { + currentLoadable = new UriLoadable<>(manifestUri, uriDataSource, parser); + currentLoadStartTimestamp = SystemClock.elapsedRealtime(); + loader.startLoading(currentLoadable, this); + notifyManifestRefreshStarted(); + } + } + + @Override + public void onLoadCompleted(Loadable loadable) { + if (currentLoadable != loadable) { + // Stale event. + return; + } + + manifest = currentLoadable.getResult(); + manifestLoadStartTimestamp = currentLoadStartTimestamp; + manifestLoadCompleteTimestamp = SystemClock.elapsedRealtime(); + loadExceptionCount = 0; + loadException = null; + + if (manifest instanceof RedirectingManifest) { + RedirectingManifest redirectingManifest = (RedirectingManifest) manifest; + String nextLocation = redirectingManifest.getNextManifestUri(); + if (!TextUtils.isEmpty(nextLocation)) { + manifestUri = nextLocation; + } + } + + notifyManifestRefreshed(); + } + + @Override + public void onLoadCanceled(Loadable loadable) { + // Do nothing. + } + + @Override + public void onLoadError(Loadable loadable, IOException exception) { + if (currentLoadable != loadable) { + // Stale event. + return; + } + + loadExceptionCount++; + loadExceptionTimestamp = SystemClock.elapsedRealtime(); + loadException = new ManifestIOException(exception); + + notifyManifestError(loadException); + } + + /* package */ void onSingleFetchCompleted(T result, long loadStartTimestamp) { + manifest = result; + manifestLoadStartTimestamp = loadStartTimestamp; + manifestLoadCompleteTimestamp = SystemClock.elapsedRealtime(); + } + + private long getRetryDelayMillis(long errorCount) { + return Math.min((errorCount - 1) * 1000, 5000); + } + + private void notifyManifestRefreshStarted() { + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onManifestRefreshStarted(); + } + }); + } + } + + private void notifyManifestRefreshed() { + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onManifestRefreshed(); + } + }); + } + } + + private void notifyManifestError(final IOException e) { + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onManifestError(e); + } + }); + } + } + + private class SingleFetchHelper implements Loader.Callback { + + private final UriLoadable singleUseLoadable; + private final Looper callbackLooper; + private final ManifestCallback wrappedCallback; + private final Loader singleUseLoader; + + private long loadStartTimestamp; + + public SingleFetchHelper(UriLoadable singleUseLoadable, Looper callbackLooper, + ManifestCallback wrappedCallback) { + this.singleUseLoadable = singleUseLoadable; + this.callbackLooper = callbackLooper; + this.wrappedCallback = wrappedCallback; + singleUseLoader = new Loader("manifestLoader:single"); + } + + public void startLoading() { + loadStartTimestamp = SystemClock.elapsedRealtime(); + singleUseLoader.startLoading(callbackLooper, singleUseLoadable, this); + } + + @Override + public void onLoadCompleted(Loadable loadable) { + try { + T result = singleUseLoadable.getResult(); + onSingleFetchCompleted(result, loadStartTimestamp); + wrappedCallback.onSingleManifest(result); + } finally { + releaseLoader(); + } + } + + @Override + public void onLoadCanceled(Loadable loadable) { + // This shouldn't ever happen, but handle it anyway. + try { + IOException exception = new ManifestIOException(new CancellationException()); + wrappedCallback.onSingleManifestError(exception); + } finally { + releaseLoader(); + } + } + + @Override + public void onLoadError(Loadable loadable, IOException exception) { + try { + wrappedCallback.onSingleManifestError(exception); + } finally { + releaseLoader(); + } + } + + private void releaseLoader() { + singleUseLoader.release(); + } + + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/MimeTypes.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/MimeTypes.java new file mode 100755 index 000000000..a0ce75379 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/MimeTypes.java @@ -0,0 +1,191 @@ +/* + * 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.messenger.exoplayer.util; + +/** + * Defines common MIME types and helper methods. + */ +public final class MimeTypes { + + public static final String BASE_TYPE_VIDEO = "video"; + public static final String BASE_TYPE_AUDIO = "audio"; + public static final String BASE_TYPE_TEXT = "text"; + public static final String BASE_TYPE_APPLICATION = "application"; + + public static final String VIDEO_UNKNOWN = BASE_TYPE_VIDEO + "/x-unknown"; + public static final String VIDEO_MP4 = BASE_TYPE_VIDEO + "/mp4"; + public static final String VIDEO_WEBM = BASE_TYPE_VIDEO + "/webm"; + public static final String VIDEO_H263 = BASE_TYPE_VIDEO + "/3gpp"; + public static final String VIDEO_H264 = BASE_TYPE_VIDEO + "/avc"; + public static final String VIDEO_H265 = BASE_TYPE_VIDEO + "/hevc"; + public static final String VIDEO_VP8 = BASE_TYPE_VIDEO + "/x-vnd.on2.vp8"; + public static final String VIDEO_VP9 = BASE_TYPE_VIDEO + "/x-vnd.on2.vp9"; + public static final String VIDEO_MP4V = BASE_TYPE_VIDEO + "/mp4v-es"; + public static final String VIDEO_MPEG2 = BASE_TYPE_VIDEO + "/mpeg2"; + public static final String VIDEO_VC1 = BASE_TYPE_VIDEO + "/wvc1"; + + public static final String AUDIO_UNKNOWN = BASE_TYPE_AUDIO + "/x-unknown"; + public static final String AUDIO_MP4 = BASE_TYPE_AUDIO + "/mp4"; + public static final String AUDIO_AAC = BASE_TYPE_AUDIO + "/mp4a-latm"; + public static final String AUDIO_WEBM = BASE_TYPE_AUDIO + "/webm"; + public static final String AUDIO_MPEG = BASE_TYPE_AUDIO + "/mpeg"; + public static final String AUDIO_MPEG_L1 = BASE_TYPE_AUDIO + "/mpeg-L1"; + public static final String AUDIO_MPEG_L2 = BASE_TYPE_AUDIO + "/mpeg-L2"; + public static final String AUDIO_RAW = BASE_TYPE_AUDIO + "/raw"; + public static final String AUDIO_AC3 = BASE_TYPE_AUDIO + "/ac3"; + public static final String AUDIO_E_AC3 = BASE_TYPE_AUDIO + "/eac3"; + public static final String AUDIO_TRUEHD = BASE_TYPE_AUDIO + "/true-hd"; + public static final String AUDIO_DTS = BASE_TYPE_AUDIO + "/vnd.dts"; + public static final String AUDIO_DTS_HD = BASE_TYPE_AUDIO + "/vnd.dts.hd"; + public static final String AUDIO_DTS_EXPRESS = BASE_TYPE_AUDIO + "/vnd.dts.hd;profile=lbr"; + public static final String AUDIO_VORBIS = BASE_TYPE_AUDIO + "/vorbis"; + public static final String AUDIO_OPUS = BASE_TYPE_AUDIO + "/opus"; + public static final String AUDIO_AMR_NB = BASE_TYPE_AUDIO + "/3gpp"; + public static final String AUDIO_AMR_WB = BASE_TYPE_AUDIO + "/amr-wb"; + public static final String AUDIO_FLAC = BASE_TYPE_AUDIO + "/x-flac"; + + public static final String TEXT_UNKNOWN = BASE_TYPE_TEXT + "/x-unknown"; + public static final String TEXT_VTT = BASE_TYPE_TEXT + "/vtt"; + + public static final String APPLICATION_MP4 = BASE_TYPE_APPLICATION + "/mp4"; + public static final String APPLICATION_WEBM = BASE_TYPE_APPLICATION + "/webm"; + public static final String APPLICATION_ID3 = BASE_TYPE_APPLICATION + "/id3"; + public static final String APPLICATION_EIA608 = BASE_TYPE_APPLICATION + "/eia-608"; + public static final String APPLICATION_SUBRIP = BASE_TYPE_APPLICATION + "/x-subrip"; + public static final String APPLICATION_TTML = BASE_TYPE_APPLICATION + "/ttml+xml"; + public static final String APPLICATION_M3U8 = BASE_TYPE_APPLICATION + "/x-mpegURL"; + public static final String APPLICATION_TX3G = BASE_TYPE_APPLICATION + "/x-quicktime-tx3g"; + public static final String APPLICATION_MP4VTT = BASE_TYPE_APPLICATION + "/x-mp4vtt"; + public static final String APPLICATION_VOBSUB = BASE_TYPE_APPLICATION + "/vobsub"; + public static final String APPLICATION_PGS = BASE_TYPE_APPLICATION + "/pgs"; + + private MimeTypes() {} + + /** + * Whether the top-level type of {@code mimeType} is audio. + * + * @param mimeType The mimeType to test. + * @return Whether the top level type is audio. + */ + public static boolean isAudio(String mimeType) { + return getTopLevelType(mimeType).equals(BASE_TYPE_AUDIO); + } + + /** + * Whether the top-level type of {@code mimeType} is video. + * + * @param mimeType The mimeType to test. + * @return Whether the top level type is video. + */ + public static boolean isVideo(String mimeType) { + return getTopLevelType(mimeType).equals(BASE_TYPE_VIDEO); + } + + /** + * Whether the top-level type of {@code mimeType} is text. + * + * @param mimeType The mimeType to test. + * @return Whether the top level type is text. + */ + public static boolean isText(String mimeType) { + return getTopLevelType(mimeType).equals(BASE_TYPE_TEXT); + } + + /** + * Whether the top-level type of {@code mimeType} is application. + * + * @param mimeType The mimeType to test. + * @return Whether the top level type is application. + */ + public static boolean isApplication(String mimeType) { + return getTopLevelType(mimeType).equals(BASE_TYPE_APPLICATION); + } + + /** + * Returns the top-level type of {@code mimeType}. + * + * @param mimeType The mimeType whose top-level type is required. + * @return The top-level type. + */ + private static String getTopLevelType(String mimeType) { + int indexOfSlash = mimeType.indexOf('/'); + if (indexOfSlash == -1) { + throw new IllegalArgumentException("Invalid mime type: " + mimeType); + } + return mimeType.substring(0, indexOfSlash); + } + + /** + * Returns the video mimeType type of {@code codecs}. + * + * @param codecs The codecs for which the video mimeType is required. + * @return The video mimeType. + */ + public static String getVideoMediaMimeType(String codecs) { + if (codecs == null) { + return MimeTypes.VIDEO_UNKNOWN; + } + String[] codecList = codecs.split(","); + for (String codec : codecList) { + codec = codec.trim(); + if (codec.startsWith("avc1") || codec.startsWith("avc3")) { + return MimeTypes.VIDEO_H264; + } else if (codec.startsWith("hev1") || codec.startsWith("hvc1")) { + return MimeTypes.VIDEO_H265; + } else if (codec.startsWith("vp9")) { + return MimeTypes.VIDEO_VP9; + } else if (codec.startsWith("vp8")) { + return MimeTypes.VIDEO_VP8; + } + } + return MimeTypes.VIDEO_UNKNOWN; + } + + /** + * Returns the audio mimeType type of {@code codecs}. + * + * @param codecs The codecs for which the audio mimeType is required. + * @return The audio mimeType. + */ + public static String getAudioMediaMimeType(String codecs) { + if (codecs == null) { + return MimeTypes.AUDIO_UNKNOWN; + } + String[] codecList = codecs.split(","); + for (String codec : codecList) { + codec = codec.trim(); + if (codec.startsWith("mp4a")) { + return MimeTypes.AUDIO_AAC; + } else if (codec.startsWith("ac-3") || codec.startsWith("dac3")) { + return MimeTypes.AUDIO_AC3; + } else if (codec.startsWith("ec-3") || codec.startsWith("dec3")) { + return MimeTypes.AUDIO_E_AC3; + } else if (codec.startsWith("dtsc")) { + return MimeTypes.AUDIO_DTS; + } else if (codec.startsWith("dtsh") || codec.startsWith("dtsl")) { + return MimeTypes.AUDIO_DTS_HD; + } else if (codec.startsWith("dtse")) { + return MimeTypes.AUDIO_DTS_EXPRESS; + } else if (codec.startsWith("opus")) { + return MimeTypes.AUDIO_OPUS; + } else if (codec.startsWith("vorbis")) { + return MimeTypes.AUDIO_VORBIS; + } + } + return MimeTypes.AUDIO_UNKNOWN; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/MpegAudioHeader.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/MpegAudioHeader.java new file mode 100755 index 000000000..af41179b6 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/MpegAudioHeader.java @@ -0,0 +1,206 @@ +/* + * 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.messenger.exoplayer.util; + +/** + * Representation of an MPEG audio frame header. + */ +public final class MpegAudioHeader { + + /** + * Theoretical maximum frame size for an MPEG audio stream, which occurs when playing a Layer 2 + * MPEG 2.5 audio stream at 16 kb/s (with padding). The size is 1152 sample/frame * + * 160000 bit/s / (8000 sample/s * 8 bit/byte) + 1 padding byte/frame = 2881 byte/frame. + * The next power of two size is 4 KiB. + */ + public static final int MAX_FRAME_SIZE_BYTES = 4096; + + private static final String[] MIME_TYPE_BY_LAYER = + new String[] {MimeTypes.AUDIO_MPEG_L1, MimeTypes.AUDIO_MPEG_L2, MimeTypes.AUDIO_MPEG}; + private static final int[] SAMPLING_RATE_V1 = {44100, 48000, 32000}; + private static final int[] BITRATE_V1_L1 = + {32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448}; + private static final int[] BITRATE_V2_L1 = + {32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256}; + private static final int[] BITRATE_V1_L2 = + {32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384}; + private static final int[] BITRATE_V1_L3 = + {32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320}; + private static final int[] BITRATE_V2 = + {8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160}; + + /** + * Returns the size of the frame associated with {@code header}, or -1 if it is invalid. + */ + public static int getFrameSize(int header) { + if ((header & 0xFFE00000) != 0xFFE00000) { + return -1; + } + + int version = (header >>> 19) & 3; + if (version == 1) { + return -1; + } + + int layer = (header >>> 17) & 3; + if (layer == 0) { + return -1; + } + + int bitrateIndex = (header >>> 12) & 15; + if (bitrateIndex == 0 || bitrateIndex == 0xF) { + // Disallow "free" bitrate. + return -1; + } + + int samplingRateIndex = (header >>> 10) & 3; + if (samplingRateIndex == 3) { + return -1; + } + + int samplingRate = SAMPLING_RATE_V1[samplingRateIndex]; + if (version == 2) { + // Version 2 + samplingRate /= 2; + } else if (version == 0) { + // Version 2.5 + samplingRate /= 4; + } + + int bitrate; + int padding = (header >>> 9) & 1; + if (layer == 3) { + // Layer I (layer == 3) + bitrate = version == 3 ? BITRATE_V1_L1[bitrateIndex - 1] : BITRATE_V2_L1[bitrateIndex - 1]; + return (12000 * bitrate / samplingRate + padding) * 4; + } else { + // Layer II (layer == 2) or III (layer == 1) + if (version == 3) { + bitrate = layer == 2 ? BITRATE_V1_L2[bitrateIndex - 1] : BITRATE_V1_L3[bitrateIndex - 1]; + } else { + // Version 2 or 2.5. + bitrate = BITRATE_V2[bitrateIndex - 1]; + } + } + + if (version == 3) { + // Version 1 + return 144000 * bitrate / samplingRate + padding; + } else { + // Version 2 or 2.5 + return (layer == 1 ? 72000 : 144000) * bitrate / samplingRate + padding; + } + } + + /** + * Parses {@code headerData}, populating {@code header} with the parsed data. + * + * @param headerData Header data to parse. + * @param header Header to populate with data from {@code headerData}. + * @return True if the header was populated. False otherwise, indicating that {@code headerData} + * is not a valid MPEG audio header. + */ + public static boolean populateHeader(int headerData, MpegAudioHeader header) { + if ((headerData & 0xFFE00000) != 0xFFE00000) { + return false; + } + + int version = (headerData >>> 19) & 3; + if (version == 1) { + return false; + } + + int layer = (headerData >>> 17) & 3; + if (layer == 0) { + return false; + } + + int bitrateIndex = (headerData >>> 12) & 15; + if (bitrateIndex == 0 || bitrateIndex == 0xF) { + // Disallow "free" bitrate. + return false; + } + + int samplingRateIndex = (headerData >>> 10) & 3; + if (samplingRateIndex == 3) { + return false; + } + + int sampleRate = SAMPLING_RATE_V1[samplingRateIndex]; + if (version == 2) { + // Version 2 + sampleRate /= 2; + } else if (version == 0) { + // Version 2.5 + sampleRate /= 4; + } + + int padding = (headerData >>> 9) & 1; + int bitrate, frameSize, samplesPerFrame; + if (layer == 3) { + // Layer I (layer == 3) + bitrate = version == 3 ? BITRATE_V1_L1[bitrateIndex - 1] : BITRATE_V2_L1[bitrateIndex - 1]; + frameSize = (12000 * bitrate / sampleRate + padding) * 4; + samplesPerFrame = 384; + } else { + // Layer II (layer == 2) or III (layer == 1) + if (version == 3) { + // Version 1 + bitrate = layer == 2 ? BITRATE_V1_L2[bitrateIndex - 1] : BITRATE_V1_L3[bitrateIndex - 1]; + samplesPerFrame = 1152; + frameSize = 144000 * bitrate / sampleRate + padding; + } else { + // Version 2 or 2.5. + bitrate = BITRATE_V2[bitrateIndex - 1]; + samplesPerFrame = layer == 1 ? 576 : 1152; + frameSize = (layer == 1 ? 72000 : 144000) * bitrate / sampleRate + padding; + } + } + + String mimeType = MIME_TYPE_BY_LAYER[3 - layer]; + int channels = ((headerData >> 6) & 3) == 3 ? 1 : 2; + header.setValues(version, mimeType, frameSize, sampleRate, channels, bitrate * 1000, + samplesPerFrame); + return true; + } + + /** MPEG audio header version. */ + public int version; + /** The mime type. */ + public String mimeType; + /** Size of the frame associated with this header, in bytes. */ + public int frameSize; + /** Sample rate in samples per second. */ + public int sampleRate; + /** Number of audio channels in the frame. */ + public int channels; + /** Bitrate of the frame in bit/s. */ + public int bitrate; + /** Number of samples stored in the frame. */ + public int samplesPerFrame; + + private void setValues(int version, String mimeType, int frameSize, int sampleRate, int channels, + int bitrate, int samplesPerFrame) { + this.version = version; + this.mimeType = mimeType; + this.frameSize = frameSize; + this.sampleRate = sampleRate; + this.channels = channels; + this.bitrate = bitrate; + this.samplesPerFrame = samplesPerFrame; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/NalUnitUtil.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/NalUnitUtil.java new file mode 100755 index 000000000..e2ab1f401 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/NalUnitUtil.java @@ -0,0 +1,478 @@ +/* + * 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.messenger.exoplayer.util; + +import android.util.Log; + +import java.nio.ByteBuffer; +import java.util.Arrays; + +/** + * Utility methods for handling H.264/AVC and H.265/HEVC NAL units. + */ +public final class NalUnitUtil { + + private static final String TAG = "NalUnitUtil"; + + /** + * Holds data parsed from a sequence parameter set NAL unit. + */ + public static final class SpsData { + + public final int seqParameterSetId; + public final int width; + public final int height; + public final float pixelWidthAspectRatio; + public final boolean separateColorPlaneFlag; + public final boolean frameMbsOnlyFlag; + public final int frameNumLength; + public final int picOrderCountType; + public final int picOrderCntLsbLength; + public final boolean deltaPicOrderAlwaysZeroFlag; + + public SpsData(int seqParameterSetId, int width, int height, float pixelWidthAspectRatio, + boolean separateColorPlaneFlag, boolean frameMbsOnlyFlag, int frameNumLength, + int picOrderCountType, int picOrderCntLsbLength, boolean deltaPicOrderAlwaysZeroFlag) { + this.seqParameterSetId = seqParameterSetId; + this.width = width; + this.height = height; + this.pixelWidthAspectRatio = pixelWidthAspectRatio; + this.separateColorPlaneFlag = separateColorPlaneFlag; + this.frameMbsOnlyFlag = frameMbsOnlyFlag; + this.frameNumLength = frameNumLength; + this.picOrderCountType = picOrderCountType; + this.picOrderCntLsbLength = picOrderCntLsbLength; + this.deltaPicOrderAlwaysZeroFlag = deltaPicOrderAlwaysZeroFlag; + } + + } + + /** + * Holds data parsed from a picture parameter set NAL unit. + */ + public static final class PpsData { + + public final int picParameterSetId; + public final int seqParameterSetId; + public final boolean bottomFieldPicOrderInFramePresentFlag; + + public PpsData(int picParameterSetId, int seqParameterSetId, + boolean bottomFieldPicOrderInFramePresentFlag) { + this.picParameterSetId = picParameterSetId; + this.seqParameterSetId = seqParameterSetId; + this.bottomFieldPicOrderInFramePresentFlag = bottomFieldPicOrderInFramePresentFlag; + } + + } + + /** Four initial bytes that must prefix NAL units for decoding. */ + public static final byte[] NAL_START_CODE = new byte[] {0, 0, 0, 1}; + + /** Value for aspect_ratio_idc indicating an extended aspect ratio, in H.264 and H.265 SPSs. */ + public static final int EXTENDED_SAR = 0xFF; + /** Aspect ratios indexed by aspect_ratio_idc, in H.264 and H.265 SPSs. */ + public static final float[] ASPECT_RATIO_IDC_VALUES = new float[] { + 1f /* Unspecified. Assume square */, + 1f, + 12f / 11f, + 10f / 11f, + 16f / 11f, + 40f / 33f, + 24f / 11f, + 20f / 11f, + 32f / 11f, + 80f / 33f, + 18f / 11f, + 15f / 11f, + 64f / 33f, + 160f / 99f, + 4f / 3f, + 3f / 2f, + 2f + }; + + private static final int NAL_UNIT_TYPE_SPS = 7; + + private static final Object scratchEscapePositionsLock = new Object(); + + /** + * Temporary store for positions of escape codes in {@link #unescapeStream(byte[], int)}. Guarded + * by {@link #scratchEscapePositionsLock}. + */ + private static int[] scratchEscapePositions = new int[10]; + + /** + * Unescapes {@code data} up to the specified limit, replacing occurrences of [0, 0, 3] with + * [0, 0]. The unescaped data is returned in-place, with the return value indicating its length. + *

+ * Executions of this method are mutually exclusive, so it should not be called with very large + * buffers. + * + * @param data The data to unescape. + * @param limit The limit (exclusive) of the data to unescape. + * @return The length of the unescaped data. + */ + public static int unescapeStream(byte[] data, int limit) { + synchronized (scratchEscapePositionsLock) { + int position = 0; + int scratchEscapeCount = 0; + while (position < limit) { + position = findNextUnescapeIndex(data, position, limit); + if (position < limit) { + if (scratchEscapePositions.length <= scratchEscapeCount) { + // Grow scratchEscapePositions to hold a larger number of positions. + scratchEscapePositions = Arrays.copyOf(scratchEscapePositions, + scratchEscapePositions.length * 2); + } + scratchEscapePositions[scratchEscapeCount++] = position; + position += 3; + } + } + + int unescapedLength = limit - scratchEscapeCount; + int escapedPosition = 0; // The position being read from. + int unescapedPosition = 0; // The position being written to. + for (int i = 0; i < scratchEscapeCount; i++) { + int nextEscapePosition = scratchEscapePositions[i]; + int copyLength = nextEscapePosition - escapedPosition; + System.arraycopy(data, escapedPosition, data, unescapedPosition, copyLength); + unescapedPosition += copyLength; + data[unescapedPosition++] = 0; + data[unescapedPosition++] = 0; + escapedPosition += copyLength + 3; + } + + int remainingLength = unescapedLength - unescapedPosition; + System.arraycopy(data, escapedPosition, data, unescapedPosition, remainingLength); + return unescapedLength; + } + } + + /** + * Discards data from the buffer up to the first SPS, where {@code data.position()} is interpreted + * as the length of the buffer. + *

+ * When the method returns, {@code data.position()} will contain the new length of the buffer. If + * the buffer is not empty it is guaranteed to start with an SPS. + * + * @param data Buffer containing start code delimited NAL units. + */ + public static void discardToSps(ByteBuffer data) { + int length = data.position(); + int consecutiveZeros = 0; + int offset = 0; + while (offset + 1 < length) { + int value = data.get(offset) & 0xFF; + if (consecutiveZeros == 3) { + if (value == 1 && (data.get(offset + 1) & 0x1F) == NAL_UNIT_TYPE_SPS) { + // Copy from this NAL unit onwards to the start of the buffer. + ByteBuffer offsetData = data.duplicate(); + offsetData.position(offset - 3); + offsetData.limit(length); + data.position(0); + data.put(offsetData); + return; + } + } else if (value == 0) { + consecutiveZeros++; + } + if (value != 0) { + consecutiveZeros = 0; + } + offset++; + } + // Empty the buffer if the SPS NAL unit was not found. + data.clear(); + } + + /** + * Constructs and returns a NAL unit with a start code followed by the data in {@code atom}. + */ + public static byte[] parseChildNalUnit(ParsableByteArray atom) { + int length = atom.readUnsignedShort(); + int offset = atom.getPosition(); + atom.skipBytes(length); + return CodecSpecificDataUtil.buildNalUnit(atom.data, offset, length); + } + + /** + * Gets the type of the NAL unit in {@code data} that starts at {@code offset}. + * + * @param data The data to search. + * @param offset The start offset of a NAL unit. Must lie between {@code -3} (inclusive) and + * {@code data.length - 3} (exclusive). + * @return The type of the unit. + */ + public static int getNalUnitType(byte[] data, int offset) { + return data[offset + 3] & 0x1F; + } + + /** + * Gets the type of the H.265 NAL unit in {@code data} that starts at {@code offset}. + * + * @param data The data to search. + * @param offset The start offset of a NAL unit. Must lie between {@code -3} (inclusive) and + * {@code data.length - 3} (exclusive). + * @return The type of the unit. + */ + public static int getH265NalUnitType(byte[] data, int offset) { + return (data[offset + 3] & 0x7E) >> 1; + } + + /** + * Parses an SPS NAL unit using the syntax defined in ITU-T Recommendation H.264 (2013) subsection + * 7.3.2.1.1. + * + * @param data A {@link ParsableBitArray} containing the SPS data. The position must to set to the + * start of the data (i.e. the first bit of the profile_idc field). + * @return A parsed representation of the SPS data. + */ + public static SpsData parseSpsNalUnit(ParsableBitArray data) { + int profileIdc = data.readBits(8); + data.skipBits(16); // constraint bits (6), reserved (2) and level_idc (8) + int seqParameterSetId = data.readUnsignedExpGolombCodedInt(); + + int chromaFormatIdc = 1; // Default is 4:2:0 + boolean separateColorPlaneFlag = false; + if (profileIdc == 100 || profileIdc == 110 || profileIdc == 122 || profileIdc == 244 + || profileIdc == 44 || profileIdc == 83 || profileIdc == 86 || profileIdc == 118 + || profileIdc == 128 || profileIdc == 138) { + chromaFormatIdc = data.readUnsignedExpGolombCodedInt(); + if (chromaFormatIdc == 3) { + separateColorPlaneFlag = data.readBit(); + } + data.readUnsignedExpGolombCodedInt(); // bit_depth_luma_minus8 + data.readUnsignedExpGolombCodedInt(); // bit_depth_chroma_minus8 + data.skipBits(1); // qpprime_y_zero_transform_bypass_flag + boolean seqScalingMatrixPresentFlag = data.readBit(); + if (seqScalingMatrixPresentFlag) { + int limit = (chromaFormatIdc != 3) ? 8 : 12; + for (int i = 0; i < limit; i++) { + boolean seqScalingListPresentFlag = data.readBit(); + if (seqScalingListPresentFlag) { + skipScalingList(data, i < 6 ? 16 : 64); + } + } + } + } + + int frameNumLength = data.readUnsignedExpGolombCodedInt() + 4; // log2_max_frame_num_minus4 + 4 + int picOrderCntType = data.readUnsignedExpGolombCodedInt(); + int picOrderCntLsbLength = 0; + boolean deltaPicOrderAlwaysZeroFlag = false; + if (picOrderCntType == 0) { + // log2_max_pic_order_cnt_lsb_minus4 + 4 + picOrderCntLsbLength = data.readUnsignedExpGolombCodedInt() + 4; + } else if (picOrderCntType == 1) { + deltaPicOrderAlwaysZeroFlag = data.readBit(); // delta_pic_order_always_zero_flag + data.readSignedExpGolombCodedInt(); // offset_for_non_ref_pic + data.readSignedExpGolombCodedInt(); // offset_for_top_to_bottom_field + long numRefFramesInPicOrderCntCycle = data.readUnsignedExpGolombCodedInt(); + for (int i = 0; i < numRefFramesInPicOrderCntCycle; i++) { + data.readUnsignedExpGolombCodedInt(); // offset_for_ref_frame[i] + } + } + data.readUnsignedExpGolombCodedInt(); // max_num_ref_frames + data.skipBits(1); // gaps_in_frame_num_value_allowed_flag + + int picWidthInMbs = data.readUnsignedExpGolombCodedInt() + 1; + int picHeightInMapUnits = data.readUnsignedExpGolombCodedInt() + 1; + boolean frameMbsOnlyFlag = data.readBit(); + int frameHeightInMbs = (2 - (frameMbsOnlyFlag ? 1 : 0)) * picHeightInMapUnits; + if (!frameMbsOnlyFlag) { + data.skipBits(1); // mb_adaptive_frame_field_flag + } + + data.skipBits(1); // direct_8x8_inference_flag + int frameWidth = picWidthInMbs * 16; + int frameHeight = frameHeightInMbs * 16; + boolean frameCroppingFlag = data.readBit(); + if (frameCroppingFlag) { + int frameCropLeftOffset = data.readUnsignedExpGolombCodedInt(); + int frameCropRightOffset = data.readUnsignedExpGolombCodedInt(); + int frameCropTopOffset = data.readUnsignedExpGolombCodedInt(); + int frameCropBottomOffset = data.readUnsignedExpGolombCodedInt(); + int cropUnitX, cropUnitY; + if (chromaFormatIdc == 0) { + cropUnitX = 1; + cropUnitY = 2 - (frameMbsOnlyFlag ? 1 : 0); + } else { + int subWidthC = (chromaFormatIdc == 3) ? 1 : 2; + int subHeightC = (chromaFormatIdc == 1) ? 2 : 1; + cropUnitX = subWidthC; + cropUnitY = subHeightC * (2 - (frameMbsOnlyFlag ? 1 : 0)); + } + frameWidth -= (frameCropLeftOffset + frameCropRightOffset) * cropUnitX; + frameHeight -= (frameCropTopOffset + frameCropBottomOffset) * cropUnitY; + } + + float pixelWidthHeightRatio = 1; + boolean vuiParametersPresentFlag = data.readBit(); + if (vuiParametersPresentFlag) { + boolean aspectRatioInfoPresentFlag = data.readBit(); + if (aspectRatioInfoPresentFlag) { + int aspectRatioIdc = data.readBits(8); + if (aspectRatioIdc == NalUnitUtil.EXTENDED_SAR) { + int sarWidth = data.readBits(16); + int sarHeight = data.readBits(16); + if (sarWidth != 0 && sarHeight != 0) { + pixelWidthHeightRatio = (float) sarWidth / sarHeight; + } + } else if (aspectRatioIdc < NalUnitUtil.ASPECT_RATIO_IDC_VALUES.length) { + pixelWidthHeightRatio = NalUnitUtil.ASPECT_RATIO_IDC_VALUES[aspectRatioIdc]; + } else { + Log.w(TAG, "Unexpected aspect_ratio_idc value: " + aspectRatioIdc); + } + } + } + + return new SpsData(seqParameterSetId, frameWidth, frameHeight, pixelWidthHeightRatio, + separateColorPlaneFlag, frameMbsOnlyFlag, frameNumLength, picOrderCntType, + picOrderCntLsbLength, deltaPicOrderAlwaysZeroFlag); + } + + /** + * Parses a PPS NAL unit using the syntax defined in ITU-T Recommendation H.264 (2013) subsection + * 7.3.2.2. + * + * @param data A {@link ParsableBitArray} containing the PPS data. The position must to set to the + * start of the data (i.e. the first bit of the pic_parameter_set_id field). + * @return A parsed representation of the PPS data. + */ + public static PpsData parsePpsNalUnit(ParsableBitArray data) { + int picParameterSetId = data.readUnsignedExpGolombCodedInt(); + int seqParameterSetId = data.readUnsignedExpGolombCodedInt(); + data.skipBits(1); // entropy_coding_mode_flag + boolean bottomFieldPicOrderInFramePresentFlag = data.readBit(); + return new PpsData(picParameterSetId, seqParameterSetId, bottomFieldPicOrderInFramePresentFlag); + } + + /** + * Finds the first NAL unit in {@code data}. + *

+ * If {@code prefixFlags} is null then the first three bytes of a NAL unit must be entirely + * contained within the part of the array being searched in order for it to be found. + *

+ * When {@code prefixFlags} is non-null, this method supports finding NAL units whose first four + * bytes span {@code data} arrays passed to successive calls. To use this feature, pass the same + * {@code prefixFlags} parameter to successive calls. State maintained in this parameter enables + * the detection of such NAL units. Note that when using this feature, the return value may be 3, + * 2 or 1 less than {@code startOffset}, to indicate a NAL unit starting 3, 2 or 1 bytes before + * the first byte in the current array. + * + * @param data The data to search. + * @param startOffset The offset (inclusive) in the data to start the search. + * @param endOffset The offset (exclusive) in the data to end the search. + * @param prefixFlags A boolean array whose first three elements are used to store the state + * required to detect NAL units where the NAL unit prefix spans array boundaries. The array + * must be at least 3 elements long. + * @return The offset of the NAL unit, or {@code endOffset} if a NAL unit was not found. + */ + public static int findNalUnit(byte[] data, int startOffset, int endOffset, + boolean[] prefixFlags) { + int length = endOffset - startOffset; + + Assertions.checkState(length >= 0); + if (length == 0) { + return endOffset; + } + + if (prefixFlags != null) { + if (prefixFlags[0]) { + clearPrefixFlags(prefixFlags); + return startOffset - 3; + } else if (length > 1 && prefixFlags[1] && data[startOffset] == 1) { + clearPrefixFlags(prefixFlags); + return startOffset - 2; + } else if (length > 2 && prefixFlags[2] && data[startOffset] == 0 + && data[startOffset + 1] == 1) { + clearPrefixFlags(prefixFlags); + return startOffset - 1; + } + } + + int limit = endOffset - 1; + // We're looking for the NAL unit start code prefix 0x000001. The value of i tracks the index of + // the third byte. + for (int i = startOffset + 2; i < limit; i += 3) { + if ((data[i] & 0xFE) != 0) { + // There isn't a NAL prefix here, or at the next two positions. Do nothing and let the + // loop advance the index by three. + } else if (data[i - 2] == 0 && data[i - 1] == 0 && data[i] == 1) { + if (prefixFlags != null) { + clearPrefixFlags(prefixFlags); + } + return i - 2; + } else { + // There isn't a NAL prefix here, but there might be at the next position. We should + // only skip forward by one. The loop will skip forward by three, so subtract two here. + i -= 2; + } + } + + if (prefixFlags != null) { + // True if the last three bytes in the data seen so far are {0,0,1}. + prefixFlags[0] = length > 2 + ? (data[endOffset - 3] == 0 && data[endOffset - 2] == 0 && data[endOffset - 1] == 1) + : length == 2 ? (prefixFlags[2] && data[endOffset - 2] == 0 && data[endOffset - 1] == 1) + : (prefixFlags[1] && data[endOffset - 1] == 1); + // True if the last two bytes in the data seen so far are {0,0}. + prefixFlags[1] = length > 1 ? data[endOffset - 2] == 0 && data[endOffset - 1] == 0 + : prefixFlags[2] && data[endOffset - 1] == 0; + // True if the last byte in the data seen so far is {0}. + prefixFlags[2] = data[endOffset - 1] == 0; + } + + return endOffset; + } + + /** + * Clears prefix flags, as used by {@link #findNalUnit(byte[], int, int, boolean[])}. + * + * @param prefixFlags The flags to clear. + */ + public static void clearPrefixFlags(boolean[] prefixFlags) { + prefixFlags[0] = false; + prefixFlags[1] = false; + prefixFlags[2] = false; + } + + private static int findNextUnescapeIndex(byte[] bytes, int offset, int limit) { + for (int i = offset; i < limit - 2; i++) { + if (bytes[i] == 0x00 && bytes[i + 1] == 0x00 && bytes[i + 2] == 0x03) { + return i; + } + } + return limit; + } + + private static void skipScalingList(ParsableBitArray bitArray, int size) { + int lastScale = 8; + int nextScale = 8; + for (int i = 0; i < size; i++) { + if (nextScale != 0) { + int deltaScale = bitArray.readSignedExpGolombCodedInt(); + nextScale = (lastScale + deltaScale + 256) % 256; + } + lastScale = (nextScale == 0) ? lastScale : nextScale; + } + } + + private NalUnitUtil() { + // Prevent instantiation. + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/ParsableBitArray.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/ParsableBitArray.java new file mode 100755 index 000000000..84ca89f4d --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/ParsableBitArray.java @@ -0,0 +1,233 @@ +/* + * 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.messenger.exoplayer.util; + +/** + * Wraps a byte array, providing methods that allow it to be read as a bitstream. + */ +public final class ParsableBitArray { + + public byte[] data; + + // The offset within the data, stored as the current byte offset, and the bit offset within that + // byte (from 0 to 7). + private int byteOffset; + private int bitOffset; + private int byteLimit; + + /** Creates a new instance that initially has no backing data. */ + public ParsableBitArray() {} + + /** + * Creates a new instance that wraps an existing array. + * + * @param data The data to wrap. + */ + public ParsableBitArray(byte[] data) { + this(data, data.length); + } + + /** + * Creates a new instance that wraps an existing array. + * + * @param data The data to wrap. + * @param limit The limit in bytes. + */ + public ParsableBitArray(byte[] data, int limit) { + this.data = data; + byteLimit = limit; + } + + /** + * Updates the instance to wrap {@code data}, and resets the position to zero. + * + * @param data The array to wrap. + */ + public void reset(byte[] data) { + reset(data, data.length); + } + + /** + * Updates the instance to wrap {@code data}, and resets the position to zero. + * + * @param data The array to wrap. + * @param limit The limit in bytes. + */ + public void reset(byte[] data, int limit) { + this.data = data; + byteOffset = 0; + bitOffset = 0; + byteLimit = limit; + } + + /** + * Returns the number of bits yet to be read. + */ + public int bitsLeft() { + return (byteLimit - byteOffset) * 8 - bitOffset; + } + + /** + * Gets the current bit offset. + * + * @return The current bit offset. + */ + public int getPosition() { + return byteOffset * 8 + bitOffset; + } + + /** + * Sets the current bit offset. + * + * @param position The position to set. + */ + public void setPosition(int position) { + byteOffset = position / 8; + bitOffset = position - (byteOffset * 8); + assertValidOffset(); + } + + /** + * Skips bits and moves current reading position forward. + * + * @param n The number of bits to skip. + */ + public void skipBits(int n) { + byteOffset += (n / 8); + bitOffset += (n % 8); + if (bitOffset > 7) { + byteOffset++; + bitOffset -= 8; + } + assertValidOffset(); + } + + /** + * Reads a single bit. + * + * @return True if the bit is set. False otherwise. + */ + public boolean readBit() { + return readBits(1) == 1; + } + + /** + * Reads up to 32 bits. + * + * @param numBits The number of bits to read. + * @return An integer whose bottom n bits hold the read data. + */ + public int readBits(int numBits) { + if (numBits == 0) { + return 0; + } + + int returnValue = 0; + + // Read as many whole bytes as we can. + int wholeBytes = (numBits / 8); + for (int i = 0; i < wholeBytes; i++) { + int byteValue; + if (bitOffset != 0) { + byteValue = ((data[byteOffset] & 0xFF) << bitOffset) + | ((data[byteOffset + 1] & 0xFF) >>> (8 - bitOffset)); + } else { + byteValue = data[byteOffset]; + } + numBits -= 8; + returnValue |= (byteValue & 0xFF) << numBits; + byteOffset++; + } + + // Read any remaining bits. + if (numBits > 0) { + int nextBit = bitOffset + numBits; + byte writeMask = (byte) (0xFF >> (8 - numBits)); + + if (nextBit > 8) { + // Combine bits from current byte and next byte. + returnValue |= ((((data[byteOffset] & 0xFF) << (nextBit - 8) + | ((data[byteOffset + 1] & 0xFF) >> (16 - nextBit))) & writeMask)); + byteOffset++; + } else { + // Bits to be read only within current byte. + returnValue |= (((data[byteOffset] & 0xFF) >> (8 - nextBit)) & writeMask); + if (nextBit == 8) { + byteOffset++; + } + } + + bitOffset = nextBit % 8; + } + + assertValidOffset(); + return returnValue; + } + + /** + * Returns whether it is possible to read an Exp-Golomb-coded integer starting from the current + * offset. The offset is not modified. + * + * @return Whether it is possible to read an Exp-Golomb-coded integer. + */ + public boolean canReadExpGolombCodedNum() { + int initialByteOffset = byteOffset; + int initialBitOffset = bitOffset; + int leadingZeros = 0; + while (byteOffset < byteLimit && !readBit()) { + leadingZeros++; + } + boolean hitLimit = byteOffset == byteLimit; + byteOffset = initialByteOffset; + bitOffset = initialBitOffset; + return !hitLimit && bitsLeft() >= leadingZeros * 2 + 1; + } + + /** + * Reads an unsigned Exp-Golomb-coded format integer. + * + * @return The value of the parsed Exp-Golomb-coded integer. + */ + public int readUnsignedExpGolombCodedInt() { + return readExpGolombCodeNum(); + } + + /** + * Reads an signed Exp-Golomb-coded format integer. + * + * @return The value of the parsed Exp-Golomb-coded integer. + */ + public int readSignedExpGolombCodedInt() { + int codeNum = readExpGolombCodeNum(); + return ((codeNum % 2) == 0 ? -1 : 1) * ((codeNum + 1) / 2); + } + + private int readExpGolombCodeNum() { + int leadingZeros = 0; + while (!readBit()) { + leadingZeros++; + } + return (1 << leadingZeros) - 1 + (leadingZeros > 0 ? readBits(leadingZeros) : 0); + } + + private void assertValidOffset() { + // It is fine for position to be at the end of the array, but no further. + Assertions.checkState(byteOffset >= 0 + && (bitOffset >= 0 && bitOffset < 8) + && (byteOffset < byteLimit || (byteOffset == byteLimit && bitOffset == 0))); + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/ParsableByteArray.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/ParsableByteArray.java new file mode 100755 index 000000000..387051375 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/ParsableByteArray.java @@ -0,0 +1,433 @@ +/* + * 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.messenger.exoplayer.util; + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; + +/** + * Wraps a byte array, providing a set of methods for parsing data from it. Numerical values are + * parsed with the assumption that their constituent bytes are in big endian order. + */ +public final class ParsableByteArray { + + public byte[] data; + + private int position; + private int limit; + + /** Creates a new instance that initially has no backing data. */ + public ParsableByteArray() {} + + /** Creates a new instance with {@code length} bytes. */ + public ParsableByteArray(int length) { + this.data = new byte[length]; + limit = data.length; + } + + /** Creates a new instance wrapping {@code data}. */ + public ParsableByteArray(byte[] data) { + this.data = data; + limit = data.length; + } + + /** + * Creates a new instance that wraps an existing array. + * + * @param data The data to wrap. + * @param limit The limit. + */ + public ParsableByteArray(byte[] data, int limit) { + this.data = data; + this.limit = limit; + } + + /** + * Updates the instance to wrap {@code data}, and resets the position to zero. + * + * @param data The array to wrap. + * @param limit The limit. + */ + public void reset(byte[] data, int limit) { + this.data = data; + this.limit = limit; + position = 0; + } + + /** + * Sets the position and limit to zero. + */ + public void reset() { + position = 0; + limit = 0; + } + + /** Returns the number of bytes yet to be read. */ + public int bytesLeft() { + return limit - position; + } + + /** Returns the limit. */ + public int limit() { + return limit; + } + + /** + * Sets the limit. + * + * @param limit The limit to set. + */ + public void setLimit(int limit) { + Assertions.checkArgument(limit >= 0 && limit <= data.length); + this.limit = limit; + } + + /** Returns the current offset in the array, in bytes. */ + public int getPosition() { + return position; + } + + /** Returns the capacity of the array, which may be larger than the limit. */ + public int capacity() { + return data == null ? 0 : data.length; + } + + /** + * Sets the reading offset in the array. + * + * @param position Byte offset in the array from which to read. + * @throws IllegalArgumentException Thrown if the new position is neither in nor at the end of the + * array. + */ + public void setPosition(int position) { + // It is fine for position to be at the end of the array. + Assertions.checkArgument(position >= 0 && position <= limit); + this.position = position; + } + + /** + * Moves the reading offset by {@code bytes}. + * + * @throws IllegalArgumentException Thrown if the new position is neither in nor at the end of the + * array. + */ + public void skipBytes(int bytes) { + setPosition(position + bytes); + } + + /** + * Reads the next {@code length} bytes into {@code bitArray}, and resets the position of + * {@code bitArray} to zero. + * + * @param bitArray The {@link ParsableBitArray} into which the bytes should be read. + * @param length The number of bytes to write. + */ + public void readBytes(ParsableBitArray bitArray, int length) { + readBytes(bitArray.data, 0, length); + bitArray.setPosition(0); + } + + /** + * Reads the next {@code length} bytes into {@code buffer} at {@code offset}. + * + * @see System#arraycopy + */ + public void readBytes(byte[] buffer, int offset, int length) { + System.arraycopy(data, position, buffer, offset, length); + position += length; + } + + /** + * Reads the next {@code length} bytes into {@code buffer}. + * + * @see ByteBuffer#put(byte[], int, int) + */ + public void readBytes(ByteBuffer buffer, int length) { + buffer.put(data, position, length); + position += length; + } + + /** Reads the next byte as an unsigned value. */ + public int readUnsignedByte() { + return (data[position++] & 0xFF); + } + + /** Reads the next two bytes as an unsigned value. */ + public int readUnsignedShort() { + return (data[position++] & 0xFF) << 8 + | (data[position++] & 0xFF); + } + + /** Reads the next two bytes as an unsigned value. */ + public int readLittleEndianUnsignedShort() { + return (data[position++] & 0xFF) | (data[position++] & 0xFF) << 8; + } + + /** Reads the next two bytes as an signed value. */ + public short readShort() { + return (short) ((data[position++] & 0xFF) << 8 + | (data[position++] & 0xFF)); + } + + /** Reads the next two bytes as a signed value. */ + public short readLittleEndianShort() { + return (short) ((data[position++] & 0xFF) | (data[position++] & 0xFF) << 8); + } + + /** Reads the next three bytes as an unsigned value. */ + public int readUnsignedInt24() { + return (data[position++] & 0xFF) << 16 + | (data[position++] & 0xFF) << 8 + | (data[position++] & 0xFF); + } + + /** Reads the next three bytes as a signed value in little endian order. */ + public int readLittleEndianInt24() { + return (data[position++] & 0xFF) + | (data[position++] & 0xFF) << 8 + | (data[position++] & 0xFF) << 16; + } + + /** Reads the next three bytes as an unsigned value in little endian order. */ + public int readLittleEndianUnsignedInt24() { + return (data[position++] & 0xFF) + | (data[position++] & 0xFF) << 8 + | (data[position++] & 0xFF) << 16; + } + + /** Reads the next four bytes as an unsigned value. */ + public long readUnsignedInt() { + return (data[position++] & 0xFFL) << 24 + | (data[position++] & 0xFFL) << 16 + | (data[position++] & 0xFFL) << 8 + | (data[position++] & 0xFFL); + } + + /** Reads the next four bytes as an unsigned value in little endian order. */ + public long readLittleEndianUnsignedInt() { + return (data[position++] & 0xFFL) + | (data[position++] & 0xFFL) << 8 + | (data[position++] & 0xFFL) << 16 + | (data[position++] & 0xFFL) << 24; + } + + /** Reads the next four bytes as a signed value. */ + public int readInt() { + return (data[position++] & 0xFF) << 24 + | (data[position++] & 0xFF) << 16 + | (data[position++] & 0xFF) << 8 + | (data[position++] & 0xFF); + } + + /** Reads the next four bytes as an signed value in little endian order. */ + public int readLittleEndianInt() { + return (data[position++] & 0xFF) + | (data[position++] & 0xFF) << 8 + | (data[position++] & 0xFF) << 16 + | (data[position++] & 0xFF) << 24; + } + + /** Reads the next eight bytes as a signed value. */ + public long readLong() { + return (data[position++] & 0xFFL) << 56 + | (data[position++] & 0xFFL) << 48 + | (data[position++] & 0xFFL) << 40 + | (data[position++] & 0xFFL) << 32 + | (data[position++] & 0xFFL) << 24 + | (data[position++] & 0xFFL) << 16 + | (data[position++] & 0xFFL) << 8 + | (data[position++] & 0xFFL); + } + + /** Reads the next eight bytes as a signed value in little endian order. */ + public long readLittleEndianLong() { + return (data[position++] & 0xFFL) + | (data[position++] & 0xFFL) << 8 + | (data[position++] & 0xFFL) << 16 + | (data[position++] & 0xFFL) << 24 + | (data[position++] & 0xFFL) << 32 + | (data[position++] & 0xFFL) << 40 + | (data[position++] & 0xFFL) << 48 + | (data[position++] & 0xFFL) << 56; + } + + /** Reads the next four bytes, returning the integer portion of the fixed point 16.16 integer. */ + public int readUnsignedFixedPoint1616() { + int result = (data[position++] & 0xFF) << 8 + | (data[position++] & 0xFF); + position += 2; // Skip the non-integer portion. + return result; + } + + /** + * Reads a Synchsafe integer. + *

+ * Synchsafe integers keep the highest bit of every byte zeroed. A 32 bit synchsafe integer can + * store 28 bits of information. + * + * @return The parsed value. + */ + public int readSynchSafeInt() { + int b1 = readUnsignedByte(); + int b2 = readUnsignedByte(); + int b3 = readUnsignedByte(); + int b4 = readUnsignedByte(); + return (b1 << 21) | (b2 << 14) | (b3 << 7) | b4; + } + + /** + * Reads the next four bytes as an unsigned integer into an integer, if the top bit is a zero. + * + * @throws IllegalStateException Thrown if the top bit of the input data is set. + */ + public int readUnsignedIntToInt() { + int result = readInt(); + if (result < 0) { + throw new IllegalStateException("Top bit not zero: " + result); + } + return result; + } + + /** + * Reads the next four bytes as a little endian unsigned integer into an integer, if the top bit + * is a zero. + * + * @throws IllegalStateException Thrown if the top bit of the input data is set. + */ + public int readLittleEndianUnsignedIntToInt() { + int result = readLittleEndianInt(); + if (result < 0) { + throw new IllegalStateException("Top bit not zero: " + result); + } + return result; + } + + /** + * Reads the next eight bytes as an unsigned long into a long, if the top bit is a zero. + * + * @throws IllegalStateException Thrown if the top bit of the input data is set. + */ + public long readUnsignedLongToLong() { + long result = readLong(); + if (result < 0) { + throw new IllegalStateException("Top bit not zero: " + result); + } + return result; + } + + /** Reads the next eight bytes as a 64-bit floating point value. */ + public double readDouble() { + return Double.longBitsToDouble(readLong()); + } + + /** + * Reads the next {@code length} bytes as UTF-8 characters. + * + * @param length The number of bytes to read. + * @return The string encoded by the bytes. + */ + public String readString(int length) { + return readString(length, Charset.defaultCharset()); + } + + /** + * Reads the next {@code length} bytes as characters in the specified {@link Charset}. + * + * @param length The number of bytes to read. + * @param charset The character set of the encoded characters. + * @return The string encoded by the bytes in the specified character set. + */ + public String readString(int length, Charset charset) { + String result = new String(data, position, length, charset); + position += length; + return result; + } + + /** + * Reads a line of text. + *

+ * A line is considered to be terminated by any one of a carriage return ('\r'), a line feed + * ('\n'), or a carriage return followed immediately by a line feed ('\r\n'). The system's default + * charset (UTF-8) is used. + * + * @return A String containing the contents of the line, not including any line-termination + * characters, or null if the end of the stream has been reached. + */ + public String readLine() { + if (bytesLeft() == 0) { + return null; + } + int lineLimit = position; + while (lineLimit < limit && data[lineLimit] != '\n' && data[lineLimit] != '\r') { + lineLimit++; + } + if (lineLimit - position >= 3 && data[position] == (byte) 0xEF + && data[position + 1] == (byte) 0xBB && data[position + 2] == (byte) 0xBF) { + // There's a byte order mark at the start of the line. Discard it. + position += 3; + } + String line = new String(data, position, lineLimit - position); + position = lineLimit; + if (position == limit) { + return line; + } + if (data[position] == '\r') { + position++; + if (position == limit) { + return line; + } + } + if (data[position] == '\n') { + position++; + } + return line; + } + + /** + * Reads a long value encoded by UTF-8 encoding + * @throws NumberFormatException if there is a problem with decoding + * @return Decoded long value + */ + public long readUTF8EncodedLong() { + int length = 0; + long value = data[position]; + // find the high most 0 bit + for (int j = 7; j >= 0; j--) { + if ((value & (1 << j)) == 0) { + if (j < 6) { + value &= (1 << j) - 1; + length = 7 - j; + } else if (j == 7) { + length = 1; + } + break; + } + } + if (length == 0) { + throw new NumberFormatException("Invalid UTF-8 sequence first byte: " + value); + } + for (int i = 1; i < length; i++) { + int x = data[position + i]; + if ((x & 0xC0) != 0x80) { // if the high most 0 bit not 7th + throw new NumberFormatException("Invalid UTF-8 sequence continuation byte: " + value); + } + value = (value << 6) | (x & 0x3F); + } + position += length; + return value; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/ParserUtil.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/ParserUtil.java new file mode 100755 index 000000000..88a477142 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/ParserUtil.java @@ -0,0 +1,55 @@ +/* + * 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.messenger.exoplayer.util; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +/** + * Parser utility functions. + */ +public final class ParserUtil { + + private ParserUtil() {} + + public static boolean isEndTag(XmlPullParser xpp, String name) throws XmlPullParserException { + return isEndTag(xpp) && xpp.getName().equals(name); + } + + public static boolean isEndTag(XmlPullParser xpp) throws XmlPullParserException { + return xpp.getEventType() == XmlPullParser.END_TAG; + } + + public static boolean isStartTag(XmlPullParser xpp, String name) + throws XmlPullParserException { + return isStartTag(xpp) && xpp.getName().equals(name); + } + + public static boolean isStartTag(XmlPullParser xpp) throws XmlPullParserException { + return xpp.getEventType() == XmlPullParser.START_TAG; + } + + public static String getAttributeValue(XmlPullParser xpp, String attributeName) { + int attributeCount = xpp.getAttributeCount(); + for (int i = 0; i < attributeCount; i++) { + if (attributeName.equals(xpp.getAttributeName(i))) { + return xpp.getAttributeValue(i); + } + } + return null; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/PlayerControl.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/PlayerControl.java new file mode 100755 index 000000000..bb3569ee6 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/PlayerControl.java @@ -0,0 +1,105 @@ +/* + * 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.messenger.exoplayer.util; + +import org.telegram.messenger.exoplayer.ExoPlayer; + +import android.widget.MediaController.MediaPlayerControl; + +/** + * An implementation of {@link MediaPlayerControl} for controlling an {@link ExoPlayer} instance. + *

+ * This class is provided for convenience, however it is expected that most applications will + * implement their own player controls and therefore not require this class. + */ +public class PlayerControl implements MediaPlayerControl { + + private final ExoPlayer exoPlayer; + + public PlayerControl(ExoPlayer exoPlayer) { + this.exoPlayer = exoPlayer; + } + + @Override + public boolean canPause() { + return true; + } + + @Override + public boolean canSeekBackward() { + return true; + } + + @Override + public boolean canSeekForward() { + return true; + } + + /** + * This is an unsupported operation. + *

+ * Application of audio effects is dependent on the audio renderer used. When using + * {@link org.telegram.messenger.exoplayer.MediaCodecAudioTrackRenderer}, the recommended approach is + * to extend the class and override + * {@link org.telegram.messenger.exoplayer.MediaCodecAudioTrackRenderer#onAudioSessionId}. + * + * @throws UnsupportedOperationException Always thrown. + */ + @Override + public int getAudioSessionId() { + throw new UnsupportedOperationException(); + } + + @Override + public int getBufferPercentage() { + return exoPlayer.getBufferedPercentage(); + } + + @Override + public int getCurrentPosition() { + return exoPlayer.getDuration() == ExoPlayer.UNKNOWN_TIME ? 0 + : (int) exoPlayer.getCurrentPosition(); + } + + @Override + public int getDuration() { + return exoPlayer.getDuration() == ExoPlayer.UNKNOWN_TIME ? 0 + : (int) exoPlayer.getDuration(); + } + + @Override + public boolean isPlaying() { + return exoPlayer.getPlayWhenReady(); + } + + @Override + public void start() { + exoPlayer.setPlayWhenReady(true); + } + + @Override + public void pause() { + exoPlayer.setPlayWhenReady(false); + } + + @Override + public void seekTo(int timeMillis) { + long seekPosition = exoPlayer.getDuration() == ExoPlayer.UNKNOWN_TIME ? 0 + : Math.min(Math.max(0, timeMillis), getDuration()); + exoPlayer.seekTo(seekPosition); + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/Predicate.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/Predicate.java new file mode 100755 index 000000000..e54ccb6af --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/Predicate.java @@ -0,0 +1,33 @@ +/* + * 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.messenger.exoplayer.util; + +/** + * Determines a true of false value for a given input. + * + * @param The input type of the predicate. + */ +public interface Predicate { + + /** + * Evaluates an input. + * + * @param input The input to evaluate. + * @return The evaluated result. + */ + boolean evaluate(T input); + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/PriorityHandlerThread.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/PriorityHandlerThread.java new file mode 100755 index 000000000..9bb3a80e4 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/PriorityHandlerThread.java @@ -0,0 +1,43 @@ +/* + * 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.messenger.exoplayer.util; + +import android.os.HandlerThread; +import android.os.Process; + +/** + * A {@link HandlerThread} with a specified process priority. + */ +public final class PriorityHandlerThread extends HandlerThread { + + private final int priority; + + /** + * @param name The name of the thread. + * @param priority The priority level. See {@link Process#setThreadPriority(int)} for details. + */ + public PriorityHandlerThread(String name, int priority) { + super(name); + this.priority = priority; + } + + @Override + public void run() { + Process.setThreadPriority(priority); + super.run(); + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/SlidingPercentile.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/SlidingPercentile.java new file mode 100755 index 000000000..d66ba46e4 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/SlidingPercentile.java @@ -0,0 +1,157 @@ +/* + * 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.messenger.exoplayer.util; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; + +/** + * Calculate any percentile over a sliding window of weighted values. A maximum total weight is + * configured. Once the maximum weight is reached, the oldest value is reduced in weight until it + * reaches zero and is removed. This maintains a constant total weight at steady state. + *

+ * SlidingPercentile can be used for bandwidth estimation based on a sliding window of past + * download rate observations. This is an alternative to sliding mean and exponential averaging + * which suffer from susceptibility to outliers and slow adaptation to step functions. + * + * @see Wiki: Moving average + * @see Wiki: Selection algorithm + */ +public final class SlidingPercentile { + + // Orderings. + private static final Comparator INDEX_COMPARATOR = new Comparator() { + @Override + public int compare(Sample a, Sample b) { + return a.index - b.index; + } + }; + + private static final Comparator VALUE_COMPARATOR = new Comparator() { + @Override + public int compare(Sample a, Sample b) { + return a.value < b.value ? -1 : b.value < a.value ? 1 : 0; + } + }; + + private static final int SORT_ORDER_NONE = -1; + private static final int SORT_ORDER_BY_VALUE = 0; + private static final int SORT_ORDER_BY_INDEX = 1; + + private static final int MAX_RECYCLED_SAMPLES = 5; + + private final int maxWeight; + private final ArrayList samples; + + private final Sample[] recycledSamples; + + private int currentSortOrder; + private int nextSampleIndex; + private int totalWeight; + private int recycledSampleCount; + + public SlidingPercentile(int maxWeight) { + this.maxWeight = maxWeight; + recycledSamples = new Sample[MAX_RECYCLED_SAMPLES]; + samples = new ArrayList<>(); + currentSortOrder = SORT_ORDER_NONE; + } + + /** + * Record a new observation. Respect the configured total weight by reducing in weight or + * removing the oldest observations as required. + * + * @param weight The weight of the new observation. + * @param value The value of the new observation. + */ + public void addSample(int weight, float value) { + ensureSortedByIndex(); + + Sample newSample = recycledSampleCount > 0 ? recycledSamples[--recycledSampleCount] + : new Sample(); + newSample.index = nextSampleIndex++; + newSample.weight = weight; + newSample.value = value; + samples.add(newSample); + totalWeight += weight; + + while (totalWeight > maxWeight) { + int excessWeight = totalWeight - maxWeight; + Sample oldestSample = samples.get(0); + if (oldestSample.weight <= excessWeight) { + totalWeight -= oldestSample.weight; + samples.remove(0); + if (recycledSampleCount < MAX_RECYCLED_SAMPLES) { + recycledSamples[recycledSampleCount++] = oldestSample; + } + } else { + oldestSample.weight -= excessWeight; + totalWeight -= excessWeight; + } + } + } + + /** + * Compute the percentile by integration. + * + * @param percentile The desired percentile, expressed as a fraction in the range (0,1]. + * @return The requested percentile value or Float.NaN. + */ + public float getPercentile(float percentile) { + ensureSortedByValue(); + float desiredWeight = percentile * totalWeight; + int accumulatedWeight = 0; + for (int i = 0; i < samples.size(); i++) { + Sample currentSample = samples.get(i); + accumulatedWeight += currentSample.weight; + if (accumulatedWeight >= desiredWeight) { + return currentSample.value; + } + } + // Clamp to maximum value or NaN if no values. + return samples.isEmpty() ? Float.NaN : samples.get(samples.size() - 1).value; + } + + /** + * Sort the samples by index, if not already. + */ + private void ensureSortedByIndex() { + if (currentSortOrder != SORT_ORDER_BY_INDEX) { + Collections.sort(samples, INDEX_COMPARATOR); + currentSortOrder = SORT_ORDER_BY_INDEX; + } + } + + /** + * Sort the samples by value, if not already. + */ + private void ensureSortedByValue() { + if (currentSortOrder != SORT_ORDER_BY_VALUE) { + Collections.sort(samples, VALUE_COMPARATOR); + currentSortOrder = SORT_ORDER_BY_VALUE; + } + } + + private static class Sample { + + public int index; + public int weight; + public float value; + + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/SystemClock.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/SystemClock.java new file mode 100755 index 000000000..fcf045a07 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/SystemClock.java @@ -0,0 +1,28 @@ +/* + * 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.messenger.exoplayer.util; + +/** + * The standard implementation of {@link Clock}. + */ +public final class SystemClock implements Clock { + + @Override + public long elapsedRealtime() { + return android.os.SystemClock.elapsedRealtime(); + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/TraceUtil.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/TraceUtil.java new file mode 100755 index 000000000..702fb204f --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/TraceUtil.java @@ -0,0 +1,61 @@ +/* + * 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.messenger.exoplayer.util; + +import org.telegram.messenger.exoplayer.ExoPlayerLibraryInfo; + +import android.annotation.TargetApi; + +/** + * Calls through to {@link android.os.Trace} methods on supported API levels. + */ +public final class TraceUtil { + + private TraceUtil() {} + + /** + * Writes a trace message to indicate that a given section of code has begun. + * + * @see android.os.Trace#beginSection(String) + */ + public static void beginSection(String sectionName) { + if (ExoPlayerLibraryInfo.TRACE_ENABLED && Util.SDK_INT >= 18) { + beginSectionV18(sectionName); + } + } + + /** + * Writes a trace message to indicate that a given section of code has ended. + * + * @see android.os.Trace#endSection() + */ + public static void endSection() { + if (ExoPlayerLibraryInfo.TRACE_ENABLED && Util.SDK_INT >= 18) { + endSectionV18(); + } + } + + @TargetApi(18) + private static void beginSectionV18(String sectionName) { + android.os.Trace.beginSection(sectionName); + } + + @TargetApi(18) + private static void endSectionV18() { + android.os.Trace.endSection(); + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/UriUtil.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/UriUtil.java new file mode 100755 index 000000000..8b1d950c6 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/UriUtil.java @@ -0,0 +1,258 @@ +/* + * 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.messenger.exoplayer.util; + +import android.net.Uri; +import android.text.TextUtils; + +/** + * Utility methods for manipulating URIs. + */ +public final class UriUtil { + + /** + * The length of arrays returned by {@link #getUriIndices(String)}. + */ + private static final int INDEX_COUNT = 4; + /** + * An index into an array returned by {@link #getUriIndices(String)}. + *

+ * The value at this position in the array is the index of the ':' after the scheme. Equals -1 if + * the URI is a relative reference (no scheme). The hier-part starts at (schemeColon + 1), + * including when the URI has no scheme. + */ + private static final int SCHEME_COLON = 0; + /** + * An index into an array returned by {@link #getUriIndices(String)}. + *

+ * The value at this position in the array is the index of the path part. Equals (schemeColon + 1) + * if no authority part, (schemeColon + 3) if the authority part consists of just "//", and + * (query) if no path part. The characters starting at this index can be "//" only if the + * authority part is non-empty (in this case the double-slash means the first segment is empty). + */ + private static final int PATH = 1; + /** + * An index into an array returned by {@link #getUriIndices(String)}. + *

+ * The value at this position in the array is the index of the query part, including the '?' + * before the query. Equals fragment if no query part, and (fragment - 1) if the query part is a + * single '?' with no data. + */ + private static final int QUERY = 2; + /** + * An index into an array returned by {@link #getUriIndices(String)}. + *

+ * The value at this position in the array is the index of the fragment part, including the '#' + * before the fragment. Equal to the length of the URI if no fragment part, and (length - 1) if + * the fragment part is a single '#' with no data. + */ + private static final int FRAGMENT = 3; + + private UriUtil() {} + + /** + * Like {@link #resolve(String, String)}, but returns a {@link Uri} instead of a {@link String}. + * + * @param baseUri The base URI. + * @param referenceUri The reference URI to resolve. + */ + public static Uri resolveToUri(String baseUri, String referenceUri) { + return Uri.parse(resolve(baseUri, referenceUri)); + } + + /** + * Performs relative resolution of a {@code referenceUri} with respect to a {@code baseUri}. + *

+ * The resolution is performed as specified by RFC-3986. + * + * @param baseUri The base URI. + * @param referenceUri The reference URI to resolve. + */ + public static String resolve(String baseUri, String referenceUri) { + StringBuilder uri = new StringBuilder(); + + // Map null onto empty string, to make the following logic simpler. + baseUri = baseUri == null ? "" : baseUri; + referenceUri = referenceUri == null ? "" : referenceUri; + + int[] refIndices = getUriIndices(referenceUri); + if (refIndices[SCHEME_COLON] != -1) { + // The reference is absolute. The target Uri is the reference. + uri.append(referenceUri); + removeDotSegments(uri, refIndices[PATH], refIndices[QUERY]); + return uri.toString(); + } + + int[] baseIndices = getUriIndices(baseUri); + if (refIndices[FRAGMENT] == 0) { + // The reference is empty or contains just the fragment part, then the target Uri is the + // concatenation of the base Uri without its fragment, and the reference. + return uri.append(baseUri, 0, baseIndices[FRAGMENT]).append(referenceUri).toString(); + } + + if (refIndices[QUERY] == 0) { + // The reference starts with the query part. The target is the base up to (but excluding) the + // query, plus the reference. + return uri.append(baseUri, 0, baseIndices[QUERY]).append(referenceUri).toString(); + } + + if (refIndices[PATH] != 0) { + // The reference has authority. The target is the base scheme plus the reference. + int baseLimit = baseIndices[SCHEME_COLON] + 1; + uri.append(baseUri, 0, baseLimit).append(referenceUri); + return removeDotSegments(uri, baseLimit + refIndices[PATH], baseLimit + refIndices[QUERY]); + } + + if (refIndices[PATH] != refIndices[QUERY] && referenceUri.charAt(refIndices[PATH]) == '/') { + // The reference path is rooted. The target is the base scheme and authority (if any), plus + // the reference. + uri.append(baseUri, 0, baseIndices[PATH]).append(referenceUri); + return removeDotSegments(uri, baseIndices[PATH], baseIndices[PATH] + refIndices[QUERY]); + } + + // The target Uri is the concatenation of the base Uri up to (but excluding) the last segment, + // and the reference. This can be split into 2 cases: + if (baseIndices[SCHEME_COLON] + 2 < baseIndices[PATH] + && baseIndices[PATH] == baseIndices[QUERY]) { + // Case 1: The base hier-part is just the authority, with an empty path. An additional '/' is + // needed after the authority, before appending the reference. + uri.append(baseUri, 0, baseIndices[PATH]).append('/').append(referenceUri); + return removeDotSegments(uri, baseIndices[PATH], baseIndices[PATH] + refIndices[QUERY] + 1); + } else { + // Case 2: Otherwise, find the last '/' in the base hier-part and append the reference after + // it. If base hier-part has no '/', it could only mean that it is completely empty or + // contains only one segment, in which case the whole hier-part is excluded and the reference + // is appended right after the base scheme colon without an added '/'. + int lastSlashIndex = baseUri.lastIndexOf('/', baseIndices[QUERY] - 1); + int baseLimit = lastSlashIndex == -1 ? baseIndices[PATH] : lastSlashIndex + 1; + uri.append(baseUri, 0, baseLimit).append(referenceUri); + return removeDotSegments(uri, baseIndices[PATH], baseLimit + refIndices[QUERY]); + } + } + + /** + * Removes dot segments from the path of a URI. + * + * @param uri A {@link StringBuilder} containing the URI. + * @param offset The index of the start of the path in {@code uri}. + * @param limit The limit (exclusive) of the path in {@code uri}. + */ + private static String removeDotSegments(StringBuilder uri, int offset, int limit) { + if (offset >= limit) { + // Nothing to do. + return uri.toString(); + } + if (uri.charAt(offset) == '/') { + // If the path starts with a /, always retain it. + offset++; + } + // The first character of the current path segment. + int segmentStart = offset; + int i = offset; + while (i <= limit) { + int nextSegmentStart = -1; + if (i == limit) { + nextSegmentStart = i; + } else if (uri.charAt(i) == '/') { + nextSegmentStart = i + 1; + } else { + i++; + continue; + } + // We've encountered the end of a segment or the end of the path. If the final segment was + // "." or "..", remove the appropriate segments of the path. + if (i == segmentStart + 1 && uri.charAt(segmentStart) == '.') { + // Given "abc/def/./ghi", remove "./" to get "abc/def/ghi". + uri.delete(segmentStart, nextSegmentStart); + limit -= nextSegmentStart - segmentStart; + i = segmentStart; + } else if (i == segmentStart + 2 && uri.charAt(segmentStart) == '.' + && uri.charAt(segmentStart + 1) == '.') { + // Given "abc/def/../ghi", remove "def/../" to get "abc/ghi". + int prevSegmentStart = uri.lastIndexOf("/", segmentStart - 2) + 1; + int removeFrom = prevSegmentStart > offset ? prevSegmentStart : offset; + uri.delete(removeFrom, nextSegmentStart); + limit -= nextSegmentStart - removeFrom; + segmentStart = prevSegmentStart; + i = prevSegmentStart; + } else { + i++; + segmentStart = i; + } + } + return uri.toString(); + } + + /** + * Calculates indices of the constituent components of a URI. + * + * @param uriString The URI as a string. + * @return The corresponding indices. + */ + private static int[] getUriIndices(String uriString) { + int[] indices = new int[INDEX_COUNT]; + if (TextUtils.isEmpty(uriString)) { + indices[SCHEME_COLON] = -1; + return indices; + } + + // Determine outer structure from right to left. + // Uri = scheme ":" hier-part [ "?" query ] [ "#" fragment ] + int length = uriString.length(); + int fragmentIndex = uriString.indexOf('#'); + if (fragmentIndex == -1) { + fragmentIndex = length; + } + int queryIndex = uriString.indexOf('?'); + if (queryIndex == -1 || queryIndex > fragmentIndex) { + // '#' before '?': '?' is within the fragment. + queryIndex = fragmentIndex; + } + // Slashes are allowed only in hier-part so any colon after the first slash is part of the + // hier-part, not the scheme colon separator. + int schemeIndexLimit = uriString.indexOf('/'); + if (schemeIndexLimit == -1 || schemeIndexLimit > queryIndex) { + schemeIndexLimit = queryIndex; + } + int schemeIndex = uriString.indexOf(':'); + if (schemeIndex > schemeIndexLimit) { + // '/' before ':' + schemeIndex = -1; + } + + // Determine hier-part structure: hier-part = "//" authority path / path + // This block can also cope with schemeIndex == -1. + boolean hasAuthority = schemeIndex + 2 < queryIndex + && uriString.charAt(schemeIndex + 1) == '/' + && uriString.charAt(schemeIndex + 2) == '/'; + int pathIndex; + if (hasAuthority) { + pathIndex = uriString.indexOf('/', schemeIndex + 3); // find first '/' after "://" + if (pathIndex == -1 || pathIndex > queryIndex) { + pathIndex = queryIndex; + } + } else { + pathIndex = schemeIndex + 1; + } + + indices[SCHEME_COLON] = schemeIndex; + indices[PATH] = pathIndex; + indices[QUERY] = queryIndex; + indices[FRAGMENT] = fragmentIndex; + return indices; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/Util.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/Util.java new file mode 100755 index 000000000..813c1a47c --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/Util.java @@ -0,0 +1,914 @@ +/* + * 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.messenger.exoplayer.util; + +import org.telegram.messenger.exoplayer.C; +import org.telegram.messenger.exoplayer.ExoPlayerLibraryInfo; +import org.telegram.messenger.exoplayer.upstream.DataSource; +import org.telegram.messenger.exoplayer.upstream.DataSpec; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.net.Uri; +import android.os.Build; +import android.text.TextUtils; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.Method; +import java.math.BigDecimal; +import java.net.HttpURLConnection; +import java.net.URL; +import java.text.ParseException; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collections; +import java.util.GregorianCalendar; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.TimeZone; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadFactory; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Miscellaneous utility functions. + */ +public final class Util { + + /** + * Like {@link android.os.Build.VERSION#SDK_INT}, but in a place where it can be conveniently + * overridden for local testing. + */ + public static final int SDK_INT = + (Build.VERSION.SDK_INT == 23 && Build.VERSION.CODENAME.charAt(0) == 'N') ? 24 + : Build.VERSION.SDK_INT; + + /** + * Like {@link Build#DEVICE}, but in a place where it can be conveniently overridden for local + * testing. + */ + public static final String DEVICE = Build.DEVICE; + + /** + * Like {@link Build#MANUFACTURER}, but in a place where it can be conveniently overridden for + * local testing. + */ + public static final String MANUFACTURER = Build.MANUFACTURER; + + /** + * Like {@link Build#MODEL}, but in a place where it can be conveniently overridden for local + * testing. + */ + public static final String MODEL = Build.MODEL; + + /** + * Value returned by {@link #inferContentType(String)} for DASH manifests. + */ + public static final int TYPE_DASH = 0; + + /** + * Value returned by {@link #inferContentType(String)} for Smooth Streaming manifests. + */ + public static final int TYPE_SS = 1; + + /** + * Value returned by {@link #inferContentType(String)} for HLS manifests. + */ + public static final int TYPE_HLS = 2; + + /** + * Value returned by {@link #inferContentType(String)} for files other than DASH, HLS or Smooth + * Streaming manifests. + */ + public static final int TYPE_OTHER = 3; + + private static final Pattern XS_DATE_TIME_PATTERN = Pattern.compile( + "(\\d\\d\\d\\d)\\-(\\d\\d)\\-(\\d\\d)[Tt]" + + "(\\d\\d):(\\d\\d):(\\d\\d)(\\.(\\d+))?" + + "([Zz]|((\\+|\\-)(\\d\\d):(\\d\\d)))?"); + + private static final Pattern XS_DURATION_PATTERN = + Pattern.compile("^(-)?P(([0-9]*)Y)?(([0-9]*)M)?(([0-9]*)D)?" + + "(T(([0-9]*)H)?(([0-9]*)M)?(([0-9.]*)S)?)?$"); + + private static final Pattern ESCAPED_CHARACTER_PATTERN = Pattern.compile("%([A-Fa-f0-9]{2})"); + + private static final long MAX_BYTES_TO_DRAIN = 2048; + + private Util() {} + + /** + * Returns whether the device is an AndroidTV. + * + * @param context A context. + * @return True if the device is an AndroidTV. False otherwise. + */ + @SuppressLint("InlinedApi") + public static boolean isAndroidTv(Context context) { + return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK); + } + + /** + * Converts the entirety of an {@link InputStream} to a byte array. + * + * @param inputStream the {@link InputStream} to be read. The input stream is not closed by this + * method. + * @return a byte array containing all of the inputStream's bytes. + * @throws IOException if an error occurs reading from the stream. + */ + public static byte[] toByteArray(InputStream inputStream) throws IOException { + byte[] buffer = new byte[1024 * 4]; + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, bytesRead); + } + return outputStream.toByteArray(); + } + + /** + * Returns true if the URI is a path to a local file or a reference to a local file. + * + * @param uri The uri to test. + */ + public static boolean isLocalFileUri(Uri uri) { + String scheme = uri.getScheme(); + return TextUtils.isEmpty(scheme) || scheme.equals("file"); + } + + /** + * Tests two objects for {@link Object#equals(Object)} equality, handling the case where one or + * both may be null. + * + * @param o1 The first object. + * @param o2 The second object. + * @return {@code o1 == null ? o2 == null : o1.equals(o2)}. + */ + public static boolean areEqual(Object o1, Object o2) { + return o1 == null ? o2 == null : o1.equals(o2); + } + + /** + * Tests whether an {@code items} array contains an object equal to {@code item}, according to + * {@link Object#equals(Object)}. + *

+ * If {@code item} is null then true is returned if and only if {@code items} contains null. + * + * @param items The array of items to search. + * @param item The item to search for. + * @return True if the array contains an object equal to the item being searched for. + */ + public static boolean contains(Object[] items, Object item) { + for (int i = 0; i < items.length; i++) { + if (Util.areEqual(items[i], item)) { + return true; + } + } + return false; + } + + /** + * Instantiates a new single threaded executor whose thread has the specified name. + * + * @param threadName The name of the thread. + * @return The executor. + */ + public static ExecutorService newSingleThreadExecutor(final String threadName) { + return Executors.newSingleThreadExecutor(new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + return new Thread(r, threadName); + } + }); + } + + /** + * Instantiates a new single threaded scheduled executor whose thread has the specified name. + * + * @param threadName The name of the thread. + * @return The executor. + */ + public static ScheduledExecutorService newSingleThreadScheduledExecutor(final String threadName) { + return Executors.newSingleThreadScheduledExecutor(new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + return new Thread(r, threadName); + } + }); + } + + /** + * Closes a {@link DataSource}, suppressing any {@link IOException} that may occur. + * + * @param dataSource The {@link DataSource} to close. + */ + public static void closeQuietly(DataSource dataSource) { + try { + dataSource.close(); + } catch (IOException e) { + // Ignore. + } + } + + /** + * Closes an {@link OutputStream}, suppressing any {@link IOException} that may occur. + * + * @param outputStream The {@link OutputStream} to close. + */ + public static void closeQuietly(OutputStream outputStream) { + try { + outputStream.close(); + } catch (IOException e) { + // Ignore. + } + } + + /** + * Converts text to lower case using {@link Locale#US}. + * + * @param text The text to convert. + * @return The lower case text, or null if {@code text} is null. + */ + public static String toLowerInvariant(String text) { + return text == null ? null : text.toLowerCase(Locale.US); + } + + /** + * Divides a {@code numerator} by a {@code denominator}, returning the ceiled result. + * + * @param numerator The numerator to divide. + * @param denominator The denominator to divide by. + * @return The ceiled result of the division. + */ + public static int ceilDivide(int numerator, int denominator) { + return (numerator + denominator - 1) / denominator; + } + + /** + * Divides a {@code numerator} by a {@code denominator}, returning the ceiled result. + * + * @param numerator The numerator to divide. + * @param denominator The denominator to divide by. + * @return The ceiled result of the division. + */ + public static long ceilDivide(long numerator, long denominator) { + return (numerator + denominator - 1) / denominator; + } + + /** + * Returns the index of the largest value in an array that is less than (or optionally equal to) + * a specified key. + *

+ * The search is performed using a binary search algorithm, and so the array must be sorted. + * + * @param a The array to search. + * @param key The key being searched for. + * @param inclusive If the key is present in the array, whether to return the corresponding index. + * If false then the returned index corresponds to the largest value in the array that is + * strictly less than the key. + * @param stayInBounds If true, then 0 will be returned in the case that the key is smaller than + * the smallest value in the array. If false then -1 will be returned. + */ + public static int binarySearchFloor(long[] a, long key, boolean inclusive, boolean stayInBounds) { + int index = Arrays.binarySearch(a, key); + index = index < 0 ? -(index + 2) : (inclusive ? index : (index - 1)); + return stayInBounds ? Math.max(0, index) : index; + } + + /** + * Returns the index of the smallest value in an array that is greater than (or optionally equal + * to) a specified key. + *

+ * The search is performed using a binary search algorithm, and so the array must be sorted. + * + * @param a The array to search. + * @param key The key being searched for. + * @param inclusive If the key is present in the array, whether to return the corresponding index. + * If false then the returned index corresponds to the smallest value in the array that is + * strictly greater than the key. + * @param stayInBounds If true, then {@code (a.length - 1)} will be returned in the case that the + * key is greater than the largest value in the array. If false then {@code a.length} will be + * returned. + */ + public static int binarySearchCeil(long[] a, long key, boolean inclusive, boolean stayInBounds) { + int index = Arrays.binarySearch(a, key); + index = index < 0 ? ~index : (inclusive ? index : (index + 1)); + return stayInBounds ? Math.min(a.length - 1, index) : index; + } + + /** + * Returns the index of the largest value in an list that is less than (or optionally equal to) + * a specified key. + *

+ * The search is performed using a binary search algorithm, and so the list must be sorted. + * + * @param list The list to search. + * @param key The key being searched for. + * @param inclusive If the key is present in the list, whether to return the corresponding index. + * If false then the returned index corresponds to the largest value in the list that is + * strictly less than the key. + * @param stayInBounds If true, then 0 will be returned in the case that the key is smaller than + * the smallest value in the list. If false then -1 will be returned. + */ + public static int binarySearchFloor(List> list, T key, + boolean inclusive, boolean stayInBounds) { + int index = Collections.binarySearch(list, key); + index = index < 0 ? -(index + 2) : (inclusive ? index : (index - 1)); + return stayInBounds ? Math.max(0, index) : index; + } + + /** + * Returns the index of the smallest value in an list that is greater than (or optionally equal + * to) a specified key. + *

+ * The search is performed using a binary search algorithm, and so the list must be sorted. + * + * @param list The list to search. + * @param key The key being searched for. + * @param inclusive If the key is present in the list, whether to return the corresponding index. + * If false then the returned index corresponds to the smallest value in the list that is + * strictly greater than the key. + * @param stayInBounds If true, then {@code (list.size() - 1)} will be returned in the case that + * the key is greater than the largest value in the list. If false then {@code list.size()} + * will be returned. + */ + public static int binarySearchCeil(List> list, T key, + boolean inclusive, boolean stayInBounds) { + int index = Collections.binarySearch(list, key); + index = index < 0 ? ~index : (inclusive ? index : (index + 1)); + return stayInBounds ? Math.min(list.size() - 1, index) : index; + } + + /** + * Creates an integer array containing the integers from 0 to {@code length - 1}. + * + * @param length The length of the array. + * @return The array. + */ + public static int[] firstIntegersArray(int length) { + int[] firstIntegers = new int[length]; + for (int i = 0; i < length; i++) { + firstIntegers[i] = i; + } + return firstIntegers; + } + + /** + * Parses an xs:duration attribute value, returning the parsed duration in milliseconds. + * + * @param value The attribute value to parse. + * @return The parsed duration in milliseconds. + */ + public static long parseXsDuration(String value) { + Matcher matcher = XS_DURATION_PATTERN.matcher(value); + if (matcher.matches()) { + boolean negated = !TextUtils.isEmpty(matcher.group(1)); + // Durations containing years and months aren't completely defined. We assume there are + // 30.4368 days in a month, and 365.242 days in a year. + String years = matcher.group(3); + double durationSeconds = (years != null) ? Double.parseDouble(years) * 31556908 : 0; + String months = matcher.group(5); + durationSeconds += (months != null) ? Double.parseDouble(months) * 2629739 : 0; + String days = matcher.group(7); + durationSeconds += (days != null) ? Double.parseDouble(days) * 86400 : 0; + String hours = matcher.group(10); + durationSeconds += (hours != null) ? Double.parseDouble(hours) * 3600 : 0; + String minutes = matcher.group(12); + durationSeconds += (minutes != null) ? Double.parseDouble(minutes) * 60 : 0; + String seconds = matcher.group(14); + durationSeconds += (seconds != null) ? Double.parseDouble(seconds) : 0; + long durationMillis = (long) (durationSeconds * 1000); + return negated ? -durationMillis : durationMillis; + } else { + return (long) (Double.parseDouble(value) * 3600 * 1000); + } + } + + /** + * Parses an xs:dateTime attribute value, returning the parsed timestamp in milliseconds since + * the epoch. + * + * @param value The attribute value to parse. + * @return The parsed timestamp in milliseconds since the epoch. + */ + public static long parseXsDateTime(String value) throws ParseException { + Matcher matcher = XS_DATE_TIME_PATTERN.matcher(value); + if (!matcher.matches()) { + throw new ParseException("Invalid date/time format: " + value, 0); + } + + int timezoneShift; + if (matcher.group(9) == null) { + // No time zone specified. + timezoneShift = 0; + } else if (matcher.group(9).equalsIgnoreCase("Z")) { + timezoneShift = 0; + } else { + timezoneShift = ((Integer.parseInt(matcher.group(12)) * 60 + + Integer.parseInt(matcher.group(13)))); + if (matcher.group(11).equals("-")) { + timezoneShift *= -1; + } + } + + Calendar dateTime = new GregorianCalendar(TimeZone.getTimeZone("GMT")); + + dateTime.clear(); + // Note: The month value is 0-based, hence the -1 on group(2) + dateTime.set(Integer.parseInt(matcher.group(1)), + Integer.parseInt(matcher.group(2)) - 1, + Integer.parseInt(matcher.group(3)), + Integer.parseInt(matcher.group(4)), + Integer.parseInt(matcher.group(5)), + Integer.parseInt(matcher.group(6))); + if (!TextUtils.isEmpty(matcher.group(8))) { + final BigDecimal bd = new BigDecimal("0." + matcher.group(8)); + // we care only for milliseconds, so movePointRight(3) + dateTime.set(Calendar.MILLISECOND, bd.movePointRight(3).intValue()); + } + + long time = dateTime.getTimeInMillis(); + if (timezoneShift != 0) { + time -= timezoneShift * 60000; + } + + return time; + } + + /** + * Scales a large timestamp. + *

+ * Logically, scaling consists of a multiplication followed by a division. The actual operations + * performed are designed to minimize the probability of overflow. + * + * @param timestamp The timestamp to scale. + * @param multiplier The multiplier. + * @param divisor The divisor. + * @return The scaled timestamp. + */ + public static long scaleLargeTimestamp(long timestamp, long multiplier, long divisor) { + if (divisor >= multiplier && (divisor % multiplier) == 0) { + long divisionFactor = divisor / multiplier; + return timestamp / divisionFactor; + } else if (divisor < multiplier && (multiplier % divisor) == 0) { + long multiplicationFactor = multiplier / divisor; + return timestamp * multiplicationFactor; + } else { + double multiplicationFactor = (double) multiplier / divisor; + return (long) (timestamp * multiplicationFactor); + } + } + + /** + * Applies {@link #scaleLargeTimestamp(long, long, long)} to a list of unscaled timestamps. + * + * @param timestamps The timestamps to scale. + * @param multiplier The multiplier. + * @param divisor The divisor. + * @return The scaled timestamps. + */ + public static long[] scaleLargeTimestamps(List timestamps, long multiplier, long divisor) { + long[] scaledTimestamps = new long[timestamps.size()]; + if (divisor >= multiplier && (divisor % multiplier) == 0) { + long divisionFactor = divisor / multiplier; + for (int i = 0; i < scaledTimestamps.length; i++) { + scaledTimestamps[i] = timestamps.get(i) / divisionFactor; + } + } else if (divisor < multiplier && (multiplier % divisor) == 0) { + long multiplicationFactor = multiplier / divisor; + for (int i = 0; i < scaledTimestamps.length; i++) { + scaledTimestamps[i] = timestamps.get(i) * multiplicationFactor; + } + } else { + double multiplicationFactor = (double) multiplier / divisor; + for (int i = 0; i < scaledTimestamps.length; i++) { + scaledTimestamps[i] = (long) (timestamps.get(i) * multiplicationFactor); + } + } + return scaledTimestamps; + } + + /** + * Applies {@link #scaleLargeTimestamp(long, long, long)} to an array of unscaled timestamps. + * + * @param timestamps The timestamps to scale. + * @param multiplier The multiplier. + * @param divisor The divisor. + */ + public static void scaleLargeTimestampsInPlace(long[] timestamps, long multiplier, long divisor) { + if (divisor >= multiplier && (divisor % multiplier) == 0) { + long divisionFactor = divisor / multiplier; + for (int i = 0; i < timestamps.length; i++) { + timestamps[i] /= divisionFactor; + } + } else if (divisor < multiplier && (multiplier % divisor) == 0) { + long multiplicationFactor = multiplier / divisor; + for (int i = 0; i < timestamps.length; i++) { + timestamps[i] *= multiplicationFactor; + } + } else { + double multiplicationFactor = (double) multiplier / divisor; + for (int i = 0; i < timestamps.length; i++) { + timestamps[i] = (long) (timestamps[i] * multiplicationFactor); + } + } + } + + /** + * Converts a list of integers to a primitive array. + * + * @param list A list of integers. + * @return The list in array form, or null if the input list was null. + */ + public static int[] toArray(List list) { + if (list == null) { + return null; + } + int length = list.size(); + int[] intArray = new int[length]; + for (int i = 0; i < length; i++) { + intArray[i] = list.get(i); + } + return intArray; + } + + /** + * On platform API levels 19 and 20, okhttp's implementation of {@link InputStream#close} can + * block for a long time if the stream has a lot of data remaining. Call this method before + * closing the input stream to make a best effort to cause the input stream to encounter an + * unexpected end of input, working around this issue. On other platform API levels, the method + * does nothing. + * + * @param connection The connection whose {@link InputStream} should be terminated. + * @param bytesRemaining The number of bytes remaining to be read from the input stream if its + * length is known. {@link C#LENGTH_UNBOUNDED} otherwise. + */ + public static void maybeTerminateInputStream(HttpURLConnection connection, long bytesRemaining) { + if (SDK_INT != 19 && SDK_INT != 20) { + return; + } + + try { + InputStream inputStream = connection.getInputStream(); + if (bytesRemaining == C.LENGTH_UNBOUNDED) { + // If the input stream has already ended, do nothing. The socket may be re-used. + if (inputStream.read() == -1) { + return; + } + } else if (bytesRemaining <= MAX_BYTES_TO_DRAIN) { + // There isn't much data left. Prefer to allow it to drain, which may allow the socket to be + // re-used. + return; + } + String className = inputStream.getClass().getName(); + if (className.equals("com.android.okhttp.internal.http.HttpTransport$ChunkedInputStream") + || className.equals( + "com.android.okhttp.internal.http.HttpTransport$FixedLengthInputStream")) { + Class superclass = inputStream.getClass().getSuperclass(); + Method unexpectedEndOfInput = superclass.getDeclaredMethod("unexpectedEndOfInput"); + unexpectedEndOfInput.setAccessible(true); + unexpectedEndOfInput.invoke(inputStream); + } + } catch (IOException e) { + // The connection didn't ever have an input stream, or it was closed already. + } catch (Exception e) { + // Something went wrong. The device probably isn't using okhttp. + } + } + + /** + * Given a {@link DataSpec} and a number of bytes already loaded, returns a {@link DataSpec} + * that represents the remainder of the data. + * + * @param dataSpec The original {@link DataSpec}. + * @param bytesLoaded The number of bytes already loaded. + * @return A {@link DataSpec} that represents the remainder of the data. + */ + public static DataSpec getRemainderDataSpec(DataSpec dataSpec, int bytesLoaded) { + if (bytesLoaded == 0) { + return dataSpec; + } else { + long remainingLength = dataSpec.length == C.LENGTH_UNBOUNDED ? C.LENGTH_UNBOUNDED + : dataSpec.length - bytesLoaded; + return new DataSpec(dataSpec.uri, dataSpec.position + bytesLoaded, remainingLength, + dataSpec.key, dataSpec.flags); + } + } + + /** + * Returns the integer equal to the big-endian concatenation of the characters in {@code string} + * as bytes. {@code string} must contain four or fewer characters. + */ + public static int getIntegerCodeForString(String string) { + int length = string.length(); + Assertions.checkArgument(length <= 4); + int result = 0; + for (int i = 0; i < length; i++) { + result <<= 8; + result |= string.charAt(i); + } + return result; + } + + /** + * Returns the top 32 bits of a long as an integer. + */ + public static int getTopInt(long value) { + return (int) (value >>> 32); + } + + /** + * Returns the bottom 32 bits of a long as an integer. + */ + public static int getBottomInt(long value) { + return (int) value; + } + + /** + * Returns a long created by concatenating the bits of two integers. + */ + public static long getLong(int topInteger, int bottomInteger) { + return ((long) topInteger << 32) | (bottomInteger & 0xFFFFFFFFL); + } + + /** + * Returns a hex string representation of the data provided. + * + * @param data The byte array containing the data to be turned into a hex string. + * @param beginIndex The begin index, inclusive. + * @param endIndex The end index, exclusive. + * @return A string containing the hex representation of the data provided. + */ + public static String getHexStringFromBytes(byte[] data, int beginIndex, int endIndex) { + StringBuilder dataStringBuilder = new StringBuilder(endIndex - beginIndex); + for (int i = beginIndex; i < endIndex; i++) { + dataStringBuilder.append(String.format(Locale.US, "%02X", data[i])); + } + return dataStringBuilder.toString(); + } + + /** + * Returns a byte array containing values parsed from the hex string provided. + * + * @param hexString The hex string to convert to bytes. + * @return A byte array containing values parsed from the hex string provided. + */ + public static byte[] getBytesFromHexString(String hexString) { + byte[] data = new byte[hexString.length() / 2]; + for (int i = 0; i < data.length; i++) { + int stringOffset = i * 2; + data[i] = (byte) ((Character.digit(hexString.charAt(stringOffset), 16) << 4) + + Character.digit(hexString.charAt(stringOffset + 1), 16)); + } + return data; + } + + /** + * Returns a string with comma delimited simple names of each object's class. + * + * @param objects The objects whose simple class names should be comma delimited and returned. + * @return A string with comma delimited simple names of each object's class. + */ + public static String getCommaDelimitedSimpleClassNames(T[] objects) { + StringBuilder stringBuilder = new StringBuilder(); + for (int i = 0; i < objects.length; i++) { + stringBuilder.append(objects[i].getClass().getSimpleName()); + if (i < objects.length - 1) { + stringBuilder.append(", "); + } + } + return stringBuilder.toString(); + } + + /** + * Returns a user agent string based on the given application name and the library version. + * + * @param context A valid context of the calling application. + * @param applicationName String that will be prefix'ed to the generated user agent. + * @return A user agent string generated using the applicationName and the library version. + */ + public static String getUserAgent(Context context, String applicationName) { + String versionName; + try { + String packageName = context.getPackageName(); + PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0); + versionName = info.versionName; + } catch (NameNotFoundException e) { + versionName = "?"; + } + return applicationName + "/" + versionName + " (Linux;Android " + Build.VERSION.RELEASE + + ") " + "ExoPlayerLib/" + ExoPlayerLibraryInfo.VERSION; + } + + /** + * Executes a post request using {@link HttpURLConnection}. + * + * @param url The request URL. + * @param data The request body, or null. + * @param requestProperties Request properties, or null. + * @return The response body. + * @throws IOException If an error occurred making the request. + */ + // TODO: Remove this and use HttpDataSource once DataSpec supports inclusion of a POST body. + public static byte[] executePost(String url, byte[] data, Map requestProperties) + throws IOException { + HttpURLConnection urlConnection = null; + try { + urlConnection = (HttpURLConnection) new URL(url).openConnection(); + urlConnection.setRequestMethod("POST"); + urlConnection.setDoOutput(data != null); + urlConnection.setDoInput(true); + if (requestProperties != null) { + for (Map.Entry requestProperty : requestProperties.entrySet()) { + urlConnection.setRequestProperty(requestProperty.getKey(), requestProperty.getValue()); + } + } + // Write the request body, if there is one. + if (data != null) { + OutputStream out = urlConnection.getOutputStream(); + try { + out.write(data); + } finally { + out.close(); + } + } + // Read and return the response body. + InputStream inputStream = urlConnection.getInputStream(); + try { + return toByteArray(inputStream); + } finally { + inputStream.close(); + } + } finally { + if (urlConnection != null) { + urlConnection.disconnect(); + } + } + } + + /** + * Converts a sample bit depth to a corresponding PCM encoding constant. + * + * @param bitDepth The bit depth. Supported values are 8, 16, 24 and 32. + * @return The corresponding encoding. One of {@link C#ENCODING_PCM_8BIT}, + * {@link C#ENCODING_PCM_16BIT}, {@link C#ENCODING_PCM_24BIT} and + * {@link C#ENCODING_PCM_32BIT}. If the bit depth is unsupported then + * {@link C#ENCODING_INVALID} is returned. + */ + public static int getPcmEncoding(int bitDepth) { + switch (bitDepth) { + case 8: + return C.ENCODING_PCM_8BIT; + case 16: + return C.ENCODING_PCM_16BIT; + case 24: + return C.ENCODING_PCM_24BIT; + case 32: + return C.ENCODING_PCM_32BIT; + default: + return C.ENCODING_INVALID; + } + } + + /** + * Makes a best guess to infer the type from a file name. + * + * @param fileName Name of the file. It can include the path of the file. + * @return One of {@link #TYPE_DASH}, {@link #TYPE_SS}, {@link #TYPE_HLS} or {@link #TYPE_OTHER}. + */ + public static int inferContentType(String fileName) { + if (fileName == null) { + return TYPE_OTHER; + } else if (fileName.endsWith(".mpd")) { + return TYPE_DASH; + } else if (fileName.endsWith(".ism")) { + return TYPE_SS; + } else if (fileName.endsWith(".m3u8")) { + return TYPE_HLS; + } else { + return TYPE_OTHER; + } + } + + /** + * Escapes a string so that it's safe for use as a file or directory name on at least FAT32 + * filesystems. FAT32 is the most restrictive of all filesystems still commonly used today. + * + *

For simplicity, this only handles common characters known to be illegal on FAT32: + * <, >, :, ", /, \, |, ?, and *. % is also escaped since it is used as the escape character. + * Escaping is performed in a consistent way so that no collisions occur and + * {@link #unescapeFileName(String)} can be used to retrieve the original file name. + * + * @param fileName File name to be escaped. + * @return An escaped file name which will be safe for use on at least FAT32 filesystems. + */ + public static String escapeFileName(String fileName) { + int length = fileName.length(); + int charactersToEscapeCount = 0; + for (int i = 0; i < length; i++) { + if (shouldEscapeCharacter(fileName.charAt(i))) { + charactersToEscapeCount++; + } + } + if (charactersToEscapeCount == 0) { + return fileName; + } + + int i = 0; + StringBuilder builder = new StringBuilder(length + charactersToEscapeCount * 2); + while (charactersToEscapeCount > 0) { + char c = fileName.charAt(i++); + if (shouldEscapeCharacter(c)) { + builder.append('%').append(Integer.toHexString(c)); + charactersToEscapeCount--; + } else { + builder.append(c); + } + } + if (i < length) { + builder.append(fileName, i, length); + } + return builder.toString(); + } + + private static boolean shouldEscapeCharacter(char c) { + switch (c) { + case '<': + case '>': + case ':': + case '"': + case '/': + case '\\': + case '|': + case '?': + case '*': + case '%': + return true; + default: + return false; + } + } + + /** + * Unescapes an escaped file or directory name back to its original value. + * + *

See {@link #escapeFileName(String)} for more information. + * + * @param fileName File name to be unescaped. + * @return The original value of the file name before it was escaped, + * or null if the escaped fileName seems invalid. + */ + public static String unescapeFileName(String fileName) { + int length = fileName.length(); + int percentCharacterCount = 0; + for (int i = 0; i < length; i++) { + if (fileName.charAt(i) == '%') { + percentCharacterCount++; + } + } + if (percentCharacterCount == 0) { + return fileName; + } + + int expectedLength = length - percentCharacterCount * 2; + StringBuilder builder = new StringBuilder(expectedLength); + Matcher matcher = ESCAPED_CHARACTER_PATTERN.matcher(fileName); + int endOfLastMatch = 0; + while (percentCharacterCount > 0 && matcher.find()) { + char unescapedCharacter = (char) Integer.parseInt(matcher.group(1), 16); + builder.append(fileName, endOfLastMatch, matcher.start()).append(unescapedCharacter); + endOfLastMatch = matcher.end(); + percentCharacterCount--; + } + if (endOfLastMatch < length) { + builder.append(fileName, endOfLastMatch, length); + } + if (builder.length() != expectedLength) { + return null; + } + return builder.toString(); + } +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/VerboseLogUtil.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/VerboseLogUtil.java new file mode 100755 index 000000000..3dc495a1f --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/VerboseLogUtil.java @@ -0,0 +1,81 @@ +/* + * 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.messenger.exoplayer.util; + +/** + * Utility class for managing a set of tags for which verbose logging should be enabled. + */ +public final class VerboseLogUtil { + + private static volatile String[] enabledTags; + private static volatile boolean enableAllTags; + + private VerboseLogUtil() {} + + /** + * Sets the tags for which verbose logging should be enabled. + * + * @param tags The set of tags. + */ + public static void setEnabledTags(String... tags) { + enabledTags = tags; + enableAllTags = false; + } + + /** + * Specifies whether or not all logging should be enabled. + * + * @param enable True if all logging should be enabled; false if only tags enabled by + * setEnabledTags should have logging enabled. + */ + public static void setEnableAllTags(boolean enable) { + enableAllTags = enable; + } + + /** + * Checks whether verbose logging should be output for a given tag. + * + * @param tag The tag. + * @return Whether verbose logging should be output for the tag. + */ + public static boolean isTagEnabled(String tag) { + if (enableAllTags) { + return true; + } + + // Take a local copy of the array to ensure thread safety. + String[] tags = enabledTags; + if (tags == null || tags.length == 0) { + return false; + } + for (int i = 0; i < tags.length; i++) { + if (tags[i].equals(tag)) { + return true; + } + } + return false; + } + + /** + * Checks whether all logging is enabled; + * + * @return True if all logging is enabled; false otherwise. + */ + public static boolean areAllTagsEnabled() { + return enableAllTags; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/extensions/Buffer.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/extensions/Buffer.java new file mode 100755 index 000000000..4454bf8a3 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/extensions/Buffer.java @@ -0,0 +1,46 @@ +/* + * 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.messenger.exoplayer.util.extensions; + +/** + * Base class for {@link Decoder} buffers with flags. + */ +public abstract class Buffer { + + /** + * Flag for empty input/output buffers that signal that the end of the stream was reached. + */ + public static final int FLAG_END_OF_STREAM = 1; + /** + * Flag for non-empty input/output buffers that should only be decoded (not rendered). + */ + public static final int FLAG_DECODE_ONLY = 2; + + private int flags; + + public void reset() { + flags = 0; + } + + public final void setFlag(int flag) { + flags |= flag; + } + + public final boolean getFlag(int flag) { + return (flags & flag) == flag; + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/extensions/Decoder.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/extensions/Decoder.java new file mode 100755 index 000000000..6b36e1f46 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/extensions/Decoder.java @@ -0,0 +1,63 @@ +/* + * 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.messenger.exoplayer.util.extensions; + +/** + * A media decoder. + * + * @param The type of buffer input to the decoder. + * @param The type of buffer output from the decoder. + * @param The type of exception thrown from the decoder. + */ +public interface Decoder { + + /** + * Dequeues the next input buffer to be filled and queued to the decoder. + * + * @return The input buffer, or null if an input buffer isn't available. + * @throws E If a decoder error has occurred. + */ + I dequeueInputBuffer() throws E; + + /** + * Queues an input buffer to the decoder. + * + * @param inputBuffer The input buffer. + * @throws E If a decoder error has occurred. + */ + void queueInputBuffer(I inputBuffer) throws E; + + /** + * Dequeues the next output buffer from the decoder. + * + * @return The output buffer, or null if an output buffer isn't available. + * @throws E If a decoder error has occurred. + */ + O dequeueOutputBuffer() throws E; + + /** + * Flushes input/output buffers that have not been dequeued yet and returns ownership of any + * dequeued input buffer to the decoder. Flushes any pending output currently in the decoder. The + * caller is still responsible for releasing any dequeued output buffers. + */ + void flush(); + + /** + * Releases the decoder. Must be called when the decoder is no longer needed. + */ + void release(); + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/extensions/InputBuffer.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/extensions/InputBuffer.java new file mode 100755 index 000000000..833638f57 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/extensions/InputBuffer.java @@ -0,0 +1,37 @@ +/* + * 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.messenger.exoplayer.util.extensions; + +import org.telegram.messenger.exoplayer.SampleHolder; + +/** + * Input buffer to be decoded by a {@link Decoder}. + */ +public class InputBuffer extends Buffer { + + public final SampleHolder sampleHolder; + + public InputBuffer() { + sampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_DIRECT); + } + + @Override + public void reset() { + super.reset(); + sampleHolder.clearData(); + } + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/extensions/OutputBuffer.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/extensions/OutputBuffer.java new file mode 100755 index 000000000..bc29630f1 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/extensions/OutputBuffer.java @@ -0,0 +1,33 @@ +/* + * 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.messenger.exoplayer.util.extensions; + +/** + * Output buffer decoded by a {@link Decoder}. + */ +public abstract class OutputBuffer extends Buffer { + + /** + * The presentation timestamp for the buffer, in microseconds. + */ + public long timestampUs; + + /** + * Releases the output buffer for reuse. Must be called when the buffer is no longer needed. + */ + public abstract void release(); + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/extensions/SimpleDecoder.java b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/extensions/SimpleDecoder.java new file mode 100755 index 000000000..86228aba3 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/exoplayer/util/extensions/SimpleDecoder.java @@ -0,0 +1,282 @@ +/* + * 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.messenger.exoplayer.util.extensions; + +import org.telegram.messenger.exoplayer.util.Assertions; + +import java.util.LinkedList; + +/** + * Base class for {@link Decoder}s that use their own decode thread. + */ +public abstract class SimpleDecoder extends Thread implements Decoder { + + /** + * Listener for {@link SimpleDecoder} events. + */ + public interface EventListener { + + /** + * Invoked when the decoder encounters an error. + * + * @param e The corresponding exception. + */ + void onDecoderError(E e); + + } + + private final Object lock; + private final LinkedList queuedInputBuffers; + private final LinkedList queuedOutputBuffers; + private final I[] availableInputBuffers; + private final O[] availableOutputBuffers; + + private int availableInputBufferCount; + private int availableOutputBufferCount; + private I dequeuedInputBuffer; + + private E exception; + private boolean flushed; + private boolean released; + + /** + * @param inputBuffers An array of nulls that will be used to store references to input buffers. + * @param outputBuffers An array of nulls that will be used to store references to output buffers. + */ + protected SimpleDecoder(I[] inputBuffers, O[] outputBuffers) { + lock = new Object(); + queuedInputBuffers = new LinkedList<>(); + queuedOutputBuffers = new LinkedList<>(); + availableInputBuffers = inputBuffers; + availableInputBufferCount = inputBuffers.length; + for (int i = 0; i < availableInputBufferCount; i++) { + availableInputBuffers[i] = createInputBuffer(); + } + availableOutputBuffers = outputBuffers; + availableOutputBufferCount = outputBuffers.length; + for (int i = 0; i < availableOutputBufferCount; i++) { + availableOutputBuffers[i] = createOutputBuffer(); + } + } + + /** + * Sets the initial size of each input buffer. + *

+ * This method should only be called before the decoder is used (i.e. before the first call to + * {@link #dequeueInputBuffer()}. + * + * @param size The required input buffer size. + */ + protected final void setInitialInputBufferSize(int size) { + Assertions.checkState(availableInputBufferCount == availableInputBuffers.length); + for (int i = 0; i < availableInputBuffers.length; i++) { + availableInputBuffers[i].sampleHolder.ensureSpaceForWrite(size); + } + } + + @Override + public final I dequeueInputBuffer() throws E { + synchronized (lock) { + maybeThrowException(); + Assertions.checkState(dequeuedInputBuffer == null); + if (availableInputBufferCount == 0) { + return null; + } + I inputBuffer = availableInputBuffers[--availableInputBufferCount]; + inputBuffer.reset(); + dequeuedInputBuffer = inputBuffer; + return inputBuffer; + } + } + + @Override + public final void queueInputBuffer(I inputBuffer) throws E { + synchronized (lock) { + maybeThrowException(); + Assertions.checkArgument(inputBuffer == dequeuedInputBuffer); + queuedInputBuffers.addLast(inputBuffer); + maybeNotifyDecodeLoop(); + dequeuedInputBuffer = null; + } + } + + @Override + public final O dequeueOutputBuffer() throws E { + synchronized (lock) { + maybeThrowException(); + if (queuedOutputBuffers.isEmpty()) { + return null; + } + return queuedOutputBuffers.removeFirst(); + } + } + + /** + * Releases an output buffer back to the decoder. + * + * @param outputBuffer The output buffer being released. + */ + protected void releaseOutputBuffer(O outputBuffer) { + synchronized (lock) { + availableOutputBuffers[availableOutputBufferCount++] = outputBuffer; + maybeNotifyDecodeLoop(); + } + } + + @Override + public final void flush() { + synchronized (lock) { + flushed = true; + if (dequeuedInputBuffer != null) { + availableInputBuffers[availableInputBufferCount++] = dequeuedInputBuffer; + dequeuedInputBuffer = null; + } + while (!queuedInputBuffers.isEmpty()) { + availableInputBuffers[availableInputBufferCount++] = queuedInputBuffers.removeFirst(); + } + while (!queuedOutputBuffers.isEmpty()) { + availableOutputBuffers[availableOutputBufferCount++] = queuedOutputBuffers.removeFirst(); + } + } + } + + @Override + public void release() { + synchronized (lock) { + released = true; + lock.notify(); + } + try { + join(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + /** + * Throws a decode exception, if there is one. + * + * @throws E The decode exception. + */ + private void maybeThrowException() throws E { + if (exception != null) { + throw exception; + } + } + + /** + * Notifies the decode loop if there exists a queued input buffer and an available output buffer + * to decode into. + *

+ * Should only be called whilst synchronized on the lock object. + */ + private void maybeNotifyDecodeLoop() { + if (canDecodeBuffer()) { + lock.notify(); + } + } + + @Override + public final void run() { + try { + while (decode()) { + // Do nothing. + } + } catch (InterruptedException e) { + // Not expected. + throw new IllegalStateException(e); + } + } + + private boolean decode() throws InterruptedException { + I inputBuffer; + O outputBuffer; + boolean resetDecoder; + + // Wait until we have an input buffer to decode, and an output buffer to decode into. + synchronized (lock) { + while (!released && !canDecodeBuffer()) { + lock.wait(); + } + if (released) { + return false; + } + inputBuffer = queuedInputBuffers.removeFirst(); + outputBuffer = availableOutputBuffers[--availableOutputBufferCount]; + resetDecoder = flushed; + flushed = false; + } + + outputBuffer.reset(); + if (inputBuffer.getFlag(Buffer.FLAG_END_OF_STREAM)) { + outputBuffer.setFlag(Buffer.FLAG_END_OF_STREAM); + } else { + if (inputBuffer.getFlag(Buffer.FLAG_DECODE_ONLY)) { + outputBuffer.setFlag(Buffer.FLAG_DECODE_ONLY); + } + exception = decode(inputBuffer, outputBuffer, resetDecoder); + if (exception != null) { + // Memory barrier to ensure that the decoder exception is visible from the playback thread. + synchronized (lock) {} + return false; + } + } + + synchronized (lock) { + if (flushed || outputBuffer.getFlag(Buffer.FLAG_DECODE_ONLY)) { + // If a flush occurred while decoding or the buffer was only for decoding (not presentation) + // then make the output buffer available again rather than queueing it to be consumed. + availableOutputBuffers[availableOutputBufferCount++] = outputBuffer; + } else { + // Queue the decoded output buffer to be consumed. + queuedOutputBuffers.addLast(outputBuffer); + } + // Make the input buffer available again. + availableInputBuffers[availableInputBufferCount++] = inputBuffer; + } + + return true; + } + + private boolean canDecodeBuffer() { + return !queuedInputBuffers.isEmpty() && availableOutputBufferCount > 0; + } + + /** + * Creates a new input buffer. + */ + protected abstract I createInputBuffer(); + + /** + * Creates a new output buffer. + */ + protected abstract O createOutputBuffer(); + + /** + * Decodes the {@code inputBuffer} and stores any decoded output in {@code outputBuffer}. + * + * @param inputBuffer The buffer to decode. + * @param outputBuffer The output buffer to store decoded data. The flag + * {@link Buffer#FLAG_DECODE_ONLY} will be set if the same flag is set on {@code inputBuffer}, + * but the decoder may set/unset the flag if required. If the flag is set after this method + * returns, any output should not be presented. + * @param reset True if the decoder must be reset before decoding. + * @return A decoder exception if an error occurred, or null if decoding was successful. + */ + protected abstract E decode(I inputBuffer, O outputBuffer, boolean reset); + +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/query/DraftQuery.java b/TMessagesProj/src/main/java/org/telegram/messenger/query/DraftQuery.java new file mode 100644 index 000000000..bfd6e574b --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/messenger/query/DraftQuery.java @@ -0,0 +1,315 @@ +/* + * This is the source code of Telegram for Android v. 3.x.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-2016. + */ + +package org.telegram.messenger.query; + +import android.app.Activity; +import android.content.SharedPreferences; +import android.text.TextUtils; + +import org.telegram.SQLite.SQLiteCursor; +import org.telegram.messenger.AndroidUtilities; +import org.telegram.messenger.ApplicationLoader; +import org.telegram.messenger.ChatObject; +import org.telegram.messenger.FileLog; +import org.telegram.messenger.MessagesController; +import org.telegram.messenger.MessagesStorage; +import org.telegram.messenger.NotificationCenter; +import org.telegram.messenger.UserConfig; +import org.telegram.messenger.Utilities; +import org.telegram.tgnet.ConnectionsManager; +import org.telegram.tgnet.NativeByteBuffer; +import org.telegram.tgnet.RequestDelegate; +import org.telegram.tgnet.SerializedData; +import org.telegram.tgnet.TLObject; +import org.telegram.tgnet.TLRPC; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +public class DraftQuery { + + private static HashMap drafts = new HashMap<>(); + private static HashMap draftMessages = new HashMap<>(); + private static boolean inTransaction; + private static SharedPreferences preferences; + private static boolean loadingDrafts; + + static { + preferences = ApplicationLoader.applicationContext.getSharedPreferences("drafts", Activity.MODE_PRIVATE); + Map values = preferences.getAll(); + for (Map.Entry entry : values.entrySet()) { + try { + String key = entry.getKey(); + long did = Utilities.parseLong(key); + byte[] bytes = Utilities.hexToBytes((String) entry.getValue()); + SerializedData serializedData = new SerializedData(bytes); + if (key.startsWith("r_")) { + TLRPC.Message message = TLRPC.Message.TLdeserialize(serializedData, serializedData.readInt32(true), true); + if (message != null) { + draftMessages.put(did, message); + } + } else { + TLRPC.DraftMessage draftMessage = TLRPC.DraftMessage.TLdeserialize(serializedData, serializedData.readInt32(true), true); + if (draftMessage != null) { + drafts.put(did, draftMessage); + } + } + } catch (Exception e) { + //igonre + } + } + } + + public static void loadDrafts() { + if (UserConfig.draftsLoaded || loadingDrafts) { + return; + } + loadingDrafts = true; + TLRPC.TL_messages_getAllDrafts req = new TLRPC.TL_messages_getAllDrafts(); + ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { + @Override + public void run(TLObject response, TLRPC.TL_error error) { + if (error != null) { + return; + } + MessagesController.getInstance().processUpdates((TLRPC.Updates) response, false); + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + UserConfig.draftsLoaded = true; + loadingDrafts = false; + UserConfig.saveConfig(false); + } + }); + } + }); + } + + public static void cleanup() { + drafts.clear(); + draftMessages.clear(); + preferences.edit().clear().commit(); + } + + public static TLRPC.DraftMessage getDraft(long did) { + return drafts.get(did); + } + + public static TLRPC.Message getDraftMessage(long did) { + return draftMessages.get(did); + } + + public static void saveDraft(long did, CharSequence message, ArrayList entities, TLRPC.Message replyToMessage, boolean noWebpage) { + saveDraft(did, message, entities, replyToMessage, noWebpage, false); + } + + public static void saveDraft(long did, CharSequence message, ArrayList entities, TLRPC.Message replyToMessage, boolean noWebpage, boolean clean) { + TLRPC.DraftMessage draftMessage; + if (!TextUtils.isEmpty(message) || replyToMessage != null) { + draftMessage = new TLRPC.TL_draftMessage(); + } else { + draftMessage = new TLRPC.TL_draftMessageEmpty(); + } + draftMessage.date = (int) (System.currentTimeMillis() / 1000); + draftMessage.message = message == null ? "" : message.toString(); + draftMessage.no_webpage = noWebpage; + if (replyToMessage != null) { + draftMessage.reply_to_msg_id = replyToMessage.id; + draftMessage.flags |= 1; + } + if (entities != null && !entities.isEmpty()) { + draftMessage.entities = entities; + draftMessage.flags |= 8; + } + + TLRPC.DraftMessage currentDraft = drafts.get(did); + if (!clean) { + if (currentDraft != null && currentDraft.message.equals(draftMessage.message) && currentDraft.reply_to_msg_id == draftMessage.reply_to_msg_id && currentDraft.no_webpage == draftMessage.no_webpage || + currentDraft == null && TextUtils.isEmpty(draftMessage.message) && draftMessage.reply_to_msg_id == 0) { + return; + } + } + + saveDraft(did, draftMessage, replyToMessage, false); + int lower_id = (int) did; + if (lower_id != 0) { + TLRPC.TL_messages_saveDraft req = new TLRPC.TL_messages_saveDraft(); + req.peer = MessagesController.getInputPeer(lower_id); + if (req.peer == null) { + return; + } + req.message = draftMessage.message; + req.no_webpage = draftMessage.no_webpage; + req.reply_to_msg_id = draftMessage.reply_to_msg_id; + req.entities = draftMessage.entities; + req.flags = draftMessage.flags; + ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { + @Override + public void run(TLObject response, TLRPC.TL_error error) { + + } + }); + } + MessagesController.getInstance().sortDialogs(null); + NotificationCenter.getInstance().postNotificationName(NotificationCenter.dialogsNeedReload); + } + + public static void saveDraft(final long did, TLRPC.DraftMessage draft, TLRPC.Message replyToMessage, boolean fromServer) { + SharedPreferences.Editor editor = preferences.edit(); + if (draft == null || draft instanceof TLRPC.TL_draftMessageEmpty) { + drafts.remove(did); + draftMessages.remove(did); + preferences.edit().remove("" + did).remove("r_" + did).commit(); + } else { + drafts.put(did, draft); + try { + SerializedData serializedData = new SerializedData(draft.getObjectSize()); + draft.serializeToStream(serializedData); + editor.putString("" + did, Utilities.bytesToHex(serializedData.toByteArray())); + } catch (Exception e) { + FileLog.e("tmessages", e); + } + } + if (replyToMessage == null) { + draftMessages.remove(did); + editor.remove("r_" + did); + } else { + draftMessages.put(did, replyToMessage); + SerializedData serializedData = new SerializedData(replyToMessage.getObjectSize()); + replyToMessage.serializeToStream(serializedData); + editor.putString("r_" + did, Utilities.bytesToHex(serializedData.toByteArray())); + } + editor.commit(); + if (fromServer) { + if (draft.reply_to_msg_id != 0 && replyToMessage == null) { + int lower_id = (int) did; + TLRPC.User user = null; + TLRPC.Chat chat = null; + if (lower_id > 0) { + user = MessagesController.getInstance().getUser(lower_id); + } else { + chat = MessagesController.getInstance().getChat(-lower_id); + } + if (user != null || chat != null) { + long messageId = draft.reply_to_msg_id; + final int channelIdFinal; + if (ChatObject.isChannel(chat)) { + messageId |= ((long) chat.id) << 32; + channelIdFinal = chat.id; + } else { + channelIdFinal = 0; + } + final long messageIdFinal = messageId; + + MessagesStorage.getInstance().getStorageQueue().postRunnable(new Runnable() { + @Override + public void run() { + try { + TLRPC.Message message = null; + SQLiteCursor cursor = MessagesStorage.getInstance().getDatabase().queryFinalized(String.format(Locale.US, "SELECT data FROM messages WHERE mid = %d", messageIdFinal)); + if (cursor.next()) { + NativeByteBuffer data = cursor.byteBufferValue(0); + if (data != null) { + message = TLRPC.Message.TLdeserialize(data, data.readInt32(false), false); + data.reuse(); + } + } + cursor.dispose(); + if (message == null) { + if (channelIdFinal != 0) { + final TLRPC.TL_channels_getMessages req = new TLRPC.TL_channels_getMessages(); + req.channel = MessagesController.getInputChannel(channelIdFinal); + req.id.add((int) messageIdFinal); + ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { + @Override + public void run(TLObject response, TLRPC.TL_error error) { + if (error == null) { + TLRPC.messages_Messages messagesRes = (TLRPC.messages_Messages) response; + if (!messagesRes.messages.isEmpty()) { + saveDraftReplyMessage(did, messagesRes.messages.get(0)); + } + } + } + }); + } else { + TLRPC.TL_messages_getMessages req = new TLRPC.TL_messages_getMessages(); + req.id.add((int) messageIdFinal); + ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { + @Override + public void run(TLObject response, TLRPC.TL_error error) { + if (error == null) { + TLRPC.messages_Messages messagesRes = (TLRPC.messages_Messages) response; + if (!messagesRes.messages.isEmpty()) { + saveDraftReplyMessage(did, messagesRes.messages.get(0)); + } + } + } + }); + } + } else { + saveDraftReplyMessage(did, message); + } + } catch (Exception e) { + FileLog.e("tmessages", e); + } + } + }); + } + } + NotificationCenter.getInstance().postNotificationName(NotificationCenter.newDraftReceived, did); + } + } + + private static void saveDraftReplyMessage(final long did, final TLRPC.Message message) { + if (message == null) { + return; + } + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + TLRPC.DraftMessage draftMessage = drafts.get(did); + if (draftMessage != null && draftMessage.reply_to_msg_id == message.id) { + draftMessages.put(did, message); + SerializedData serializedData = new SerializedData(message.getObjectSize()); + message.serializeToStream(serializedData); + preferences.edit().putString("r_" + did, Utilities.bytesToHex(serializedData.toByteArray())).commit(); + NotificationCenter.getInstance().postNotificationName(NotificationCenter.newDraftReceived, did); + } + } + }); + } + + public static void cleanDraft(long did, boolean replyOnly) { + TLRPC.DraftMessage draftMessage = drafts.get(did); + if (draftMessage == null) { + return; + } + if (!replyOnly) { + drafts.remove(did); + draftMessages.remove(did); + preferences.edit().remove("" + did).remove("r_" + did).commit(); + MessagesController.getInstance().sortDialogs(null); + NotificationCenter.getInstance().postNotificationName(NotificationCenter.dialogsNeedReload); + } else if (draftMessage.reply_to_msg_id != 0) { + draftMessage.reply_to_msg_id = 0; + draftMessage.flags &= ~1; + saveDraft(did, draftMessage.message, draftMessage.entities, null, draftMessage.no_webpage, true); + } + } + + public static void beginTransaction() { + inTransaction = true; + } + + public static void endTransaction() { + inTransaction = false; + } +} diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/query/MessagesQuery.java b/TMessagesProj/src/main/java/org/telegram/messenger/query/MessagesQuery.java index c9291a278..6f0a230c7 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/query/MessagesQuery.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/query/MessagesQuery.java @@ -8,6 +8,7 @@ package org.telegram.messenger.query; +import android.text.Spannable; import android.text.TextUtils; import org.telegram.SQLite.SQLiteCursor; @@ -19,11 +20,13 @@ import org.telegram.messenger.MessagesController; import org.telegram.messenger.MessagesStorage; import org.telegram.messenger.NotificationCenter; import org.telegram.messenger.FileLog; +import org.telegram.messenger.Utilities; import org.telegram.tgnet.ConnectionsManager; import org.telegram.tgnet.NativeByteBuffer; import org.telegram.tgnet.RequestDelegate; import org.telegram.tgnet.TLObject; import org.telegram.tgnet.TLRPC; +import org.telegram.ui.Components.URLSpanUserMention; import java.util.ArrayList; import java.util.HashMap; @@ -436,4 +439,31 @@ public class MessagesQuery { } }); } + + public static ArrayList getEntities(CharSequence message) { + if (message == null) { + return null; + } + ArrayList entities = null; + if (message instanceof Spannable) { + Spannable spannable = (Spannable) message; + URLSpanUserMention spans[] = spannable.getSpans(0, message.length(), URLSpanUserMention.class); + if (spans != null && spans.length > 0) { + entities = new ArrayList<>(); + for (int b = 0; b < spans.length; b++) { + TLRPC.TL_inputMessageEntityMentionName entity = new TLRPC.TL_inputMessageEntityMentionName(); + entity.user_id = MessagesController.getInputUser(Utilities.parseInt(spans[b].getURL())); + if (entity.user_id != null) { + entity.offset = spannable.getSpanStart(spans[b]); + entity.length = Math.min(spannable.getSpanEnd(spans[b]), message.length()) - entity.offset; + if (message.charAt(entity.offset + entity.length - 1) == ' ') { + entity.length--; + } + entities.add(entity); + } + } + } + } + return entities; + } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/query/MessagesSearchQuery.java b/TMessagesProj/src/main/java/org/telegram/messenger/query/MessagesSearchQuery.java index 551327785..33fb1ce6b 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/query/MessagesSearchQuery.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/query/MessagesSearchQuery.java @@ -24,7 +24,10 @@ import java.util.ArrayList; public class MessagesSearchQuery { private static int reqId; + private static int mergeReqId; + private static long lastMergeDialogId; private static int lastReqId; + private static int messagesSearchCount[] = new int[] {0, 0}; private static boolean messagesSearchEndReached[] = new boolean[] {false, false}; private static ArrayList searchResultMessages = new ArrayList<>(); private static String lastSearchQuery; @@ -41,28 +44,38 @@ public class MessagesSearchQuery { return mask; } - public static void searchMessagesInChat(String query, final long dialog_id, final long mergeDialogId, final int guid, int direction) { + public static void searchMessagesInChat(String query, final long dialog_id, final long mergeDialogId, final int guid, final int direction) { + searchMessagesInChat(query, dialog_id, mergeDialogId, guid, direction, false); + } + + private static void searchMessagesInChat(String query, final long dialog_id, final long mergeDialogId, final int guid, final int direction, final boolean internal) { + int max_id = 0; + long queryWithDialog = dialog_id; + boolean firstQuery = !internal; if (reqId != 0) { ConnectionsManager.getInstance().cancelRequest(reqId, true); reqId = 0; } - int max_id = 0; - long queryWithDialog = dialog_id; + if (mergeReqId != 0) { + ConnectionsManager.getInstance().cancelRequest(mergeReqId, true); + mergeReqId = 0; + } if (query == null || query.length() == 0) { + if (searchResultMessages.isEmpty()) { + return; + } if (direction == 1) { lastReturnedNum++; if (lastReturnedNum < searchResultMessages.size()) { MessageObject messageObject = searchResultMessages.get(lastReturnedNum); - NotificationCenter.getInstance().postNotificationName(NotificationCenter.chatSearchResultsAvailable, guid, messageObject.getId(), getMask(), messageObject.getDialogId()); + NotificationCenter.getInstance().postNotificationName(NotificationCenter.chatSearchResultsAvailable, guid, messageObject.getId(), getMask(), messageObject.getDialogId(), lastReturnedNum, messagesSearchCount[0] + messagesSearchCount[1]); return; } else { if (messagesSearchEndReached[0] && mergeDialogId == 0 || messagesSearchEndReached[1]) { lastReturnedNum--; return; } - if (searchResultMessages.isEmpty()) { - return; - } + firstQuery = false; query = lastSearchQuery; MessageObject messageObject = searchResultMessages.get(searchResultMessages.size() - 1); if (messageObject.getDialogId() == dialog_id && !messagesSearchEndReached[0]) { @@ -86,7 +99,7 @@ public class MessagesSearchQuery { lastReturnedNum = searchResultMessages.size() - 1; } MessageObject messageObject = searchResultMessages.get(lastReturnedNum); - NotificationCenter.getInstance().postNotificationName(NotificationCenter.chatSearchResultsAvailable, guid, messageObject.getId(), getMask(), messageObject.getDialogId()); + NotificationCenter.getInstance().postNotificationName(NotificationCenter.chatSearchResultsAvailable, guid, messageObject.getId(), getMask(), messageObject.getDialogId(), lastReturnedNum, messagesSearchCount[0] + messagesSearchCount[1]); return; } else { return; @@ -95,13 +108,50 @@ public class MessagesSearchQuery { if (messagesSearchEndReached[0] && !messagesSearchEndReached[1] && mergeDialogId != 0) { queryWithDialog = mergeDialogId; } + if (queryWithDialog == dialog_id && firstQuery) { + if (mergeDialogId != 0) { + TLRPC.InputPeer inputPeer = MessagesController.getInputPeer((int) mergeDialogId); + if (inputPeer == null) { + return; + } + final TLRPC.TL_messages_search req = new TLRPC.TL_messages_search(); + req.peer = inputPeer; + lastMergeDialogId = mergeDialogId; + req.limit = 1; + req.q = query; + req.filter = new TLRPC.TL_inputMessagesFilterEmpty(); + mergeReqId = ConnectionsManager.getInstance().sendRequest(req, new RequestDelegate() { + @Override + public void run(final TLObject response, final TLRPC.TL_error error) { + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + if (lastMergeDialogId == mergeDialogId) { + mergeReqId = 0; + if (response != null) { + TLRPC.messages_Messages res = (TLRPC.messages_Messages) response; + messagesSearchEndReached[1] = res.messages.isEmpty(); + messagesSearchCount[1] = res instanceof TLRPC.TL_messages_messagesSlice ? res.count : res.messages.size(); + searchMessagesInChat(req.q, dialog_id, mergeDialogId, guid, direction, true); + } + } + } + }); + } + }, ConnectionsManager.RequestFlagFailOnServerErrors); + return; + } else { + lastMergeDialogId = 0; + messagesSearchEndReached[1] = true; + messagesSearchCount[1] = 0; + } + } final TLRPC.TL_messages_search req = new TLRPC.TL_messages_search(); - req.limit = 21; - int lower_part = (int) queryWithDialog; - req.peer = MessagesController.getInputPeer(lower_part); + req.peer = MessagesController.getInputPeer((int) queryWithDialog); if (req.peer == null) { return; } + req.limit = 21; req.q = query; req.max_id = max_id; req.filter = new TLRPC.TL_inputMessagesFilterEmpty(); @@ -114,9 +164,9 @@ public class MessagesSearchQuery { AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { - reqId = 0; if (currentReqId == lastReqId) { - if (error == null) { + reqId = 0; + if (response != null) { TLRPC.messages_Messages res = (TLRPC.messages_Messages) response; MessagesStorage.getInstance().putUsersAndChats(res.users, res.chats, true, true); MessagesController.getInstance().putUsers(res.users, false); @@ -124,6 +174,7 @@ public class MessagesSearchQuery { if (req.max_id == 0 && queryWithDialogFinal == dialog_id) { lastReturnedNum = 0; searchResultMessages.clear(); + messagesSearchCount[0] = 0; } boolean added = false; for (int a = 0; a < Math.min(res.messages.size(), 20); a++) { @@ -132,23 +183,20 @@ public class MessagesSearchQuery { searchResultMessages.add(new MessageObject(message, null, false)); } messagesSearchEndReached[queryWithDialogFinal == dialog_id ? 0 : 1] = res.messages.size() != 21; - if (mergeDialogId == 0) { - messagesSearchEndReached[1] = messagesSearchEndReached[0]; - } + messagesSearchCount[queryWithDialogFinal == dialog_id ? 0 : 1] = res instanceof TLRPC.TL_messages_messagesSlice ? res.count : res.messages.size(); if (searchResultMessages.isEmpty()) { - NotificationCenter.getInstance().postNotificationName(NotificationCenter.chatSearchResultsAvailable, guid, 0, getMask(), (long) 0); + NotificationCenter.getInstance().postNotificationName(NotificationCenter.chatSearchResultsAvailable, guid, 0, getMask(), (long) 0, 0, 0); } else { if (added) { if (lastReturnedNum >= searchResultMessages.size()) { lastReturnedNum = searchResultMessages.size() - 1; } MessageObject messageObject = searchResultMessages.get(lastReturnedNum); - NotificationCenter.getInstance().postNotificationName(NotificationCenter.chatSearchResultsAvailable, guid, messageObject.getId(), getMask(), messageObject.getDialogId()); + NotificationCenter.getInstance().postNotificationName(NotificationCenter.chatSearchResultsAvailable, guid, messageObject.getId(), getMask(), messageObject.getDialogId(), lastReturnedNum, messagesSearchCount[0] + messagesSearchCount[1]); } } - if (queryWithDialogFinal == dialog_id && messagesSearchEndReached[0] && mergeDialogId != 0) { - messagesSearchEndReached[1] = false; - searchMessagesInChat(lastSearchQuery, dialog_id, mergeDialogId, guid, 0); + if (queryWithDialogFinal == dialog_id && messagesSearchEndReached[0] && mergeDialogId != 0 && !messagesSearchEndReached[1]) { + searchMessagesInChat(lastSearchQuery, dialog_id, mergeDialogId, guid, 0, true); } } } @@ -157,4 +205,8 @@ public class MessagesSearchQuery { } }, ConnectionsManager.RequestFlagFailOnServerErrors); } + + public static String getLastSearchQuery() { + return lastSearchQuery; + } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/query/SearchQuery.java b/TMessagesProj/src/main/java/org/telegram/messenger/query/SearchQuery.java index 6f4982c1b..f4e050420 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/query/SearchQuery.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/query/SearchQuery.java @@ -37,7 +37,7 @@ public class SearchQuery { private static boolean loaded; private static boolean loading; - public static void cleanUp() { + public static void cleanup() { loading = false; loaded = false; hints.clear(); diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/query/StickersQuery.java b/TMessagesProj/src/main/java/org/telegram/messenger/query/StickersQuery.java index 922eee48d..74fbcb5ad 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/query/StickersQuery.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/query/StickersQuery.java @@ -155,7 +155,10 @@ public class StickersQuery { if (!stickersByEmoji.containsKey(id)) { stickersByEmoji.put(id, stickerPack.emoticon); } - arrayList.add(stickersById.get(id)); + TLRPC.Document sticker = stickersById.get(id); + if (sticker != null) { + arrayList.add(sticker); + } } } loadHash = calcStickersHash(stickerSets); @@ -404,7 +407,10 @@ public class StickersQuery { if (!stickersByEmojiNew.containsKey(id)) { stickersByEmojiNew.put(id, stickerPack.emoticon); } - arrayList.add(stickersByIdNew.get(id)); + TLRPC.Document sticker = stickersByIdNew.get(id); + if (sticker != null) { + arrayList.add(sticker); + } } } } diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/support/widget/RecyclerView.java b/TMessagesProj/src/main/java/org/telegram/messenger/support/widget/RecyclerView.java index c3e33d5ac..3e46bc8a4 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/support/widget/RecyclerView.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/support/widget/RecyclerView.java @@ -4667,7 +4667,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro if (offsetPosition < 0 || offsetPosition >= mAdapter.getItemCount()) { throw new IndexOutOfBoundsException("Inconsistency detected. Invalid item " + "position " + position + "(offset:" + offsetPosition + ")." - + "state:" + mState.getItemCount()); + + "state:" + mState.getItemCount() + " tag " + getTag() + " adapter " + getAdapter()); } final int type = mAdapter.getItemViewType(offsetPosition); diff --git a/TMessagesProj/src/main/java/org/telegram/tgnet/ConnectionsManager.java b/TMessagesProj/src/main/java/org/telegram/tgnet/ConnectionsManager.java index 1f4d26f16..c0e032609 100644 --- a/TMessagesProj/src/main/java/org/telegram/tgnet/ConnectionsManager.java +++ b/TMessagesProj/src/main/java/org/telegram/tgnet/ConnectionsManager.java @@ -170,7 +170,7 @@ public class ConnectionsManager { native_cancelRequest(token, notifyServer); } - public void cleanUp() { + public void cleanup() { native_cleanUp(); } diff --git a/TMessagesProj/src/main/java/org/telegram/tgnet/TLRPC.java b/TMessagesProj/src/main/java/org/telegram/tgnet/TLRPC.java index efc3a381d..fc2342996 100644 --- a/TMessagesProj/src/main/java/org/telegram/tgnet/TLRPC.java +++ b/TMessagesProj/src/main/java/org/telegram/tgnet/TLRPC.java @@ -57,7 +57,95 @@ public class TLRPC { public static final int MESSAGE_FLAG_EDITED = 0x00008000; public static final int MESSAGE_FLAG_MEGAGROUP = 0x80000000; - public static final int LAYER = 52; + public static final int LAYER = 53; + + public static class DraftMessage extends TLObject { + public int flags; + public boolean no_webpage; + public int reply_to_msg_id; + public String message; + public ArrayList entities = new ArrayList<>(); + public int date; + + public static DraftMessage TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) { + DraftMessage result = null; + switch(constructor) { + case 0xba4baec5: + result = new TL_draftMessageEmpty(); + break; + case 0xfd8e711f: + result = new TL_draftMessage(); + break; + } + if (result == null && exception) { + throw new RuntimeException(String.format("can't parse magic %x in DraftMessage", constructor)); + } + if (result != null) { + result.readParams(stream, exception); + } + return result; + } + } + + public static class TL_draftMessageEmpty extends DraftMessage { + public static int constructor = 0xba4baec5; + + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + } + } + + public static class TL_draftMessage extends DraftMessage { + public static int constructor = 0xfd8e711f; + + + public void readParams(AbstractSerializedData stream, boolean exception) { + flags = stream.readInt32(exception); + no_webpage = (flags & 2) != 0; + if ((flags & 1) != 0) { + reply_to_msg_id = stream.readInt32(exception); + } + message = stream.readString(exception); + if ((flags & 8) != 0) { + int magic = stream.readInt32(exception); + if (magic != 0x1cb5c415) { + if (exception) { + throw new RuntimeException(String.format("wrong Vector magic, got %x", magic)); + } + return; + } + int count = stream.readInt32(exception); + for (int a = 0; a < count; a++) { + MessageEntity object = MessageEntity.TLdeserialize(stream, stream.readInt32(exception), exception); + if (object == null) { + return; + } + entities.add(object); + } + } + date = stream.readInt32(exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + flags = no_webpage ? (flags | 2) : (flags &~ 2); + stream.writeInt32(flags); + if ((flags & 1) != 0) { + stream.writeInt32(reply_to_msg_id); + } + stream.writeString(message); + if ((flags & 8) != 0) { + stream.writeInt32(0x1cb5c415); + int count = entities.size(); + stream.writeInt32(count); + for (int a = 0; a < count; a++) { + entities.get(a).serializeToStream(stream); + } + } + stream.writeInt32(date); + } + } public static class ChatPhoto extends TLObject { public FileLocation photo_small; @@ -747,7 +835,6 @@ public class TLRPC { public int flags; public int pts; public int count; - public ArrayList collapsed = new ArrayList<>(); public static messages_Messages TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) { messages_Messages result = null; @@ -755,7 +842,7 @@ public class TLRPC { case 0x8c718e87: result = new TL_messages_messages(); break; - case 0xbc0f17bc: + case 0x99262e37: result = new TL_messages_channelMessages(); break; case 0xb446ae3: @@ -847,111 +934,86 @@ public class TLRPC { } } - public static class TL_messages_channelMessages extends messages_Messages { - public static int constructor = 0xbc0f17bc; + public static class TL_messages_channelMessages extends messages_Messages { + public static int constructor = 0x99262e37; - public void readParams(AbstractSerializedData stream, boolean exception) { - flags = stream.readInt32(exception); - pts = stream.readInt32(exception); - count = stream.readInt32(exception); - int magic = stream.readInt32(exception); - if (magic != 0x1cb5c415) { - if (exception) { - throw new RuntimeException(String.format("wrong Vector magic, got %x", magic)); - } - return; - } - int count = stream.readInt32(exception); - for (int a = 0; a < count; a++) { - Message object = Message.TLdeserialize(stream, stream.readInt32(exception), exception); - if (object == null) { - return; - } - messages.add(object); - } - if ((flags & 1) != 0) { - magic = stream.readInt32(exception); - if (magic != 0x1cb5c415) { - if (exception) { - throw new RuntimeException(String.format("wrong Vector magic, got %x", magic)); - } - return; - } - count = stream.readInt32(exception); - for (int a = 0; a < count; a++) { - TL_messageGroup object = TL_messageGroup.TLdeserialize(stream, stream.readInt32(exception), exception); - if (object == null) { - return; - } - collapsed.add(object); - } - } - magic = stream.readInt32(exception); - if (magic != 0x1cb5c415) { - if (exception) { - throw new RuntimeException(String.format("wrong Vector magic, got %x", magic)); - } - return; - } - count = stream.readInt32(exception); - for (int a = 0; a < count; a++) { - Chat object = Chat.TLdeserialize(stream, stream.readInt32(exception), exception); - if (object == null) { - return; - } - chats.add(object); - } - magic = stream.readInt32(exception); - if (magic != 0x1cb5c415) { - if (exception) { - throw new RuntimeException(String.format("wrong Vector magic, got %x", magic)); - } - return; - } - count = stream.readInt32(exception); - for (int a = 0; a < count; a++) { - User object = User.TLdeserialize(stream, stream.readInt32(exception), exception); - if (object == null) { - return; - } - users.add(object); - } - } + public void readParams(AbstractSerializedData stream, boolean exception) { + flags = stream.readInt32(exception); + pts = stream.readInt32(exception); + count = stream.readInt32(exception); + int magic = stream.readInt32(exception); + if (magic != 0x1cb5c415) { + if (exception) { + throw new RuntimeException(String.format("wrong Vector magic, got %x", magic)); + } + return; + } + int count = stream.readInt32(exception); + for (int a = 0; a < count; a++) { + Message object = Message.TLdeserialize(stream, stream.readInt32(exception), exception); + if (object == null) { + return; + } + messages.add(object); + } + magic = stream.readInt32(exception); + if (magic != 0x1cb5c415) { + if (exception) { + throw new RuntimeException(String.format("wrong Vector magic, got %x", magic)); + } + return; + } + count = stream.readInt32(exception); + for (int a = 0; a < count; a++) { + Chat object = Chat.TLdeserialize(stream, stream.readInt32(exception), exception); + if (object == null) { + return; + } + chats.add(object); + } + magic = stream.readInt32(exception); + if (magic != 0x1cb5c415) { + if (exception) { + throw new RuntimeException(String.format("wrong Vector magic, got %x", magic)); + } + return; + } + count = stream.readInt32(exception); + for (int a = 0; a < count; a++) { + User object = User.TLdeserialize(stream, stream.readInt32(exception), exception); + if (object == null) { + return; + } + users.add(object); + } + } - public void serializeToStream(AbstractSerializedData stream) { - stream.writeInt32(constructor); - stream.writeInt32(flags); - stream.writeInt32(pts); - stream.writeInt32(count); - stream.writeInt32(0x1cb5c415); - int count = messages.size(); - stream.writeInt32(count); - for (int a = 0; a < count; a++) { - messages.get(a).serializeToStream(stream); - } - if ((flags & 1) != 0) { - stream.writeInt32(0x1cb5c415); - count = collapsed.size(); - stream.writeInt32(count); - for (int a = 0; a < count; a++) { - collapsed.get(a).serializeToStream(stream); - } - } - stream.writeInt32(0x1cb5c415); - count = chats.size(); - stream.writeInt32(count); - for (int a = 0; a < count; a++) { - chats.get(a).serializeToStream(stream); - } - stream.writeInt32(0x1cb5c415); - count = users.size(); - stream.writeInt32(count); - for (int a = 0; a < count; a++) { - users.get(a).serializeToStream(stream); - } - } - } + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeInt32(flags); + stream.writeInt32(pts); + stream.writeInt32(count); + stream.writeInt32(0x1cb5c415); + int count = messages.size(); + stream.writeInt32(count); + for (int a = 0; a < count; a++) { + messages.get(a).serializeToStream(stream); + } + stream.writeInt32(0x1cb5c415); + count = chats.size(); + stream.writeInt32(count); + for (int a = 0; a < count; a++) { + chats.get(a).serializeToStream(stream); + } + stream.writeInt32(0x1cb5c415); + count = users.size(); + stream.writeInt32(count); + for (int a = 0; a < count; a++) { + users.get(a).serializeToStream(stream); + } + } + } public static class TL_messages_messagesSlice extends messages_Messages { public static int constructor = 0xb446ae3; @@ -2767,8 +2829,8 @@ public class TLRPC { public void serializeToStream(AbstractSerializedData stream) { stream.writeInt32(constructor); - stream.writeInt32(flags); flags = phone_registered ? (flags | 1) : (flags &~ 1); + stream.writeInt32(flags); type.serializeToStream(stream); stream.writeString(phone_code_hash); if ((flags & 2) != 0) { @@ -4057,8 +4119,8 @@ public class TLRPC { public int admins_count; public int kicked_count; public int read_inbox_max_id; + public int read_outbox_max_id; public int unread_count; - public int unread_important_count; public Photo chat_photo; public PeerNotifySettings notify_settings; public ExportedChatInvite exported_invite; @@ -4071,9 +4133,12 @@ public class TLRPC { public static ChatFull TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) { ChatFull result = null; switch(constructor) { - case 0x97bee562: + case 0xc3d5512f: result = new TL_channelFull(); break; + case 0x97bee562: + result = new TL_channelFull_layer52(); + break; case 0x2e02a614: result = new TL_chatFull(); break; @@ -4095,6 +4160,97 @@ public class TLRPC { } public static class TL_channelFull extends ChatFull { + public static int constructor = 0xc3d5512f; + + + public void readParams(AbstractSerializedData stream, boolean exception) { + flags = stream.readInt32(exception); + can_view_participants = (flags & 8) != 0; + can_set_username = (flags & 64) != 0; + id = stream.readInt32(exception); + about = stream.readString(exception); + if ((flags & 1) != 0) { + participants_count = stream.readInt32(exception); + } + if ((flags & 2) != 0) { + admins_count = stream.readInt32(exception); + } + if ((flags & 4) != 0) { + kicked_count = stream.readInt32(exception); + } + read_inbox_max_id = stream.readInt32(exception); + read_outbox_max_id = stream.readInt32(exception); + unread_count = stream.readInt32(exception); + chat_photo = Photo.TLdeserialize(stream, stream.readInt32(exception), exception); + notify_settings = PeerNotifySettings.TLdeserialize(stream, stream.readInt32(exception), exception); + exported_invite = ExportedChatInvite.TLdeserialize(stream, stream.readInt32(exception), exception); + int magic = stream.readInt32(exception); + if (magic != 0x1cb5c415) { + if (exception) { + throw new RuntimeException(String.format("wrong Vector magic, got %x", magic)); + } + return; + } + int count = stream.readInt32(exception); + for (int a = 0; a < count; a++) { + BotInfo object = BotInfo.TLdeserialize(stream, stream.readInt32(exception), exception); + if (object == null) { + return; + } + bot_info.add(object); + } + if ((flags & 16) != 0) { + migrated_from_chat_id = stream.readInt32(exception); + } + if ((flags & 16) != 0) { + migrated_from_max_id = stream.readInt32(exception); + } + if ((flags & 32) != 0) { + pinned_msg_id = stream.readInt32(exception); + } + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + flags = can_view_participants ? (flags | 8) : (flags &~ 8); + flags = can_set_username ? (flags | 64) : (flags &~ 64); + stream.writeInt32(flags); + stream.writeInt32(id); + stream.writeString(about); + if ((flags & 1) != 0) { + stream.writeInt32(participants_count); + } + if ((flags & 2) != 0) { + stream.writeInt32(admins_count); + } + if ((flags & 4) != 0) { + stream.writeInt32(kicked_count); + } + stream.writeInt32(read_inbox_max_id); + stream.writeInt32(read_outbox_max_id); + stream.writeInt32(unread_count); + chat_photo.serializeToStream(stream); + notify_settings.serializeToStream(stream); + exported_invite.serializeToStream(stream); + stream.writeInt32(0x1cb5c415); + int count = bot_info.size(); + stream.writeInt32(count); + for (int a = 0; a < count; a++) { + bot_info.get(a).serializeToStream(stream); + } + if ((flags & 16) != 0) { + stream.writeInt32(migrated_from_chat_id); + } + if ((flags & 16) != 0) { + stream.writeInt32(migrated_from_max_id); + } + if ((flags & 32) != 0) { + stream.writeInt32(pinned_msg_id); + } + } + } + + public static class TL_channelFull_layer52 extends TL_channelFull { public static int constructor = 0x97bee562; @@ -4115,7 +4271,7 @@ public class TLRPC { } read_inbox_max_id = stream.readInt32(exception); unread_count = stream.readInt32(exception); - unread_important_count = stream.readInt32(exception); + stream.readInt32(exception); chat_photo = Photo.TLdeserialize(stream, stream.readInt32(exception), exception); notify_settings = PeerNotifySettings.TLdeserialize(stream, stream.readInt32(exception), exception); exported_invite = ExportedChatInvite.TLdeserialize(stream, stream.readInt32(exception), exception); @@ -4163,7 +4319,7 @@ public class TLRPC { } stream.writeInt32(read_inbox_max_id); stream.writeInt32(unread_count); - stream.writeInt32(unread_important_count); + stream.writeInt32(0); chat_photo.serializeToStream(stream); notify_settings.serializeToStream(stream); exported_invite.serializeToStream(stream); @@ -4248,7 +4404,7 @@ public class TLRPC { } read_inbox_max_id = stream.readInt32(exception); unread_count = stream.readInt32(exception); - unread_important_count = stream.readInt32(exception); + stream.readInt32(exception); chat_photo = Photo.TLdeserialize(stream, stream.readInt32(exception), exception); notify_settings = PeerNotifySettings.TLdeserialize(stream, stream.readInt32(exception), exception); exported_invite = ExportedChatInvite.TLdeserialize(stream, stream.readInt32(exception), exception); @@ -4292,7 +4448,7 @@ public class TLRPC { } stream.writeInt32(read_inbox_max_id); stream.writeInt32(unread_count); - stream.writeInt32(unread_important_count); + stream.writeInt32(0); chat_photo.serializeToStream(stream); notify_settings.serializeToStream(stream); exported_invite.serializeToStream(stream); @@ -4331,7 +4487,7 @@ public class TLRPC { } read_inbox_max_id = stream.readInt32(exception); unread_count = stream.readInt32(exception); - unread_important_count = stream.readInt32(exception); + stream.readInt32(exception); chat_photo = Photo.TLdeserialize(stream, stream.readInt32(exception), exception); notify_settings = PeerNotifySettings.TLdeserialize(stream, stream.readInt32(exception), exception); exported_invite = ExportedChatInvite.TLdeserialize(stream, stream.readInt32(exception), exception); @@ -4354,7 +4510,7 @@ public class TLRPC { } stream.writeInt32(read_inbox_max_id); stream.writeInt32(unread_count); - stream.writeInt32(unread_important_count); + stream.writeInt32(0); chat_photo.serializeToStream(stream); notify_settings.serializeToStream(stream); exported_invite.serializeToStream(stream); @@ -6315,6 +6471,9 @@ public class TLRPC { case 0x51bdb021: result = new TL_messageActionChatMigrateTo(); break; + case 0x9fbab604: + result = new TL_messageActionHistoryClear(); + break; case 0x7fcb13a8: result = new TL_messageActionChatEditPhoto(); break; @@ -6452,6 +6611,15 @@ public class TLRPC { } } + public static class TL_messageActionHistoryClear extends MessageAction { + public static int constructor = 0x9fbab604; + + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + } + } + public static class TL_messageActionChatEditPhoto extends MessageAction { public static int constructor = 0x7fcb13a8; @@ -8825,6 +8993,7 @@ public class TLRPC { public int inviter_id; public int date; public int version; + public DraftMessage draft; public int pts; public int pts_count; public long query_id; @@ -8857,7 +9026,6 @@ public class TLRPC { public TL_messages_stickerSet stickerset; public ContactLink my_link; public ContactLink foreign_link; - public TL_messageGroup group; public String first_name; public String last_name; public String username; @@ -8876,6 +9044,9 @@ public class TLRPC { case 0xea4b0e5c: result = new TL_updateChatParticipantAdd(); break; + case 0xee2bb969: + result = new TL_updateDraftMessage(); + break; case 0x43ae3dec: result = new TL_updateStickerSets(); break; @@ -8939,6 +9110,9 @@ public class TLRPC { case 0x2cbd95af: result = new TL_updateInlineBotCallbackQuery(); break; + case 0x25d6c9c7: + result = new TL_updateReadChannelOutbox(); + break; case 0x1710f156: result = new TL_updateEncryptedChatTyping(); break; @@ -8969,9 +9143,6 @@ public class TLRPC { case 0x9961fd5c: result = new TL_updateReadHistoryInbox(); break; - case 0xc36c1e3c: - result = new TL_updateChannelGroup(); - break; case 0xa7332b73: result = new TL_updateUserName(); break; @@ -9053,6 +9224,23 @@ public class TLRPC { } } + public static class TL_updateDraftMessage extends Update { + public static int constructor = 0xee2bb969; + + public Peer peer; + + public void readParams(AbstractSerializedData stream, boolean exception) { + peer = Peer.TLdeserialize(stream, stream.readInt32(exception), exception); + draft = DraftMessage.TLdeserialize(stream, stream.readInt32(exception), exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + peer.serializeToStream(stream); + draft.serializeToStream(stream); + } + } + public static class TL_updateStickerSets extends Update { public static int constructor = 0x43ae3dec; @@ -9449,6 +9637,22 @@ public class TLRPC { } } + public static class TL_updateReadChannelOutbox extends Update { + public static int constructor = 0x25d6c9c7; + + + public void readParams(AbstractSerializedData stream, boolean exception) { + channel_id = stream.readInt32(exception); + max_id = stream.readInt32(exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeInt32(channel_id); + stream.writeInt32(max_id); + } + } + public static class TL_updateEncryptedChatTyping extends Update { public static int constructor = 0x1710f156; @@ -9629,22 +9833,6 @@ public class TLRPC { } } - public static class TL_updateChannelGroup extends Update { - public static int constructor = 0xc36c1e3c; - - - public void readParams(AbstractSerializedData stream, boolean exception) { - channel_id = stream.readInt32(exception); - group = TL_messageGroup.TLdeserialize(stream, stream.readInt32(exception), exception); - } - - public void serializeToStream(AbstractSerializedData stream) { - stream.writeInt32(constructor); - stream.writeInt32(channel_id); - group.serializeToStream(stream); - } - } - public static class TL_updateUserName extends Update { public static int constructor = 0xa7332b73; @@ -11743,43 +11931,6 @@ public class TLRPC { } } - public static class TL_messageGroup extends TLObject { - public static int constructor = 0xe8346f53; - - public int min_id; - public int max_id; - public int count; - public int date; - - public static TL_messageGroup TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) { - if (TL_messageGroup.constructor != constructor) { - if (exception) { - throw new RuntimeException(String.format("can't parse magic %x in TL_messageGroup", constructor)); - } else { - return null; - } - } - TL_messageGroup result = new TL_messageGroup(); - result.readParams(stream, exception); - return result; - } - - public void readParams(AbstractSerializedData stream, boolean exception) { - min_id = stream.readInt32(exception); - max_id = stream.readInt32(exception); - count = stream.readInt32(exception); - date = stream.readInt32(exception); - } - - public void serializeToStream(AbstractSerializedData stream) { - stream.writeInt32(constructor); - stream.writeInt32(min_id); - stream.writeInt32(max_id); - stream.writeInt32(count); - stream.writeInt32(date); - } - } - public static class TL_messages_botResults extends TLObject { public static int constructor = 0x256709a6; @@ -11944,10 +12095,9 @@ public class TLRPC { public ArrayList chats = new ArrayList<>(); public ArrayList users = new ArrayList<>(); public int top_message; - public int top_important_message; public int read_inbox_max_id; + public int read_outbox_max_id; public int unread_count; - public int unread_important_count; public ArrayList messages = new ArrayList<>(); public static updates_ChannelDifference TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) { @@ -11959,7 +12109,7 @@ public class TLRPC { case 0x2064674e: result = new TL_updates_channelDifference(); break; - case 0x5e167646: + case 0x410dee07: result = new TL_updates_channelDifferenceTooLong(); break; } @@ -12106,7 +12256,7 @@ public class TLRPC { } public static class TL_updates_channelDifferenceTooLong extends updates_ChannelDifference { - public static int constructor = 0x5e167646; + public static int constructor = 0x410dee07; public void readParams(AbstractSerializedData stream, boolean exception) { @@ -12117,10 +12267,9 @@ public class TLRPC { timeout = stream.readInt32(exception); } top_message = stream.readInt32(exception); - top_important_message = stream.readInt32(exception); read_inbox_max_id = stream.readInt32(exception); + read_outbox_max_id = stream.readInt32(exception); unread_count = stream.readInt32(exception); - unread_important_count = stream.readInt32(exception); int magic = stream.readInt32(exception); if (magic != 0x1cb5c415) { if (exception) { @@ -12177,10 +12326,9 @@ public class TLRPC { stream.writeInt32(timeout); } stream.writeInt32(top_message); - stream.writeInt32(top_important_message); stream.writeInt32(read_inbox_max_id); + stream.writeInt32(read_outbox_max_id); stream.writeInt32(unread_count); - stream.writeInt32(unread_important_count); stream.writeInt32(0x1cb5c415); int count = messages.size(); stream.writeInt32(count); @@ -12204,7 +12352,6 @@ public class TLRPC { public static class ChannelMessagesFilter extends TLObject { public int flags; - public boolean important_only; public boolean exclude_new_messages; public ArrayList ranges = new ArrayList<>(); @@ -12217,9 +12364,6 @@ public class TLRPC { case 0xcd77d957: result = new TL_channelMessagesFilter(); break; - case 0xfa01232e: - result = new TL_channelMessagesFilterCollapsed(); - break; } if (result == null && exception) { throw new RuntimeException(String.format("can't parse magic %x in ChannelMessagesFilter", constructor)); @@ -12246,7 +12390,6 @@ public class TLRPC { public void readParams(AbstractSerializedData stream, boolean exception) { flags = stream.readInt32(exception); - important_only = (flags & 1) != 0; exclude_new_messages = (flags & 2) != 0; int magic = stream.readInt32(exception); if (magic != 0x1cb5c415) { @@ -12267,7 +12410,6 @@ public class TLRPC { public void serializeToStream(AbstractSerializedData stream) { stream.writeInt32(constructor); - flags = important_only ? (flags | 1) : (flags &~ 1); flags = exclude_new_messages ? (flags | 2) : (flags &~ 2); stream.writeInt32(flags); stream.writeInt32(0x1cb5c415); @@ -12279,15 +12421,6 @@ public class TLRPC { } } - public static class TL_channelMessagesFilterCollapsed extends ChannelMessagesFilter { - public static int constructor = 0xfa01232e; - - - public void serializeToStream(AbstractSerializedData stream) { - stream.writeInt32(constructor); - } - } - public static class TL_contacts_resolvedPeer extends TLObject { public static int constructor = 0x7f077ad9; @@ -13428,6 +13561,9 @@ public class TLRPC { case 0x7328bdb: result = new TL_chatForbidden(); break; + case 0x8537784f: + result = new TL_channelForbidden(); + break; case 0x678e9587: result = new TL_channel_old(); break; @@ -13447,7 +13583,7 @@ public class TLRPC { result = new TL_geoChat(); break; case 0x2d85832c: - result = new TL_channelForbidden(); + result = new TL_channelForbidden_layer52(); break; case 0xa14dca52: result = new TL_channel(); @@ -13503,6 +13639,30 @@ public class TLRPC { } } + public static class TL_channelForbidden extends Chat { + public static int constructor = 0x8537784f; + + + public void readParams(AbstractSerializedData stream, boolean exception) { + flags = stream.readInt32(exception); + broadcast = (flags & 32) != 0; + megagroup = (flags & 256) != 0; + id = stream.readInt32(exception); + access_hash = stream.readInt64(exception); + title = stream.readString(exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + flags = broadcast ? (flags | 32) : (flags &~ 32); + flags = megagroup ? (flags | 256) : (flags &~ 256); + stream.writeInt32(flags); + stream.writeInt32(id); + stream.writeInt64(access_hash); + stream.writeString(title); + } + } + public static class TL_chatForbidden extends Chat { public static int constructor = 0x7328bdb; @@ -13728,7 +13888,7 @@ public class TLRPC { } } - public static class TL_channelForbidden extends Chat { + public static class TL_channelForbidden_layer52 extends Chat { public static int constructor = 0x2d85832c; @@ -14113,6 +14273,9 @@ public class TLRPC { case 0x3751b49e: result = new TL_inputMessagesFilterMusic(); break; + case 0x3a20ecb8: + result = new TL_inputMessagesFilterChatPhotos(); + break; case 0x9fc00e65: result = new TL_inputMessagesFilterVideo(); break; @@ -14166,6 +14329,15 @@ public class TLRPC { } } + public static class TL_inputMessagesFilterChatPhotos extends MessagesFilter { + public static int constructor = 0x3a20ecb8; + + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + } + } + public static class TL_inputMessagesFilterVideo extends MessagesFilter { public static int constructor = 0x9fc00e65; @@ -14966,7 +15138,6 @@ public class TLRPC { public int date; public int seq; public int flags; - public boolean unread; public boolean out; public boolean mentioned; public boolean media_unread; @@ -15082,7 +15253,6 @@ public class TLRPC { public void readParams(AbstractSerializedData stream, boolean exception) { flags = stream.readInt32(exception); - unread = (flags & 1) != 0; out = (flags & 2) != 0; mentioned = (flags & 16) != 0; media_unread = (flags & 32) != 0; @@ -15128,7 +15298,6 @@ public class TLRPC { public void readParams(AbstractSerializedData stream, boolean exception) { flags = stream.readInt32(exception); - unread = (flags & 1) != 0; out = (flags & 2) != 0; id = stream.readInt32(exception); pts = stream.readInt32(exception); @@ -15173,7 +15342,6 @@ public class TLRPC { public void readParams(AbstractSerializedData stream, boolean exception) { flags = stream.readInt32(exception); - unread = (flags & 1) != 0; out = (flags & 2) != 0; mentioned = (flags & 16) != 0; media_unread = (flags & 32) != 0; @@ -15960,7 +16128,7 @@ public class TLRPC { public static class TL_messages_peerDialogs extends TLObject { public static int constructor = 0x3371c354; - public ArrayList

dialogs = new ArrayList<>(); + public ArrayList dialogs = new ArrayList<>(); public ArrayList messages = new ArrayList<>(); public ArrayList chats = new ArrayList<>(); public ArrayList users = new ArrayList<>(); @@ -15989,7 +16157,7 @@ public class TLRPC { } int count = stream.readInt32(exception); for (int a = 0; a < count; a++) { - Dialog object = Dialog.TLdeserialize(stream, stream.readInt32(exception), exception); + TL_dialog object = TL_dialog.TLdeserialize(stream, stream.readInt32(exception), exception); if (object == null) { return; } @@ -16157,7 +16325,7 @@ public class TLRPC { } public static class messages_Dialogs extends TLObject { - public ArrayList dialogs = new ArrayList<>(); + public ArrayList dialogs = new ArrayList<>(); public ArrayList messages = new ArrayList<>(); public ArrayList chats = new ArrayList<>(); public ArrayList users = new ArrayList<>(); @@ -16197,7 +16365,7 @@ public class TLRPC { } int count = stream.readInt32(exception); for (int a = 0; a < count; a++) { - Dialog object = Dialog.TLdeserialize(stream, stream.readInt32(exception), exception); + TL_dialog object = TL_dialog.TLdeserialize(stream, stream.readInt32(exception), exception); if (object == null) { return; } @@ -16294,7 +16462,7 @@ public class TLRPC { } int count = stream.readInt32(exception); for (int a = 0; a < count; a++) { - Dialog object = Dialog.TLdeserialize(stream, stream.readInt32(exception), exception); + TL_dialog object = TL_dialog.TLdeserialize(stream, stream.readInt32(exception), exception); if (object == null) { return; } @@ -16440,7 +16608,7 @@ public class TLRPC { } public static class TL_auth_sendCode extends TLObject { - public static int constructor = 0xccfd70cf; + public static int constructor = 0x86aef0ec; public int flags; public boolean allow_flashcall; @@ -16448,7 +16616,6 @@ public class TLRPC { public boolean current_number; public int api_id; public String api_hash; - public String lang_code; public TLObject deserializeResponse(AbstractSerializedData stream, int constructor, boolean exception) { return TL_auth_sentCode.TLdeserialize(stream, constructor, exception); @@ -16464,7 +16631,6 @@ public class TLRPC { } stream.writeInt32(api_id); stream.writeString(api_hash); - stream.writeString(lang_code); } } @@ -16611,16 +16777,11 @@ public class TLRPC { } } - public static class TL_account_registerDevice extends TLObject { - public static int constructor = 0x446c712c; + public static class TL_account_registerDevice extends TLObject { + public static int constructor = 0x637ea878; public int token_type; public String token; - public String device_model; - public String system_version; - public String app_version; - public boolean app_sandbox; - public String lang_code; public TLObject deserializeResponse(AbstractSerializedData stream, int constructor, boolean exception) { return Bool.TLdeserialize(stream, constructor, exception); @@ -16629,12 +16790,7 @@ public class TLRPC { public void serializeToStream(AbstractSerializedData stream) { stream.writeInt32(constructor); stream.writeInt32(token_type); - stream.writeString(token); - stream.writeString(device_model); - stream.writeString(system_version); - stream.writeString(app_version); - stream.writeBool(app_sandbox); - stream.writeString(lang_code); + stream.writeString(token); } } @@ -17076,7 +17232,6 @@ public class TLRPC { public static int constructor = 0xd4569248; public int flags; - public boolean important_only; public InputPeer peer; public String q; public MessagesFilter filter; @@ -17092,7 +17247,6 @@ public class TLRPC { public void serializeToStream(AbstractSerializedData stream) { stream.writeInt32(constructor); - flags = important_only ? (flags | 1) : (flags &~ 1); stream.writeInt32(flags); peer.serializeToStream(stream); stream.writeString(q); @@ -17123,8 +17277,10 @@ public class TLRPC { } public static class TL_messages_deleteHistory extends TLObject { - public static int constructor = 0xb7c13bd9; + public static int constructor = 0x1c015b09; + public int flags; + public boolean just_clear; public InputPeer peer; public int max_id; @@ -17134,6 +17290,8 @@ public class TLRPC { public void serializeToStream(AbstractSerializedData stream) { stream.writeInt32(constructor); + flags = just_clear ? (flags | 1) : (flags &~ 1); + stream.writeInt32(flags); peer.serializeToStream(stream); stream.writeInt32(max_id); } @@ -17279,9 +17437,9 @@ public class TLRPC { public int flags; public boolean no_webpage; - public boolean broadcast; public boolean silent; public boolean background; + public boolean clear_draft; public InputPeer peer; public int reply_to_msg_id; public String message; @@ -17296,9 +17454,9 @@ public class TLRPC { public void serializeToStream(AbstractSerializedData stream) { stream.writeInt32(constructor); flags = no_webpage ? (flags | 2) : (flags &~ 2); - flags = broadcast ? (flags | 16) : (flags &~ 16); flags = silent ? (flags | 32) : (flags &~ 32); flags = background ? (flags | 64) : (flags &~ 64); + flags = clear_draft ? (flags | 128) : (flags &~ 128); stream.writeInt32(flags); peer.serializeToStream(stream); if ((flags & 1) != 0) { @@ -17324,9 +17482,9 @@ public class TLRPC { public static int constructor = 0xc8f16791; public int flags; - public boolean broadcast; public boolean silent; public boolean background; + public boolean clear_draft; public InputPeer peer; public int reply_to_msg_id; public InputMedia media; @@ -17339,9 +17497,9 @@ public class TLRPC { public void serializeToStream(AbstractSerializedData stream) { stream.writeInt32(constructor); - flags = broadcast ? (flags | 16) : (flags &~ 16); flags = silent ? (flags | 32) : (flags &~ 32); flags = background ? (flags | 64) : (flags &~ 64); + flags = clear_draft ? (flags | 128) : (flags &~ 128); stream.writeInt32(flags); peer.serializeToStream(stream); if ((flags & 1) != 0) { @@ -17359,7 +17517,6 @@ public class TLRPC { public static int constructor = 0x708e0195; public int flags; - public boolean broadcast; public boolean silent; public boolean background; public InputPeer from_peer; @@ -17373,7 +17530,6 @@ public class TLRPC { public void serializeToStream(AbstractSerializedData stream) { stream.writeInt32(constructor); - flags = broadcast ? (flags | 16) : (flags &~ 16); flags = silent ? (flags | 32) : (flags &~ 32); flags = background ? (flags | 64) : (flags &~ 64); stream.writeInt32(flags); @@ -17728,12 +17884,8 @@ public class TLRPC { } public static class TL_help_getAppUpdate extends TLObject { - public static int constructor = 0xc812ac7e; + public static int constructor = 0xae2de196; - public String device_model; - public String system_version; - public String app_version; - public String lang_code; public TLObject deserializeResponse(AbstractSerializedData stream, int constructor, boolean exception) { return help_AppUpdate.TLdeserialize(stream, constructor, exception); @@ -17741,10 +17893,6 @@ public class TLRPC { public void serializeToStream(AbstractSerializedData stream) { stream.writeInt32(constructor); - stream.writeString(device_model); - stream.writeString(system_version); - stream.writeString(app_version); - stream.writeString(lang_code); } } @@ -17757,21 +17905,20 @@ public class TLRPC { return Bool.TLdeserialize(stream, constructor, exception); } - public void serializeToStream(AbstractSerializedData stream) { + public void serializeToStream(AbstractSerializedData stream) { stream.writeInt32(constructor); - stream.writeInt32(0x1cb5c415); + stream.writeInt32(0x1cb5c415); int count = events.size(); stream.writeInt32(count); for (int a = 0; a < count; a++) { - events.get(a).serializeToStream(stream); + events.get(a).serializeToStream(stream); } } } - public static class TL_help_getInviteText extends TLObject { - public static int constructor = 0xa4a95186; + public static class TL_help_getInviteText extends TLObject { + public static int constructor = 0x4d392343; - public String lang_code; public TLObject deserializeResponse(AbstractSerializedData stream, int constructor, boolean exception) { return TL_help_inviteText.TLdeserialize(stream, constructor, exception); @@ -17779,7 +17926,6 @@ public class TLRPC { public void serializeToStream(AbstractSerializedData stream) { stream.writeInt32(constructor); - stream.writeString(lang_code); } } @@ -18400,12 +18546,12 @@ public class TLRPC { public void serializeToStream(AbstractSerializedData stream) { stream.writeInt32(constructor); - stream.writeInt32(flags); flags = correspondents ? (flags | 1) : (flags &~ 1); flags = bots_pm ? (flags | 2) : (flags &~ 2); flags = bots_inline ? (flags | 4) : (flags &~ 4); flags = groups ? (flags | 1024) : (flags &~ 1024); flags = channels ? (flags | 32768) : (flags &~ 32768); + stream.writeInt32(flags); stream.writeInt32(offset); stream.writeInt32(limit); stream.writeInt32(hash); @@ -18905,9 +19051,9 @@ public class TLRPC { public static int constructor = 0xb16e06fe; public int flags; - public boolean broadcast; public boolean silent; public boolean background; + public boolean clear_draft; public InputPeer peer; public int reply_to_msg_id; public long random_id; @@ -18920,9 +19066,9 @@ public class TLRPC { public void serializeToStream(AbstractSerializedData stream) { stream.writeInt32(constructor); - flags = broadcast ? (flags | 16) : (flags &~ 16); flags = silent ? (flags | 32) : (flags &~ 32); flags = background ? (flags | 64) : (flags &~ 64); + flags = clear_draft ? (flags | 128) : (flags &~ 128); stream.writeInt32(flags); peer.serializeToStream(stream); if ((flags & 1) != 0) { @@ -19087,13 +19233,56 @@ public class TLRPC { } } - public static class TL_help_getAppChangelog extends TLObject { - public static int constructor = 0x5bab7fb2; + public static class TL_messages_saveDraft extends TLObject { + public static int constructor = 0xbc39e14b; + + public int flags; + public boolean no_webpage; + public int reply_to_msg_id; + public InputPeer peer; + public String message; + public ArrayList entities = new ArrayList<>(); + + public TLObject deserializeResponse(AbstractSerializedData stream, int constructor, boolean exception) { + return Bool.TLdeserialize(stream, constructor, exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + flags = no_webpage ? (flags | 2) : (flags &~ 2); + stream.writeInt32(flags); + if ((flags & 1) != 0) { + stream.writeInt32(reply_to_msg_id); + } + peer.serializeToStream(stream); + stream.writeString(message); + if ((flags & 8) != 0) { + stream.writeInt32(0x1cb5c415); + int count = entities.size(); + stream.writeInt32(count); + for (int a = 0; a < count; a++) { + entities.get(a).serializeToStream(stream); + } + } + } + } + + public static class TL_messages_getAllDrafts extends TLObject { + public static int constructor = 0x6a3f8d65; + + + public TLObject deserializeResponse(AbstractSerializedData stream, int constructor, boolean exception) { + return Updates.TLdeserialize(stream, constructor, exception); + } + + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + } + } + + public static class TL_help_getAppChangelog extends TLObject { + public static int constructor = 0xb921197a; - public String device_model; - public String system_version; - public String app_version; - public String lang_code; public TLObject deserializeResponse(AbstractSerializedData stream, int constructor, boolean exception) { return help_AppChangelog.TLdeserialize(stream, constructor, exception); @@ -19101,17 +19290,12 @@ public class TLRPC { public void serializeToStream(AbstractSerializedData stream) { stream.writeInt32(constructor); - stream.writeString(device_model); - stream.writeString(system_version); - stream.writeString(app_version); - stream.writeString(lang_code); } } public static class TL_help_getTermsOfService extends TLObject { - public static int constructor = 0x37d78f83; + public static int constructor = 0x350170f3; - public String lang_code; public TLObject deserializeResponse(AbstractSerializedData stream, int constructor, boolean exception) { return TL_help_termsOfService.TLdeserialize(stream, constructor, exception); @@ -19119,7 +19303,6 @@ public class TLRPC { public void serializeToStream(AbstractSerializedData stream) { stream.writeInt32(constructor); - stream.writeString(lang_code); } } @@ -19162,50 +19345,6 @@ public class TLRPC { } } - public static class TL_channels_getDialogs extends TLObject { - public static int constructor = 0xa9d3d249; - - public int offset; - public int limit; - - public TLObject deserializeResponse(AbstractSerializedData stream, int constructor, boolean exception) { - return messages_Dialogs.TLdeserialize(stream, constructor, exception); - } - - public void serializeToStream(AbstractSerializedData stream) { - stream.writeInt32(constructor); - stream.writeInt32(offset); - stream.writeInt32(limit); - } - } - - public static class TL_channels_getImportantHistory extends TLObject { - public static int constructor = 0x8f494bb2; - - public InputChannel channel; - public int offset_id; - public int offset_date; - public int add_offset; - public int limit; - public int max_id; - public int min_id; - - public TLObject deserializeResponse(AbstractSerializedData stream, int constructor, boolean exception) { - return messages_Messages.TLdeserialize(stream, constructor, exception); - } - - public void serializeToStream(AbstractSerializedData stream) { - stream.writeInt32(constructor); - channel.serializeToStream(stream); - stream.writeInt32(offset_id); - stream.writeInt32(offset_date); - stream.writeInt32(add_offset); - stream.writeInt32(limit); - stream.writeInt32(max_id); - stream.writeInt32(min_id); - } - } - public static class TL_channels_readHistory extends TLObject { public static int constructor = 0xcc104937; @@ -19474,23 +19613,6 @@ public class TLRPC { } } - public static class TL_channels_toggleComments extends TLObject { - public static int constructor = 0xaaa29e88; - - public InputChannel channel; - public boolean enabled; - - public TLObject deserializeResponse(AbstractSerializedData stream, int constructor, boolean exception) { - return Updates.TLdeserialize(stream, constructor, exception); - } - - public void serializeToStream(AbstractSerializedData stream) { - stream.writeInt32(constructor); - channel.serializeToStream(stream); - stream.writeBool(enabled); - } - } - public static class TL_channels_checkUsername extends TLObject { public static int constructor = 0x10e6bd2c; @@ -21190,89 +21312,67 @@ public class TLRPC { //Message end //TL_dialog start - public static class Dialog extends TLObject { - public Peer peer; - public int top_message; - public int top_not_important_message; - public int read_inbox_max_id; - public int unread_count; - public int unread_not_important_count; - public PeerNotifySettings notify_settings; - public int pts; - public int last_message_date; //custom - public int last_message_date_i; //custom - public long id; //custom - public int last_read; //custom + public static class TL_dialog extends TLObject { + public static int constructor = 0x66ffba14; - public static Dialog TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) { - Dialog result = null; - switch(constructor) { - case 0x5b8496b2: - result = new TL_dialogChannel(); - break; - case 0xc1dd804a: - result = new TL_dialog(); - break; - } - if (result == null && exception) { - throw new RuntimeException(String.format("can't parse magic %x in Dialog", constructor)); - } - if (result != null) { - result.readParams(stream, exception); - } - return result; - } - } + public int flags; + public Peer peer; + public int top_message; + public int read_inbox_max_id; + public int read_outbox_max_id; + public int unread_count; + public PeerNotifySettings notify_settings; + public int pts; + public DraftMessage draft; + public int last_message_date; //custom + public long id; //custom - public static class TL_dialogChannel extends Dialog { - public static int constructor = 0x5b8496b2; + public static TL_dialog TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) { + if (TL_dialog.constructor != constructor) { + if (exception) { + throw new RuntimeException(String.format("can't parse magic %x in TL_dialog", constructor)); + } else { + return null; + } + } + TL_dialog result = new TL_dialog(); + result.readParams(stream, exception); + return result; + } + public void readParams(AbstractSerializedData stream, boolean exception) { + flags = stream.readInt32(exception); + peer = Peer.TLdeserialize(stream, stream.readInt32(exception), exception); + top_message = stream.readInt32(exception); + read_inbox_max_id = stream.readInt32(exception); + read_outbox_max_id = stream.readInt32(exception); + unread_count = stream.readInt32(exception); + notify_settings = PeerNotifySettings.TLdeserialize(stream, stream.readInt32(exception), exception); + if ((flags & 1) != 0) { + pts = stream.readInt32(exception); + } + if ((flags & 2) != 0) { + draft = DraftMessage.TLdeserialize(stream, stream.readInt32(exception), exception); + } + } - public void readParams(AbstractSerializedData stream, boolean exception) { - peer = Peer.TLdeserialize(stream, stream.readInt32(exception), exception); - top_not_important_message = stream.readInt32(exception); - top_message = stream.readInt32(exception); - read_inbox_max_id = stream.readInt32(exception); - unread_not_important_count = stream.readInt32(exception); - unread_count = stream.readInt32(exception); - notify_settings = PeerNotifySettings.TLdeserialize(stream, stream.readInt32(exception), exception); - pts = stream.readInt32(exception); - } - - public void serializeToStream(AbstractSerializedData stream) { - stream.writeInt32(constructor); - peer.serializeToStream(stream); - stream.writeInt32(top_not_important_message); - stream.writeInt32(top_message); - stream.writeInt32(read_inbox_max_id); - stream.writeInt32(unread_not_important_count); - stream.writeInt32(unread_count); - notify_settings.serializeToStream(stream); - stream.writeInt32(pts); - } - } - - public static class TL_dialog extends Dialog { - public static int constructor = 0xc1dd804a; - - - public void readParams(AbstractSerializedData stream, boolean exception) { - peer = Peer.TLdeserialize(stream, stream.readInt32(exception), exception); - top_message = stream.readInt32(exception); - read_inbox_max_id = stream.readInt32(exception); - unread_count = stream.readInt32(exception); - notify_settings = PeerNotifySettings.TLdeserialize(stream, stream.readInt32(exception), exception); - } - - public void serializeToStream(AbstractSerializedData stream) { - stream.writeInt32(constructor); - peer.serializeToStream(stream); - stream.writeInt32(top_message); - stream.writeInt32(read_inbox_max_id); - stream.writeInt32(unread_count); - notify_settings.serializeToStream(stream); - } - } + public void serializeToStream(AbstractSerializedData stream) { + stream.writeInt32(constructor); + stream.writeInt32(flags); + peer.serializeToStream(stream); + stream.writeInt32(top_message); + stream.writeInt32(read_inbox_max_id); + stream.writeInt32(read_outbox_max_id); + stream.writeInt32(unread_count); + notify_settings.serializeToStream(stream); + if ((flags & 1) != 0) { + stream.writeInt32(pts); + } + if ((flags & 2) != 0) { + draft.serializeToStream(stream); + } + } + } //TL_dialog end //ChatParticipant start diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ActionBar.java b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ActionBar.java index 6f52388af..8d420f95a 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ActionBar.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ActionBar.java @@ -8,6 +8,9 @@ package org.telegram.ui.ActionBar; +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; import android.content.Context; import android.content.res.Configuration; import android.graphics.drawable.Drawable; @@ -19,9 +22,7 @@ import android.widget.FrameLayout; import android.widget.ImageView; import org.telegram.messenger.AndroidUtilities; -import org.telegram.messenger.AnimationCompat.AnimatorListenerAdapterProxy; -import org.telegram.messenger.AnimationCompat.AnimatorSetProxy; -import org.telegram.messenger.AnimationCompat.ObjectAnimatorProxy; +import org.telegram.messenger.AnimatorListenerAdapterProxy; import org.telegram.messenger.ApplicationLoader; import org.telegram.ui.Components.LayoutHelper; @@ -50,7 +51,7 @@ public class ActionBar extends FrameLayout { private boolean addToContainer = true; private boolean interceptTouches = true; private int extraHeight; - private AnimatorSetProxy actionModeAnimation; + private AnimatorSet actionModeAnimation; private boolean allowOverlayTitle; private CharSequence lastTitle; @@ -222,66 +223,50 @@ public class ActionBar extends FrameLayout { return; } actionModeVisible = true; - if (Build.VERSION.SDK_INT >= 14) { - ArrayList animators = new ArrayList<>(); - animators.add(ObjectAnimatorProxy.ofFloat(actionMode, "alpha", 0.0f, 1.0f)); - if (occupyStatusBar && actionModeTop != null) { - animators.add(ObjectAnimatorProxy.ofFloat(actionModeTop, "alpha", 0.0f, 1.0f)); - } - if (actionModeAnimation != null) { - actionModeAnimation.cancel(); - } - actionModeAnimation = new AnimatorSetProxy(); - actionModeAnimation.playTogether(animators); - actionModeAnimation.setDuration(200); - actionModeAnimation.addListener(new AnimatorListenerAdapterProxy() { - @Override - public void onAnimationStart(Object animation) { - actionMode.setVisibility(VISIBLE); - if (occupyStatusBar && actionModeTop != null) { - actionModeTop.setVisibility(VISIBLE); - } - } - - @Override - public void onAnimationEnd(Object animation) { - if (actionModeAnimation != null && actionModeAnimation.equals(animation)) { - actionModeAnimation = null; - if (titleTextView != null) { - titleTextView.setVisibility(INVISIBLE); - } - if (subtitleTextView != null) { - subtitleTextView.setVisibility(INVISIBLE); - } - if (menu != null) { - menu.setVisibility(INVISIBLE); - } - } - } - - @Override - public void onAnimationCancel(Object animation) { - if (actionModeAnimation != null && actionModeAnimation.equals(animation)) { - actionModeAnimation = null; - } - } - }); - actionModeAnimation.start(); - } else { - actionMode.setVisibility(VISIBLE); - if (occupyStatusBar && actionModeTop != null) { - actionModeTop.setVisibility(VISIBLE); - } - if (titleTextView != null) { - titleTextView.setVisibility(INVISIBLE); - } - if (subtitleTextView != null) { - subtitleTextView.setVisibility(INVISIBLE); - } - if (menu != null) { - menu.setVisibility(INVISIBLE); - } + ArrayList animators = new ArrayList<>(); + animators.add(ObjectAnimator.ofFloat(actionMode, "alpha", 0.0f, 1.0f)); + if (occupyStatusBar && actionModeTop != null) { + animators.add(ObjectAnimator.ofFloat(actionModeTop, "alpha", 0.0f, 1.0f)); } + if (actionModeAnimation != null) { + actionModeAnimation.cancel(); + } + actionModeAnimation = new AnimatorSet(); + actionModeAnimation.playTogether(animators); + actionModeAnimation.setDuration(200); + actionModeAnimation.addListener(new AnimatorListenerAdapterProxy() { + @Override + public void onAnimationStart(Animator animation) { + actionMode.setVisibility(VISIBLE); + if (occupyStatusBar && actionModeTop != null) { + actionModeTop.setVisibility(VISIBLE); + } + } + + @Override + public void onAnimationEnd(Animator animation) { + if (actionModeAnimation != null && actionModeAnimation.equals(animation)) { + actionModeAnimation = null; + if (titleTextView != null) { + titleTextView.setVisibility(INVISIBLE); + } + if (subtitleTextView != null) { + subtitleTextView.setVisibility(INVISIBLE); + } + if (menu != null) { + menu.setVisibility(INVISIBLE); + } + } + } + + @Override + public void onAnimationCancel(Animator animation) { + if (actionModeAnimation != null && actionModeAnimation.equals(animation)) { + actionModeAnimation = null; + } + } + }); + actionModeAnimation.start(); if (backButtonImageView != null) { Drawable drawable = backButtonImageView.getDrawable(); if (drawable instanceof BackDrawable) { @@ -296,44 +281,37 @@ public class ActionBar extends FrameLayout { return; } actionModeVisible = false; - if (Build.VERSION.SDK_INT >= 14) { - ArrayList animators = new ArrayList<>(); - animators.add(ObjectAnimatorProxy.ofFloat(actionMode, "alpha", 0.0f)); - if (occupyStatusBar && actionModeTop != null) { - animators.add(ObjectAnimatorProxy.ofFloat(actionModeTop, "alpha", 0.0f)); - } - if (actionModeAnimation != null) { - actionModeAnimation.cancel(); - } - actionModeAnimation = new AnimatorSetProxy(); - actionModeAnimation.playTogether(animators); - actionModeAnimation.setDuration(200); - actionModeAnimation.addListener(new AnimatorListenerAdapterProxy() { - @Override - public void onAnimationEnd(Object animation) { - if (actionModeAnimation != null && actionModeAnimation.equals(animation)) { - actionModeAnimation = null; - actionMode.setVisibility(INVISIBLE); - if (occupyStatusBar && actionModeTop != null) { - actionModeTop.setVisibility(INVISIBLE); - } - } - } - - @Override - public void onAnimationCancel(Object animation) { - if (actionModeAnimation != null && actionModeAnimation.equals(animation)) { - actionModeAnimation = null; - } - } - }); - actionModeAnimation.start(); - } else { - actionMode.setVisibility(INVISIBLE); - if (occupyStatusBar && actionModeTop != null) { - actionModeTop.setVisibility(INVISIBLE); - } + ArrayList animators = new ArrayList<>(); + animators.add(ObjectAnimator.ofFloat(actionMode, "alpha", 0.0f)); + if (occupyStatusBar && actionModeTop != null) { + animators.add(ObjectAnimator.ofFloat(actionModeTop, "alpha", 0.0f)); } + if (actionModeAnimation != null) { + actionModeAnimation.cancel(); + } + actionModeAnimation = new AnimatorSet(); + actionModeAnimation.playTogether(animators); + actionModeAnimation.setDuration(200); + actionModeAnimation.addListener(new AnimatorListenerAdapterProxy() { + @Override + public void onAnimationEnd(Animator animation) { + if (actionModeAnimation != null && actionModeAnimation.equals(animation)) { + actionModeAnimation = null; + actionMode.setVisibility(INVISIBLE); + if (occupyStatusBar && actionModeTop != null) { + actionModeTop.setVisibility(INVISIBLE); + } + } + } + + @Override + public void onAnimationCancel(Animator animation) { + if (actionModeAnimation != null && actionModeAnimation.equals(animation)) { + actionModeAnimation = null; + } + } + }); + actionModeAnimation.start(); if (titleTextView != null) { titleTextView.setVisibility(VISIBLE); } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ActionBarLayout.java b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ActionBarLayout.java index f6cd41e01..ce1242084 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ActionBarLayout.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ActionBarLayout.java @@ -8,6 +8,9 @@ package org.telegram.ui.ActionBar; +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; import android.app.Activity; import android.content.Context; import android.content.Intent; @@ -30,10 +33,7 @@ import android.widget.LinearLayout; import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.R; -import org.telegram.messenger.AnimationCompat.AnimatorListenerAdapterProxy; -import org.telegram.messenger.AnimationCompat.AnimatorSetProxy; -import org.telegram.messenger.AnimationCompat.ObjectAnimatorProxy; -import org.telegram.messenger.AnimationCompat.ViewProxy; +import org.telegram.messenger.AnimatorListenerAdapterProxy; import org.telegram.ui.Components.LayoutHelper; import java.util.ArrayList; @@ -119,7 +119,7 @@ public class ActionBarLayout extends FrameLayout { private DrawerLayoutContainer drawerLayoutContainer; private ActionBar currentActionBar; - private AnimatorSetProxy currentAnimation; + private AnimatorSet currentAnimation; private DecelerateInterpolator decelerateInterpolator = new DecelerateInterpolator(1.5f); private AccelerateDecelerateInterpolator accelerateDecelerateInterpolator = new AccelerateDecelerateInterpolator(); @@ -335,9 +335,8 @@ public class ActionBarLayout extends FrameLayout { containerViewBack.setVisibility(View.GONE); startedTracking = false; animationInProgress = false; - - ViewProxy.setTranslationX(containerView, 0); - ViewProxy.setTranslationX(containerViewBack, 0); + containerView.setTranslationX(0); + containerViewBack.setTranslationX(0); setInnerTranslationX(0); } @@ -417,7 +416,7 @@ public class ActionBarLayout extends FrameLayout { currentFragment.onBeginSlide(); beginTrackingSent = true; } - ViewProxy.setTranslationX(containerView, dx); + containerView.setTranslationX(dx); setInnerTranslationX(dx); } } else if (ev != null && ev.getPointerId(0) == startedTrackingPointerId && (ev.getAction() == MotionEvent.ACTION_CANCEL || ev.getAction() == MotionEvent.ACTION_UP || ev.getAction() == MotionEvent.ACTION_POINTER_UP)) { @@ -439,8 +438,8 @@ public class ActionBarLayout extends FrameLayout { } } if (startedTracking) { - float x = ViewProxy.getX(containerView); - AnimatorSetProxy animatorSet = new AnimatorSetProxy(); + float x = containerView.getX(); + AnimatorSet animatorSet = new AnimatorSet(); float velX = velocityTracker.getXVelocity(); float velY = velocityTracker.getYVelocity(); final boolean backAnimation = x < containerView.getMeasuredWidth() / 3.0f && (velX < 3500 || velX < velY); @@ -448,21 +447,21 @@ public class ActionBarLayout extends FrameLayout { if (!backAnimation) { distToMove = containerView.getMeasuredWidth() - x; animatorSet.playTogether( - ObjectAnimatorProxy.ofFloat(containerView, "translationX", containerView.getMeasuredWidth()), - ObjectAnimatorProxy.ofFloat(this, "innerTranslationX", (float) containerView.getMeasuredWidth()) + ObjectAnimator.ofFloat(containerView, "translationX", containerView.getMeasuredWidth()), + ObjectAnimator.ofFloat(this, "innerTranslationX", (float) containerView.getMeasuredWidth()) ); } else { distToMove = x; animatorSet.playTogether( - ObjectAnimatorProxy.ofFloat(containerView, "translationX", 0), - ObjectAnimatorProxy.ofFloat(this, "innerTranslationX", 0.0f) + ObjectAnimator.ofFloat(containerView, "translationX", 0), + ObjectAnimator.ofFloat(this, "innerTranslationX", 0.0f) ); } animatorSet.setDuration(Math.max((int) (200.0f / containerView.getMeasuredWidth() * distToMove), 50)); animatorSet.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animator) { + public void onAnimationEnd(Animator animator) { onSlideAnimationEnd(backAnimation); } }); @@ -529,13 +528,13 @@ public class ActionBarLayout extends FrameLayout { AndroidUtilities.cancelRunOnUIThread(animationRunnable); animationRunnable = null; } - ViewProxy.setAlpha(this, 1.0f); - ViewProxy.setAlpha(containerView, 1.0f); - ViewProxy.setScaleX(containerView, 1.0f); - ViewProxy.setScaleY(containerView, 1.0f); - ViewProxy.setAlpha(containerViewBack, 1.0f); - ViewProxy.setScaleX(containerViewBack, 1.0f); - ViewProxy.setScaleY(containerViewBack, 1.0f); + setAlpha(1.0f); + containerView.setAlpha(1.0f); + containerView.setScaleX(1.0f); + containerView.setScaleY(1.0f); + containerViewBack.setAlpha(1.0f); + containerViewBack.setScaleX(1.0f); + containerViewBack.setScaleY(1.0f); } public boolean checkTransitionAnimation() { @@ -610,11 +609,11 @@ public class ActionBarLayout extends FrameLayout { } float interpolated = decelerateInterpolator.getInterpolation(animationProgress); if (open) { - ViewProxy.setAlpha(containerView, interpolated); - ViewProxy.setTranslationX(containerView, AndroidUtilities.dp(48) * (1.0f - interpolated)); + containerView.setAlpha(interpolated); + containerView.setTranslationX(AndroidUtilities.dp(48) * (1.0f - interpolated)); } else { - ViewProxy.setAlpha(containerViewBack, 1.0f - interpolated); - ViewProxy.setTranslationX(containerViewBack, AndroidUtilities.dp(48) * interpolated); + containerViewBack.setAlpha(1.0f - interpolated); + containerViewBack.setTranslationX(AndroidUtilities.dp(48) * interpolated); } if (animationProgress < 1) { startLayoutAnimation(open, false); @@ -641,7 +640,7 @@ public class ActionBarLayout extends FrameLayout { if (parentActivity.getCurrentFocus() != null) { AndroidUtilities.hideKeyboard(parentActivity.getCurrentFocus()); } - boolean needAnimation = Build.VERSION.SDK_INT > 10 && !forceWithoutAnimation && parentActivity.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE).getBoolean("view_animations", true); + boolean needAnimation = !forceWithoutAnimation && parentActivity.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE).getBoolean("view_animations", true); final BaseFragment currentFragment = !fragmentsStack.isEmpty() ? fragmentsStack.get(fragmentsStack.size() - 1) : null; @@ -706,21 +705,21 @@ public class ActionBarLayout extends FrameLayout { fragment.onBecomeFullyVisible(); } }; - ArrayList animators = new ArrayList<>(); - animators.add(ObjectAnimatorProxy.ofFloat(this, "alpha", 0.0f, 1.0f)); + ArrayList animators = new ArrayList<>(); + animators.add(ObjectAnimator.ofFloat(this, "alpha", 0.0f, 1.0f)); if (backgroundView != null) { backgroundView.setVisibility(VISIBLE); - animators.add(ObjectAnimatorProxy.ofFloat(backgroundView, "alpha", 0.0f, 1.0f)); + animators.add(ObjectAnimator.ofFloat(backgroundView, "alpha", 0.0f, 1.0f)); } fragment.onTransitionAnimationStart(true, false); - currentAnimation = new AnimatorSetProxy(); + currentAnimation = new AnimatorSet(); currentAnimation.playTogether(animators); currentAnimation.setInterpolator(accelerateDecelerateInterpolator); currentAnimation.setDuration(200); currentAnimation.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { onAnimationEndCheck(false); } }); @@ -738,19 +737,19 @@ public class ActionBarLayout extends FrameLayout { presentFragmentInternalRemoveOld(removeLast, currentFragment); fragment.onTransitionAnimationEnd(true, false); fragment.onBecomeFullyVisible(); - ViewProxy.setTranslationX(containerView, 0); + containerView.setTranslationX(0); } }; fragment.onTransitionAnimationStart(true, false); - AnimatorSetProxy animation = fragment.onCustomTransitionAnimation(true, new Runnable() { + AnimatorSet animation = fragment.onCustomTransitionAnimation(true, new Runnable() { @Override public void run() { onAnimationEndCheck(false); } }); if (animation == null) { - ViewProxy.setAlpha(containerView, 0.0f); - ViewProxy.setTranslationX(containerView, 48.0f); + containerView.setAlpha(0.0f); + containerView.setTranslationX(48.0f); if (containerView.isKeyboardVisible || containerViewBack.isKeyboardVisible) { waitingForKeyboardCloseRunnable = new Runnable() { @Override @@ -782,14 +781,14 @@ public class ActionBarLayout extends FrameLayout { //containerView.setLayerType(LAYER_TYPE_HARDWARE, null); //containerViewBack.setLayerType(LAYER_TYPE_HARDWARE, null); } - ViewProxy.setAlpha(containerView, 1.0f); - ViewProxy.setTranslationX(containerView, 0.0f); + containerView.setAlpha(1.0f); + containerView.setTranslationX(0.0f); currentAnimation = animation; } } } else { if (backgroundView != null) { - ViewProxy.setAlpha(backgroundView, 1.0f); + backgroundView.setAlpha(1.0f); backgroundView.setVisibility(VISIBLE); } fragment.onTransitionAnimationStart(true, false); @@ -849,7 +848,7 @@ public class ActionBarLayout extends FrameLayout { AndroidUtilities.hideKeyboard(parentActivity.getCurrentFocus()); } setInnerTranslationX(0); - boolean needAnimation = Build.VERSION.SDK_INT > 10 && animated && parentActivity.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE).getBoolean("view_animations", true); + boolean needAnimation = animated && parentActivity.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE).getBoolean("view_animations", true); final BaseFragment currentFragment = fragmentsStack.get(fragmentsStack.size() - 1); BaseFragment previousFragment = null; if (fragmentsStack.size() > 1) { @@ -912,13 +911,13 @@ public class ActionBarLayout extends FrameLayout { containerViewBack.setLayerType(LAYER_TYPE_NONE, null); } closeLastFragmentInternalRemoveOld(currentFragment); - ViewProxy.setTranslationX(containerViewBack, 0); + containerViewBack.setTranslationX(0); currentFragment.onTransitionAnimationEnd(false, false); previousFragmentFinal.onTransitionAnimationEnd(true, true); previousFragmentFinal.onBecomeFullyVisible(); } }; - AnimatorSetProxy animation = currentFragment.onCustomTransitionAnimation(false, new Runnable() { + AnimatorSet animation = currentFragment.onCustomTransitionAnimation(false, new Runnable() { @Override public void run() { onAnimationEndCheck(false); @@ -970,24 +969,24 @@ public class ActionBarLayout extends FrameLayout { } }; - ArrayList animators = new ArrayList<>(); - animators.add(ObjectAnimatorProxy.ofFloat(this, "alpha", 1.0f, 0.0f)); + ArrayList animators = new ArrayList<>(); + animators.add(ObjectAnimator.ofFloat(this, "alpha", 1.0f, 0.0f)); if (backgroundView != null) { - animators.add(ObjectAnimatorProxy.ofFloat(backgroundView, "alpha", 1.0f, 0.0f)); + animators.add(ObjectAnimator.ofFloat(backgroundView, "alpha", 1.0f, 0.0f)); } - currentAnimation = new AnimatorSetProxy(); + currentAnimation = new AnimatorSet(); currentAnimation.playTogether(animators); currentAnimation.setInterpolator(accelerateDecelerateInterpolator); currentAnimation.setDuration(200); currentAnimation.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationStart(Object animation) { + public void onAnimationStart(Animator animation) { transitionAnimationStartTime = System.currentTimeMillis(); } @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { onAnimationEndCheck(false); } }); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ActionBarMenu.java b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ActionBarMenu.java index b0fa9ab09..0ca40d4e1 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ActionBarMenu.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ActionBarMenu.java @@ -97,7 +97,8 @@ public class ActionBarMenu extends LinearLayout { } public void hideAllPopupMenus() { - for (int a = 0; a < getChildCount(); a++) { + int count = getChildCount(); + for (int a = 0; a < count; a++) { View view = getChildAt(a); if (view instanceof ActionBarMenuItem) { ((ActionBarMenuItem) view).closeSubMenu(); @@ -116,7 +117,8 @@ public class ActionBarMenu extends LinearLayout { } public void onMenuButtonPressed() { - for (int a = 0; a < getChildCount(); a++) { + int count = getChildCount(); + for (int a = 0; a < count; a++) { View view = getChildAt(a); if (view instanceof ActionBarMenuItem) { ActionBarMenuItem item = (ActionBarMenuItem) view; @@ -135,7 +137,8 @@ public class ActionBarMenu extends LinearLayout { } public void closeSearchField() { - for (int a = 0; a < getChildCount(); a++) { + int count = getChildCount(); + for (int a = 0; a < count; a++) { View view = getChildAt(a); if (view instanceof ActionBarMenuItem) { ActionBarMenuItem item = (ActionBarMenuItem) view; @@ -148,7 +151,8 @@ public class ActionBarMenu extends LinearLayout { } public void openSearchField(boolean toggle, String text) { - for (int a = 0; a < getChildCount(); a++) { + int count = getChildCount(); + for (int a = 0; a < count; a++) { View view = getChildAt(a); if (view instanceof ActionBarMenuItem) { ActionBarMenuItem item = (ActionBarMenuItem) view; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ActionBarMenuItem.java b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ActionBarMenuItem.java index ae135b33d..0706cfaa0 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ActionBarMenuItem.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ActionBarMenuItem.java @@ -15,7 +15,6 @@ import android.text.Editable; import android.text.TextWatcher; import android.util.TypedValue; import android.view.ActionMode; -import android.view.ContextMenu; import android.view.Gravity; import android.view.KeyEvent; import android.view.Menu; @@ -33,13 +32,11 @@ import android.widget.TextView; import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.LocaleController; import org.telegram.messenger.R; -import org.telegram.messenger.AnimationCompat.ViewProxy; -import org.telegram.ui.Components.FrameLayoutFixed; import org.telegram.ui.Components.LayoutHelper; import java.lang.reflect.Field; -public class ActionBarMenuItem extends FrameLayoutFixed { +public class ActionBarMenuItem extends FrameLayout { public static class ActionBarMenuItemSearchListener { public void onSearchExpand() { @@ -230,7 +227,7 @@ public class ActionBarMenuItem extends FrameLayoutFixed { textView.setGravity(Gravity.CENTER_VERTICAL | Gravity.RIGHT); } textView.setPadding(AndroidUtilities.dp(16), 0, AndroidUtilities.dp(16), 0); - textView.setTextSize(18); + textView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 18); textView.setMinWidth(AndroidUtilities.dp(196)); textView.setTag(id); textView.setText(text); @@ -336,6 +333,7 @@ public class ActionBarMenuItem extends FrameLayoutFixed { if (searchContainer.getVisibility() == VISIBLE) { if (listener == null || listener != null && listener.canCollapseSearch()) { searchContainer.setVisibility(GONE); + searchField.clearFocus(); setVisibility(VISIBLE); AndroidUtilities.hideKeyboard(searchField); if (listener != null) { @@ -376,16 +374,12 @@ public class ActionBarMenuItem extends FrameLayoutFixed { return searchField; } - public ActionBarMenuItem setIsSearchField(boolean value) { - return setIsSearchField(value, true); - } - public ActionBarMenuItem setOverrideMenuClick(boolean value) { overrideMenuClick = value; return this; } - public ActionBarMenuItem setIsSearchField(boolean value, boolean needClearButton) { + public ActionBarMenuItem setIsSearchField(boolean value) { if (parentMenu == null) { return this; } @@ -409,31 +403,23 @@ public class ActionBarMenuItem extends FrameLayoutFixed { searchField.setPadding(0, 0, 0, 0); int inputType = searchField.getInputType() | EditorInfo.TYPE_TEXT_FLAG_NO_SUGGESTIONS; searchField.setInputType(inputType); - if (android.os.Build.VERSION.SDK_INT < 11) { - searchField.setOnCreateContextMenuListener(new OnCreateContextMenuListener() { - public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { - menu.clear(); - } - }); - } else { - searchField.setCustomSelectionActionModeCallback(new ActionMode.Callback() { - public boolean onPrepareActionMode(ActionMode mode, Menu menu) { - return false; - } + searchField.setCustomSelectionActionModeCallback(new ActionMode.Callback() { + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + return false; + } - public void onDestroyActionMode(ActionMode mode) { + public void onDestroyActionMode(ActionMode mode) { - } + } - public boolean onCreateActionMode(ActionMode mode, Menu menu) { - return false; - } + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + return false; + } - public boolean onActionItemClicked(ActionMode mode, MenuItem item) { - return false; - } - }); - } + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + return false; + } + }); searchField.setOnEditorActionListener(new TextView.OnEditorActionListener() { @Override public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { @@ -458,7 +444,7 @@ public class ActionBarMenuItem extends FrameLayoutFixed { listener.onTextChanged(searchField); } if (clearButton != null) { - ViewProxy.setAlpha(clearButton, s == null || s.length() == 0 ? 0.6f : 1.0f); + clearButton.setAlpha(s == null || s.length() == 0 ? 0.6f : 1.0f); } } @@ -475,38 +461,33 @@ public class ActionBarMenuItem extends FrameLayoutFixed { } catch (Exception e) { //nothing to do } - if (Build.VERSION.SDK_INT >= 11) { - searchField.setImeOptions(EditorInfo.IME_FLAG_NO_FULLSCREEN | EditorInfo.IME_ACTION_SEARCH); - searchField.setTextIsSelectable(false); - } else { - searchField.setImeOptions(EditorInfo.IME_ACTION_SEARCH); - } + searchField.setImeOptions(EditorInfo.IME_FLAG_NO_FULLSCREEN | EditorInfo.IME_ACTION_SEARCH); + searchField.setTextIsSelectable(false); searchContainer.addView(searchField); FrameLayout.LayoutParams layoutParams2 = (FrameLayout.LayoutParams) searchField.getLayoutParams(); layoutParams2.width = LayoutHelper.MATCH_PARENT; layoutParams2.gravity = Gravity.CENTER_VERTICAL; layoutParams2.height = AndroidUtilities.dp(36); - layoutParams2.rightMargin = needClearButton ? AndroidUtilities.dp(48) : 0; + layoutParams2.rightMargin = AndroidUtilities.dp(48); searchField.setLayoutParams(layoutParams2); - if (needClearButton) { - clearButton = new ImageView(getContext()); - clearButton.setImageResource(R.drawable.ic_close_white); - clearButton.setScaleType(ImageView.ScaleType.CENTER); - clearButton.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - searchField.setText(""); - AndroidUtilities.showKeyboard(searchField); - } - }); - searchContainer.addView(clearButton); - layoutParams2 = (FrameLayout.LayoutParams) clearButton.getLayoutParams(); - layoutParams2.width = AndroidUtilities.dp(48); - layoutParams2.gravity = Gravity.CENTER_VERTICAL | Gravity.RIGHT; - layoutParams2.height = LayoutHelper.MATCH_PARENT; - clearButton.setLayoutParams(layoutParams2); - } + clearButton = new ImageView(getContext()); + clearButton.setImageResource(R.drawable.ic_close_white); + clearButton.setScaleType(ImageView.ScaleType.CENTER); + clearButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + searchField.setText(""); + searchField.requestFocus(); + AndroidUtilities.showKeyboard(searchField); + } + }); + searchContainer.addView(clearButton); + layoutParams2 = (FrameLayout.LayoutParams) clearButton.getLayoutParams(); + layoutParams2.width = AndroidUtilities.dp(48); + layoutParams2.gravity = Gravity.CENTER_VERTICAL | Gravity.RIGHT; + layoutParams2.height = LayoutHelper.MATCH_PARENT; + clearButton.setLayoutParams(layoutParams2); } isSearchField = value; return this; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/BaseFragment.java b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/BaseFragment.java index e898fa7bb..200187b41 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/BaseFragment.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/BaseFragment.java @@ -8,6 +8,7 @@ package org.telegram.ui.ActionBar; +import android.animation.AnimatorSet; import android.app.Activity; import android.app.Dialog; import android.content.Context; @@ -17,7 +18,6 @@ import android.os.Bundle; import android.view.View; import android.view.ViewGroup; -import org.telegram.messenger.AnimationCompat.AnimatorSetProxy; import org.telegram.messenger.FileLog; import org.telegram.tgnet.ConnectionsManager; @@ -259,7 +259,7 @@ public class BaseFragment { } - protected AnimatorSetProxy onCustomTransitionAnimation(boolean isOpen, final Runnable callback) { + protected AnimatorSet onCustomTransitionAnimation(boolean isOpen, final Runnable callback) { return null; } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/BottomSheet.java b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/BottomSheet.java index f4ae2f34e..e92a95d0c 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/BottomSheet.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/BottomSheet.java @@ -8,7 +8,9 @@ package org.telegram.ui.ActionBar; +import android.animation.Animator; import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; import android.annotation.SuppressLint; import android.app.Dialog; import android.content.Context; @@ -40,14 +42,10 @@ import android.widget.ImageView; import android.widget.TextView; import org.telegram.messenger.AndroidUtilities; -import org.telegram.messenger.AnimationCompat.AnimatorListenerAdapterProxy; -import org.telegram.messenger.AnimationCompat.AnimatorSetProxy; -import org.telegram.messenger.AnimationCompat.ObjectAnimatorProxy; -import org.telegram.messenger.AnimationCompat.ViewProxy; +import org.telegram.messenger.AnimatorListenerAdapterProxy; import org.telegram.messenger.LocaleController; import org.telegram.messenger.FileLog; import org.telegram.messenger.R; -import org.telegram.ui.Components.FrameLayoutFixed; import org.telegram.ui.Components.LayoutHelper; import java.util.ArrayList; @@ -96,7 +94,7 @@ public class BottomSheet extends Dialog { private BottomSheetDelegateInterface delegate; - protected Object currentSheetAnimation; + protected AnimatorSet currentSheetAnimation; private class ContainerView extends FrameLayout implements NestedScrollingParent { @@ -106,7 +104,7 @@ public class BottomSheet extends Dialog { private int startedTrackingPointerId; private boolean maybeStartTracking = false; private boolean startedTracking = false; - private AnimatorSetProxy currentAnimation = null; + private AnimatorSet currentAnimation = null; private NestedScrollingParentHelper nestedScrollingParentHelper; public ContainerView(Context context) { @@ -134,7 +132,7 @@ public class BottomSheet extends Dialog { if (dismissed) { return; } - float currentTranslation = ViewProxy.getTranslationY(containerView); + float currentTranslation = containerView.getTranslationY(); checkDismiss(0, 0); } @@ -145,12 +143,12 @@ public class BottomSheet extends Dialog { } cancelCurrentAnimation(); if (dyUnconsumed != 0) { - float currentTranslation = ViewProxy.getTranslationY(containerView); + float currentTranslation = containerView.getTranslationY(); currentTranslation -= dyUnconsumed; if (currentTranslation < 0) { currentTranslation = 0; } - ViewProxy.setTranslationY(containerView, currentTranslation); + containerView.setTranslationY(currentTranslation); } } @@ -160,7 +158,7 @@ public class BottomSheet extends Dialog { return; } cancelCurrentAnimation(); - float currentTranslation = ViewProxy.getTranslationY(containerView); + float currentTranslation = containerView.getTranslationY(); if (currentTranslation > 0 && dy > 0) { currentTranslation -= dy; consumed[1] = dy; @@ -168,7 +166,7 @@ public class BottomSheet extends Dialog { currentTranslation = 0; consumed[1] += currentTranslation; } - ViewProxy.setTranslationY(containerView, currentTranslation); + containerView.setTranslationY(currentTranslation); } } @@ -188,7 +186,7 @@ public class BottomSheet extends Dialog { } private void checkDismiss(float velX, float velY) { - float translationY = ViewProxy.getTranslationY(containerView); + float translationY = containerView.getTranslationY(); boolean backAnimation = translationY < AndroidUtilities.getPixelsInCM(0.8f, false) && (velY < 3500 || Math.abs(velY) < Math.abs(velX)) || velY < 0 && Math.abs(velY) >= 3500; if (!backAnimation) { boolean allowOld = allowCustomAnimation; @@ -197,13 +195,13 @@ public class BottomSheet extends Dialog { dismiss(); allowCustomAnimation = allowOld; } else { - currentAnimation = new AnimatorSetProxy(); - currentAnimation.playTogether(ObjectAnimatorProxy.ofFloat(containerView, "translationY", 0)); + currentAnimation = new AnimatorSet(); + currentAnimation.playTogether(ObjectAnimator.ofFloat(containerView, "translationY", 0)); currentAnimation.setDuration((int) (150 * (translationY / AndroidUtilities.getPixelsInCM(0.8f, false)))); currentAnimation.setInterpolator(new DecelerateInterpolator()); currentAnimation.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (currentAnimation != null && currentAnimation.equals(animation)) { currentAnimation = null; } @@ -251,12 +249,12 @@ public class BottomSheet extends Dialog { startedTracking = true; requestDisallowInterceptTouchEvent(true); } else if (startedTracking) { - float translationY = ViewProxy.getTranslationY(containerView); + float translationY = containerView.getTranslationY(); translationY += dy; if (translationY < 0) { translationY = 0; } - ViewProxy.setTranslationY(containerView, translationY); + containerView.setTranslationY(translationY); startedTrackingY = (int) ev.getY(); } } else if (ev == null || ev != null && ev.getPointerId(0) == startedTrackingPointerId && (ev.getAction() == MotionEvent.ACTION_CANCEL || ev.getAction() == MotionEvent.ACTION_UP || ev.getAction() == MotionEvent.ACTION_POINTER_UP)) { @@ -264,7 +262,7 @@ public class BottomSheet extends Dialog { velocityTracker = VelocityTracker.obtain(); } velocityTracker.computeCurrentVelocity(1000); - float translationY = ViewProxy.getTranslationY(containerView); + float translationY = containerView.getTranslationY(); if (startedTracking || translationY != 0) { checkDismiss(velocityTracker.getXVelocity(), velocityTracker.getYVelocity()); startedTracking = false; @@ -286,6 +284,7 @@ public class BottomSheet extends Dialog { int height = MeasureSpec.getSize(heightMeasureSpec); if (lastInsets != null && Build.VERSION.SDK_INT >= 21) { width -= lastInsets.getSystemWindowInsetRight() + lastInsets.getSystemWindowInsetLeft(); + height -= lastInsets.getSystemWindowInsetBottom(); } setMeasuredDimension(width, height); @@ -299,12 +298,7 @@ public class BottomSheet extends Dialog { } else { widthSpec = MeasureSpec.makeMeasureSpec(isPortrait ? width + backgroundPaddingLeft * 2 : (int) Math.max(width * 0.8f, Math.min(AndroidUtilities.dp(480), width)) + backgroundPaddingLeft * 2, MeasureSpec.EXACTLY); } - if (lastInsets != null && Build.VERSION.SDK_INT >= 21 && focusable) { - containerView.getLayoutParams(); - containerView.measure(widthSpec, MeasureSpec.makeMeasureSpec(height - lastInsets.getSystemWindowInsetBottom(), MeasureSpec.AT_MOST)); - } else { - containerView.measure(widthSpec, MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST)); - } + containerView.measure(widthSpec, MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST)); } else { containerView.measure(MeasureSpec.makeMeasureSpec(width + backgroundPaddingLeft * 2, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST)); } @@ -327,9 +321,6 @@ public class BottomSheet extends Dialog { if (lastInsets != null && Build.VERSION.SDK_INT >= 21) { left += lastInsets.getSystemWindowInsetLeft(); right += lastInsets.getSystemWindowInsetLeft(); - if (focusable) { - t -= lastInsets.getSystemWindowInsetBottom(); - } } int l = ((right - left) - containerView.getMeasuredWidth()) / 2; containerView.layout(l, t, l + containerView.getMeasuredWidth(), t + containerView.getMeasuredHeight()); @@ -497,7 +488,7 @@ public class BottomSheet extends Dialog { public BottomSheet(Context context, boolean needFocus) { super(context, R.style.TransparentDialog); - if (Build.VERSION.SDK_INT >= 21 && !"N".equals(Build.VERSION.CODENAME)) { + if (Build.VERSION.SDK_INT >= 21) { getWindow().addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); } ViewConfiguration vc = ViewConfiguration.get(context); @@ -512,7 +503,7 @@ public class BottomSheet extends Dialog { container = new ContainerView(getContext()); container.setBackgroundDrawable(backDrawable); focusable = needFocus; - if (Build.VERSION.SDK_INT >= 21 && !"N".equals(Build.VERSION.CODENAME)) { + if (Build.VERSION.SDK_INT >= 21) { container.setFitsSystemWindows(true); container.setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() { @SuppressLint("NewApi") @@ -527,7 +518,7 @@ public class BottomSheet extends Dialog { } ciclePaint.setColor(0xffffffff); - backDrawable.setAlpha(Build.VERSION.SDK_INT >= 11 ? 0 : 51); + backDrawable.setAlpha(0); } @Override @@ -539,7 +530,7 @@ public class BottomSheet extends Dialog { setContentView(container, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); if (containerView == null) { - containerView = new FrameLayoutFixed(getContext()) { + containerView = new FrameLayout(getContext()) { @Override public boolean hasOverlappingRendering() { return false; @@ -609,9 +600,7 @@ public class BottomSheet extends Dialog { if (!focusable) { params.flags |= WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; } - if (Build.VERSION.SDK_INT < 21) { - params.height = ViewGroup.LayoutParams.MATCH_PARENT; - } + params.height = ViewGroup.LayoutParams.MATCH_PARENT; window.setAttributes(params); } @@ -626,9 +615,7 @@ public class BottomSheet extends Dialog { if (containerView.getMeasuredHeight() == 0) { containerView.measure(View.MeasureSpec.makeMeasureSpec(AndroidUtilities.displaySize.x, View.MeasureSpec.AT_MOST), View.MeasureSpec.makeMeasureSpec(AndroidUtilities.displaySize.y, View.MeasureSpec.AT_MOST)); } - if (Build.VERSION.SDK_INT >= 11) { - backDrawable.setAlpha(0); - } + backDrawable.setAlpha(0); if (Build.VERSION.SDK_INT >= 18) { layoutCount = 2; AndroidUtilities.runOnUIThread(startAnimationRunnable = new Runnable() { @@ -667,11 +654,8 @@ public class BottomSheet extends Dialog { } private void cancelSheetAnimation() { - if (currentSheetAnimation instanceof AnimatorSetProxy) { - ((AnimatorSetProxy) currentSheetAnimation).cancel(); - currentSheetAnimation = null; - } else if (Build.VERSION.SDK_INT >= 11 && currentSheetAnimation instanceof AnimatorSet) { - ((AnimatorSet) currentSheetAnimation).cancel(); + if (currentSheetAnimation != null) { + currentSheetAnimation.cancel(); currentSheetAnimation = null; } } @@ -683,41 +667,35 @@ public class BottomSheet extends Dialog { if (Build.VERSION.SDK_INT >= 20) { container.setLayerType(View.LAYER_TYPE_HARDWARE, null); } - ViewProxy.setTranslationY(containerView, containerView.getMeasuredHeight()); - AnimatorSetProxy animatorSetProxy = new AnimatorSetProxy(); - if (Build.VERSION.SDK_INT < 11) { - animatorSetProxy.playTogether(ObjectAnimatorProxy.ofFloat(containerView, "translationY", 0)); - } else { - animatorSetProxy.playTogether( - ObjectAnimatorProxy.ofFloat(containerView, "translationY", 0), - ObjectAnimatorProxy.ofInt(backDrawable, "alpha", 51)); - } - animatorSetProxy.setDuration(200); - animatorSetProxy.setStartDelay(20); - animatorSetProxy.setInterpolator(new DecelerateInterpolator()); - animatorSetProxy.addListener(new AnimatorListenerAdapterProxy() { + containerView.setTranslationY(containerView.getMeasuredHeight()); + AnimatorSet animatorSet = new AnimatorSet(); + animatorSet.playTogether( + ObjectAnimator.ofFloat(containerView, "translationY", 0), + ObjectAnimator.ofInt(backDrawable, "alpha", 51)); + animatorSet.setDuration(200); + animatorSet.setStartDelay(20); + animatorSet.setInterpolator(new DecelerateInterpolator()); + animatorSet.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (currentSheetAnimation != null && currentSheetAnimation.equals(animation)) { currentSheetAnimation = null; if (delegate != null) { delegate.onOpenAnimationEnd(); } - if (Build.VERSION.SDK_INT >= 11) { - container.setLayerType(View.LAYER_TYPE_NONE, null); - } + container.setLayerType(View.LAYER_TYPE_NONE, null); } } @Override - public void onAnimationCancel(Object animation) { + public void onAnimationCancel(Animator animation) { if (currentSheetAnimation != null && currentSheetAnimation.equals(animation)) { currentSheetAnimation = null; } } }); - animatorSetProxy.start(); - currentSheetAnimation = animatorSetProxy; + animatorSet.start(); + currentSheetAnimation = animatorSet; } } @@ -755,20 +733,16 @@ public class BottomSheet extends Dialog { } dismissed = true; cancelSheetAnimation(); - AnimatorSetProxy animatorSetProxy = new AnimatorSetProxy(); - if (Build.VERSION.SDK_INT < 11) { - animatorSetProxy.playTogether(ObjectAnimatorProxy.ofFloat(containerView, "translationY", containerView.getMeasuredHeight() + AndroidUtilities.dp(10))); - } else { - animatorSetProxy.playTogether( - ObjectAnimatorProxy.ofFloat(containerView, "translationY", containerView.getMeasuredHeight() + AndroidUtilities.dp(10)), - ObjectAnimatorProxy.ofInt(backDrawable, "alpha", 0) - ); - } - animatorSetProxy.setDuration(180); - animatorSetProxy.setInterpolator(new AccelerateInterpolator()); - animatorSetProxy.addListener(new AnimatorListenerAdapterProxy() { + AnimatorSet animatorSet = new AnimatorSet(); + animatorSet.playTogether( + ObjectAnimator.ofFloat(containerView, "translationY", containerView.getMeasuredHeight() + AndroidUtilities.dp(10)), + ObjectAnimator.ofInt(backDrawable, "alpha", 0) + ); + animatorSet.setDuration(180); + animatorSet.setInterpolator(new AccelerateInterpolator()); + animatorSet.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (currentSheetAnimation != null && currentSheetAnimation.equals(animation)) { currentSheetAnimation = null; if (onClickListener != null) { @@ -788,14 +762,14 @@ public class BottomSheet extends Dialog { } @Override - public void onAnimationCancel(Object animation) { + public void onAnimationCancel(Animator animation) { if (currentSheetAnimation != null && currentSheetAnimation.equals(animation)) { currentSheetAnimation = null; } } }); - animatorSetProxy.start(); - currentSheetAnimation = animatorSetProxy; + animatorSet.start(); + currentSheetAnimation = animatorSet; } @Override @@ -806,26 +780,22 @@ public class BottomSheet extends Dialog { dismissed = true; cancelSheetAnimation(); if (!allowCustomAnimation || !onCustomCloseAnimation()) { - AnimatorSetProxy animatorSetProxy = new AnimatorSetProxy(); - if (Build.VERSION.SDK_INT < 11) { - animatorSetProxy.playTogether(ObjectAnimatorProxy.ofFloat(containerView, "translationY", containerView.getMeasuredHeight() + AndroidUtilities.dp(10))); - } else { - animatorSetProxy.playTogether( - ObjectAnimatorProxy.ofFloat(containerView, "translationY", containerView.getMeasuredHeight() + AndroidUtilities.dp(10)), - ObjectAnimatorProxy.ofInt(backDrawable, "alpha", 0) - ); - } + AnimatorSet animatorSet = new AnimatorSet(); + animatorSet.playTogether( + ObjectAnimator.ofFloat(containerView, "translationY", containerView.getMeasuredHeight() + AndroidUtilities.dp(10)), + ObjectAnimator.ofInt(backDrawable, "alpha", 0) + ); if (useFastDismiss) { int height = containerView.getMeasuredHeight(); - animatorSetProxy.setDuration(Math.max(60, (int) (180 * (height - ViewProxy.getTranslationY(containerView)) / (float) height))); + animatorSet.setDuration(Math.max(60, (int) (180 * (height - containerView.getTranslationY()) / (float) height))); useFastDismiss = false; } else { - animatorSetProxy.setDuration(180); + animatorSet.setDuration(180); } - animatorSetProxy.setInterpolator(new AccelerateInterpolator()); - animatorSetProxy.addListener(new AnimatorListenerAdapterProxy() { + animatorSet.setInterpolator(new AccelerateInterpolator()); + animatorSet.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (currentSheetAnimation != null && currentSheetAnimation.equals(animation)) { currentSheetAnimation = null; AndroidUtilities.runOnUIThread(new Runnable() { @@ -842,14 +812,14 @@ public class BottomSheet extends Dialog { } @Override - public void onAnimationCancel(Object animation) { + public void onAnimationCancel(Animator animation) { if (currentSheetAnimation != null && currentSheetAnimation.equals(animation)) { currentSheetAnimation = null; } } }); - animatorSetProxy.start(); - currentSheetAnimation = animatorSetProxy; + animatorSet.start(); + currentSheetAnimation = animatorSet; } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/DrawerLayoutContainer.java b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/DrawerLayoutContainer.java index 7a7d21947..df50bcb90 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/DrawerLayoutContainer.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/DrawerLayoutContainer.java @@ -8,6 +8,9 @@ package org.telegram.ui.ActionBar; +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; import android.annotation.SuppressLint; import android.app.Activity; import android.content.Context; @@ -28,9 +31,7 @@ import android.widget.ListView; import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.FileLog; import org.telegram.messenger.R; -import org.telegram.messenger.AnimationCompat.AnimatorListenerAdapterProxy; -import org.telegram.messenger.AnimationCompat.AnimatorSetProxy; -import org.telegram.messenger.AnimationCompat.ObjectAnimatorProxy; +import org.telegram.messenger.AnimatorListenerAdapterProxy; public class DrawerLayoutContainer extends FrameLayout { @@ -39,14 +40,14 @@ public class DrawerLayoutContainer extends FrameLayout { private ViewGroup drawerLayout; private ActionBarLayout parentActionBarLayout; - private boolean maybeStartTracking = false; - private boolean startedTracking = false; + private boolean maybeStartTracking; + private boolean startedTracking; private int startedTrackingX; private int startedTrackingY; private int startedTrackingPointerId; - private VelocityTracker velocityTracker = null; + private VelocityTracker velocityTracker; private boolean beginTrackingSent; - private AnimatorSetProxy currentAnimation = null; + private AnimatorSet currentAnimation; private Paint scrimPaint = new Paint(); @@ -57,8 +58,8 @@ public class DrawerLayoutContainer extends FrameLayout { private Drawable shadowLeft; private boolean allowOpenDrawer; - private float drawerPosition = 0; - private boolean drawerOpened = false; + private float drawerPosition; + private boolean drawerOpened; private boolean allowDrawContent = true; public DrawerLayoutContainer(Context context) { @@ -138,7 +139,7 @@ public class DrawerLayoutContainer extends FrameLayout { } else if (drawerPosition < 0) { drawerPosition = 0; } - requestLayout(); + drawerLayout.setTranslationX(drawerPosition); final int newVisibility = drawerPosition > 0 ? VISIBLE : GONE; if (drawerLayout.getVisibility() != newVisibility) { @@ -166,10 +167,8 @@ public class DrawerLayoutContainer extends FrameLayout { AndroidUtilities.hideKeyboard(parentActionBarLayout.parentActivity.getCurrentFocus()); } cancelCurrentAnimation(); - AnimatorSetProxy animatorSet = new AnimatorSetProxy(); - animatorSet.playTogether( - ObjectAnimatorProxy.ofFloat(this, "drawerPosition", drawerLayout.getMeasuredWidth()) - ); + AnimatorSet animatorSet = new AnimatorSet(); + animatorSet.playTogether(ObjectAnimator.ofFloat(this, "drawerPosition", drawerLayout.getMeasuredWidth())); animatorSet.setInterpolator(new DecelerateInterpolator()); if (fast) { animatorSet.setDuration(Math.max((int) (200.0f / drawerLayout.getMeasuredWidth() * (drawerLayout.getMeasuredWidth() - drawerPosition)), 50)); @@ -178,7 +177,7 @@ public class DrawerLayoutContainer extends FrameLayout { } animatorSet.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animator) { + public void onAnimationEnd(Animator animator) { onDrawerAnimationEnd(true); } }); @@ -188,9 +187,9 @@ public class DrawerLayoutContainer extends FrameLayout { public void closeDrawer(boolean fast) { cancelCurrentAnimation(); - AnimatorSetProxy animatorSet = new AnimatorSetProxy(); + AnimatorSet animatorSet = new AnimatorSet(); animatorSet.playTogether( - ObjectAnimatorProxy.ofFloat(this, "drawerPosition", 0) + ObjectAnimator.ofFloat(this, "drawerPosition", 0) ); animatorSet.setInterpolator(new DecelerateInterpolator()); if (fast) { @@ -200,7 +199,7 @@ public class DrawerLayoutContainer extends FrameLayout { } animatorSet.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animator) { + public void onAnimationEnd(Animator animator) { onDrawerAnimationEnd(false); } }); @@ -213,7 +212,7 @@ public class DrawerLayoutContainer extends FrameLayout { drawerOpened = opened; if (!opened) { if (drawerLayout instanceof ListView) { - ((ListView)drawerLayout).setSelectionFromTop(0, 0); + ((ListView) drawerLayout).setSelectionFromTop(0, 0); } } } @@ -298,8 +297,8 @@ public class DrawerLayoutContainer extends FrameLayout { requestDisallowInterceptTouchEvent(true); } else if (startedTracking) { if (!beginTrackingSent) { - if (((Activity)getContext()).getCurrentFocus() != null) { - AndroidUtilities.hideKeyboard(((Activity)getContext()).getCurrentFocus()); + if (((Activity) getContext()).getCurrentFocus() != null) { + AndroidUtilities.hideKeyboard(((Activity) getContext()).getCurrentFocus()); } beginTrackingSent = true; } @@ -379,7 +378,7 @@ public class DrawerLayoutContainer extends FrameLayout { if (drawerLayout != child) { child.layout(lp.leftMargin, lp.topMargin, lp.leftMargin + child.getMeasuredWidth(), lp.topMargin + child.getMeasuredHeight()); } else { - child.layout(-child.getMeasuredWidth() + (int)drawerPosition, lp.topMargin, (int)drawerPosition, lp.topMargin + child.getMeasuredHeight()); + child.layout(-child.getMeasuredWidth(), lp.topMargin, 0, lp.topMargin + child.getMeasuredHeight()); } } catch (Exception e) { FileLog.e("tmessages", e); @@ -482,7 +481,7 @@ public class DrawerLayoutContainer extends FrameLayout { } else if (shadowLeft != null) { final float alpha = Math.max(0, Math.min(drawerPosition / AndroidUtilities.dp(20), 1.0f)); if (alpha != 0) { - shadowLeft.setBounds((int)drawerPosition, child.getTop(), (int)drawerPosition + shadowLeft.getIntrinsicWidth(), child.getBottom()); + shadowLeft.setBounds((int) drawerPosition, child.getTop(), (int) drawerPosition + shadowLeft.getIntrinsicWidth(), child.getBottom()); shadowLeft.setAlpha((int) (0xff * alpha)); shadowLeft.draw(canvas); } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/SimpleTextView.java b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/SimpleTextView.java index 82f219832..165b72c63 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/SimpleTextView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/SimpleTextView.java @@ -41,14 +41,6 @@ public class SimpleTextView extends View implements Drawable.Callback { private int textHeight; private boolean wasLayout; - public enum Alignment { - ALIGN_NORMAL, - ALIGN_OPPOSITE, - ALIGN_CENTER, - ALIGN_LEFT, - ALIGN_RIGHT - } - public SimpleTextView(Context context) { super(context); textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); @@ -71,7 +63,9 @@ public class SimpleTextView extends View implements Drawable.Callback { return; } textPaint.setTextSize(newSize); - recreateLayoutMaybe(); + if (!recreateLayoutMaybe()) { + invalidate(); + } } public void setGravity(int value) { @@ -97,7 +91,7 @@ public class SimpleTextView extends View implements Drawable.Callback { return textPaint; } - private void createLayout(int width) { + private boolean createLayout(int width) { if (text != null) { try { if (leftDrawable != null) { @@ -111,7 +105,7 @@ public class SimpleTextView extends View implements Drawable.Callback { width -= getPaddingLeft() + getPaddingRight(); CharSequence string = TextUtils.ellipsize(text, textPaint, width, TextUtils.TruncateAt.END); if (layout != null && TextUtils.equals(layout.getText(), string)) { - return; + return false; } layout = new StaticLayout(string, 0, string.length(), textPaint, width + AndroidUtilities.dp(8), Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); @@ -135,6 +129,7 @@ public class SimpleTextView extends View implements Drawable.Callback { textHeight = 0; } invalidate(); + return true; } @Override @@ -144,7 +139,7 @@ public class SimpleTextView extends View implements Drawable.Callback { createLayout(width - getPaddingLeft() - getPaddingRight()); int finalHeight; - if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY) { + if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY) { finalHeight = height; } else { finalHeight = textHeight; @@ -192,7 +187,9 @@ public class SimpleTextView extends View implements Drawable.Callback { if (drawable != null) { drawable.setCallback(this); } - recreateLayoutMaybe(); + if (!recreateLayoutMaybe()) { + invalidate(); + } } public void setRightDrawable(Drawable drawable) { @@ -206,7 +203,9 @@ public class SimpleTextView extends View implements Drawable.Callback { if (drawable != null) { drawable.setCallback(this); } - recreateLayoutMaybe(); + if (!recreateLayoutMaybe()) { + invalidate(); + } } public void setText(CharSequence value) { @@ -222,15 +221,18 @@ public class SimpleTextView extends View implements Drawable.Callback { return; } drawablePadding = value; - recreateLayoutMaybe(); + if (!recreateLayoutMaybe()) { + invalidate(); + } } - private void recreateLayoutMaybe() { + private boolean recreateLayoutMaybe() { if (wasLayout) { - createLayout(getMeasuredWidth()); + return createLayout(getMeasuredWidth()); } else { requestLayout(); } + return true; } public CharSequence getText() { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/Theme.java b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/Theme.java index 52e5dcf31..bfdec5741 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/Theme.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/Theme.java @@ -61,6 +61,7 @@ public class Theme { public static final int DIALOGS_NAME_TEXT_COLOR = 0xff4d83b3; public static final int DIALOGS_ATTACH_TEXT_COLOR = 0xff4d83b3; public static final int DIALOGS_PRINTING_TEXT_COLOR = 0xff4d83b3; + public static final int DIALOGS_DRAFT_TEXT_COLOR = 0xffdd4b39; public static final int CHAT_UNREAD_TEXT_COLOR = 0xff5695cc; public static final int CHAT_ADD_CONTACT_TEXT_COLOR = 0xff4a82b5; @@ -69,6 +70,7 @@ public class Theme { public static final int CHAT_BOTTOM_CHAT_OVERLAY_TEXT_COLOR = 0xff3a8ccf; public static final int CHAT_GIF_HINT_TEXT_COLOR = 0xffffffff; public static final int CHAT_EMPTY_VIEW_TEXT_COLOR = 0xffffffff; + public static final int CHAT_SEARCH_COUNT_TEXT_COLOR = 0xff4e9ad4; public static final int INAPP_PLAYER_PERFORMER_TEXT_COLOR = 0xff2f3438; public static final int INAPP_PLAYER_TITLE_TEXT_COLOR = 0xff2f3438; @@ -186,6 +188,7 @@ public class Theme { public static final int MSG_TEXT_COLOR = 0xff000000; public static final int MSG_LINK_TEXT_COLOR = 0xff2678b6; public static final int MSG_LINK_SELECT_BACKGROUND_COLOR = 0x3362a9e3; + public static final int MSG_TEXT_SELECT_BACKGROUND_COLOR = 0x6662a9e3; public static Drawable backgroundDrawableIn; @@ -421,9 +424,7 @@ public class Theme { stateListDrawable.addState(new int[]{android.R.attr.state_pressed}, new ColorDrawable(color)); stateListDrawable.addState(new int[]{android.R.attr.state_focused}, new ColorDrawable(color)); stateListDrawable.addState(new int[]{android.R.attr.state_selected}, new ColorDrawable(color)); - if (Build.VERSION.SDK_INT >= 11) { - stateListDrawable.addState(new int[]{android.R.attr.state_activated}, new ColorDrawable(color)); - } + stateListDrawable.addState(new int[]{android.R.attr.state_activated}, new ColorDrawable(color)); stateListDrawable.addState(new int[]{}, new ColorDrawable(0x00000000)); return stateListDrawable; } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Adapters/ContactsAdapter.java b/TMessagesProj/src/main/java/org/telegram/ui/Adapters/ContactsAdapter.java index 13f79f1ff..ade259fc5 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Adapters/ContactsAdapter.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Adapters/ContactsAdapter.java @@ -9,7 +9,6 @@ package org.telegram.ui.Adapters; import android.content.Context; -import android.os.Build; import android.view.View; import android.view.ViewGroup; @@ -19,7 +18,6 @@ import org.telegram.tgnet.TLRPC; import org.telegram.messenger.ContactsController; import org.telegram.messenger.MessagesController; import org.telegram.messenger.R; -import org.telegram.messenger.AnimationCompat.ViewProxy; import org.telegram.ui.Cells.DividerCell; import org.telegram.ui.Cells.GreySectionCell; import org.telegram.ui.Cells.LetterSectionCell; @@ -229,12 +227,13 @@ public class ContactsAdapter extends BaseSectionsAdapter { convertView = new TextCell(mContext); } ContactsController.Contact contact = ContactsController.getInstance().phoneBookContacts.get(position); + TextCell textCell = (TextCell) convertView; if (contact.first_name != null && contact.last_name != null) { - ((TextCell) convertView).setText(contact.first_name + " " + contact.last_name); + textCell.setText(contact.first_name + " " + contact.last_name); } else if (contact.first_name != null && contact.last_name == null) { - ((TextCell) convertView).setText(contact.first_name); + textCell.setText(contact.first_name); } else { - ((TextCell) convertView).setText(contact.last_name); + textCell.setText(contact.last_name); } } else if (type == 0) { if (convertView == null) { @@ -249,13 +248,13 @@ public class ContactsAdapter extends BaseSectionsAdapter { TLRPC.User user = MessagesController.getInstance().getUser(arr.get(position).user_id); ((UserCell) convertView).setData(user, null, null, 0); if (checkedMap != null) { - ((UserCell) convertView).setChecked(checkedMap.containsKey(user.id), !scrolling && Build.VERSION.SDK_INT > 10); + ((UserCell) convertView).setChecked(checkedMap.containsKey(user.id), !scrolling); } if (ignoreUsers != null) { if (ignoreUsers.containsKey(user.id)) { - ViewProxy.setAlpha(convertView, 0.5f); + convertView.setAlpha(0.5f); } else { - ViewProxy.setAlpha(convertView, 1.0f); + convertView.setAlpha(1.0f); } } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Adapters/DialogsAdapter.java b/TMessagesProj/src/main/java/org/telegram/ui/Adapters/DialogsAdapter.java index 11a1ccec8..46557a3ac 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Adapters/DialogsAdapter.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Adapters/DialogsAdapter.java @@ -49,7 +49,7 @@ public class DialogsAdapter extends RecyclerView.Adapter { return current != getItemCount() || current == 1; } - private ArrayList getDialogsArray() { + private ArrayList getDialogsArray() { if (dialogsType == 0) { return MessagesController.getInstance().dialogs; } else if (dialogsType == 1) { @@ -73,8 +73,8 @@ public class DialogsAdapter extends RecyclerView.Adapter { return count; } - public TLRPC.Dialog getItem(int i) { - ArrayList arrayList = getDialogsArray(); + public TLRPC.TL_dialog getItem(int i) { + ArrayList arrayList = getDialogsArray(); if (i < 0 || i >= arrayList.size()) { return null; } @@ -110,7 +110,7 @@ public class DialogsAdapter extends RecyclerView.Adapter { if (viewHolder.getItemViewType() == 0) { DialogCell cell = (DialogCell) viewHolder.itemView; cell.useSeparator = (i != getItemCount() - 1); - TLRPC.Dialog dialog = getItem(i); + TLRPC.TL_dialog dialog = getItem(i); if (dialogsType == 0) { if (AndroidUtilities.isTablet()) { cell.setDialogSelected(dialog.id == openedDialogId); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Adapters/DialogsSearchAdapter.java b/TMessagesProj/src/main/java/org/telegram/ui/Adapters/DialogsSearchAdapter.java index c98cf45ae..45c34f658 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Adapters/DialogsSearchAdapter.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Adapters/DialogsSearchAdapter.java @@ -48,6 +48,7 @@ import java.util.HashMap; import java.util.Locale; import java.util.Timer; import java.util.TimerTask; +import java.util.concurrent.ConcurrentHashMap; public class DialogsSearchAdapter extends BaseSearchAdapterRecycler { @@ -113,7 +114,7 @@ public class DialogsSearchAdapter extends BaseSearchAdapterRecycler { HintDialogCell cell = (HintDialogCell) holder.itemView; TLRPC.TL_topPeer peer = SearchQuery.hints.get(position); - TLRPC.Dialog dialog = new TLRPC.Dialog(); + TLRPC.TL_dialog dialog = new TLRPC.TL_dialog(); TLRPC.Chat chat = null; TLRPC.User user = null; int did = 0; @@ -227,8 +228,17 @@ public class DialogsSearchAdapter extends BaseSearchAdapterRecycler { if (req.offset_id == 0) { searchResultMessages.clear(); } - for (TLRPC.Message message : res.messages) { + for (int a = 0; a < res.messages.size(); a++) { + TLRPC.Message message = res.messages.get(a); searchResultMessages.add(new MessageObject(message, null, false)); + long dialog_id = MessageObject.getDialogId(message); + ConcurrentHashMap read_max = message.out ? MessagesController.getInstance().dialogs_read_outbox_max : MessagesController.getInstance().dialogs_read_inbox_max; + Integer value = read_max.get(dialog_id); + if (value == null) { + value = MessagesStorage.getInstance().getDialogReadMax(message.out, dialog_id); + read_max.put(dialog_id, value); + } + message.unread = value < message.id; } messagesSearchEndReached = res.messages.size() != 20; notifyDataSetChanged(); @@ -958,6 +968,7 @@ public class DialogsSearchAdapter extends BaseSearchAdapterRecycler { return super.onInterceptTouchEvent(e); } }; + horizontalListView.setTag(9); horizontalListView.setItemAnimator(null); horizontalListView.setLayoutAnimation(null); LinearLayoutManager layoutManager = new LinearLayoutManager(mContext) { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/AudioPlayerActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/AudioPlayerActivity.java index 7be4b309f..2d8b76f4e 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/AudioPlayerActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/AudioPlayerActivity.java @@ -318,8 +318,7 @@ public class AudioPlayerActivity extends BaseFragment implements NotificationCen updateTitle(id == NotificationCenter.audioDidReset && (Boolean) args[1]); } else if (id == NotificationCenter.audioProgressDidChanged) { MessageObject messageObject = MediaController.getInstance().getPlayingMessageObject(); - - if (messageObject.isMusic()) { + if (messageObject != null && messageObject.isMusic()) { updateProgress(messageObject); } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/AudioSelectActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/AudioSelectActivity.java index ae365bbe0..92ff709ab 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/AudioSelectActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/AudioSelectActivity.java @@ -10,7 +10,6 @@ package org.telegram.ui; import android.content.Context; import android.database.Cursor; -import android.os.Build; import android.provider.MediaStore; import android.view.Gravity; import android.view.View; @@ -108,9 +107,7 @@ public class AudioSelectActivity extends BaseFragment implements NotificationCen listView.setDivider(null); listView.setDividerHeight(0); listView.setAdapter(listViewAdapter = new ListAdapter(context)); - if (Build.VERSION.SDK_INT >= 11) { - listView.setVerticalScrollbarPosition(LocaleController.isRTL ? ListView.SCROLLBAR_POSITION_LEFT : ListView.SCROLLBAR_POSITION_RIGHT); - } + listView.setVerticalScrollbarPosition(LocaleController.isRTL ? ListView.SCROLLBAR_POSITION_LEFT : ListView.SCROLLBAR_POSITION_RIGHT); frameLayout.addView(listView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, Gravity.LEFT | Gravity.TOP, 0, 0, 0, 48)); listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/BlockedUsersActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/BlockedUsersActivity.java index efde21e42..fb41fc92f 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/BlockedUsersActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/BlockedUsersActivity.java @@ -118,9 +118,7 @@ public class BlockedUsersActivity extends BaseFragment implements NotificationCe listView.setDivider(null); listView.setDividerHeight(0); listView.setAdapter(listViewAdapter = new ListAdapter(context)); - if (Build.VERSION.SDK_INT >= 11) { - listView.setVerticalScrollbarPosition(LocaleController.isRTL ? ListView.SCROLLBAR_POSITION_LEFT : ListView.SCROLLBAR_POSITION_RIGHT); - } + listView.setVerticalScrollbarPosition(LocaleController.isRTL ? ListView.SCROLLBAR_POSITION_LEFT : ListView.SCROLLBAR_POSITION_RIGHT); frameLayout.addView(listView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/CacheControlActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/CacheControlActivity.java index 032203b8d..81355575a 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/CacheControlActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/CacheControlActivity.java @@ -386,8 +386,6 @@ public class CacheControlActivity extends BaseFragment { SQLitePreparedStatement state5 = database.executeFast("REPLACE INTO messages_holes VALUES(?, ?, ?)"); SQLitePreparedStatement state6 = database.executeFast("REPLACE INTO media_holes_v2 VALUES(?, ?, ?, ?)"); - SQLitePreparedStatement state7 = database.executeFast("REPLACE INTO messages_imp_holes VALUES(?, ?, ?)"); - SQLitePreparedStatement state8 = database.executeFast("REPLACE INTO channel_group VALUES(?, ?, ?, ?)"); database.beginTransaction(); for (int a = 0; a < dialogsToCleanup.size(); a++) { @@ -403,7 +401,7 @@ public class CacheControlActivity extends BaseFragment { } cursor = database.queryFinalized("SELECT last_mid_i, last_mid FROM dialogs WHERE did = " + did); - ArrayList arrayList = new ArrayList<>(); + int messageId = -1; if (cursor.next()) { long last_mid_i = cursor.longValue(0); long last_mid = cursor.longValue(1); @@ -415,7 +413,7 @@ public class CacheControlActivity extends BaseFragment { TLRPC.Message message = TLRPC.Message.TLdeserialize(data, data.readInt32(false), false); data.reuse(); if (message != null) { - arrayList.add(message); + messageId = message.id; } } } @@ -425,22 +423,20 @@ public class CacheControlActivity extends BaseFragment { cursor2.dispose(); database.executeFast("DELETE FROM messages WHERE uid = " + did + " AND mid != " + last_mid_i + " AND mid != " + last_mid).stepThis().dispose(); - database.executeFast("DELETE FROM channel_group WHERE uid = " + did).stepThis().dispose(); database.executeFast("DELETE FROM messages_holes WHERE uid = " + did).stepThis().dispose(); - database.executeFast("DELETE FROM messages_imp_holes WHERE uid = " + did).stepThis().dispose(); database.executeFast("DELETE FROM bot_keyboard WHERE uid = " + did).stepThis().dispose(); database.executeFast("DELETE FROM media_counts_v2 WHERE uid = " + did).stepThis().dispose(); database.executeFast("DELETE FROM media_v2 WHERE uid = " + did).stepThis().dispose(); database.executeFast("DELETE FROM media_holes_v2 WHERE uid = " + did).stepThis().dispose(); BotQuery.clearBotKeyboard(did, null); - MessagesStorage.createFirstHoles(did, state5, state6, state7, state8, arrayList); + if (messageId != -1) { + MessagesStorage.createFirstHoles(did, state5, state6, messageId); + } } cursor.dispose(); } state5.dispose(); state6.dispose(); - state7.dispose(); - state8.dispose(); database.commitTransaction(); database.executeFast("VACUUM").stepThis().dispose(); } catch (Exception e) { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/AboutLinkCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/AboutLinkCell.java index 160f91b83..f4aa9af9b 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/AboutLinkCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/AboutLinkCell.java @@ -195,7 +195,13 @@ public class AboutLinkCell extends FrameLayout { if (pressedLink != null) { canvas.drawPath(urlPath, urlPaint); } - textLayout.draw(canvas); + try { + if (textLayout != null) { + textLayout.draw(canvas); + } + } catch (Exception e) { + FileLog.e("tmessages", e); + } canvas.restore(); } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/ChatActionCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/ChatActionCell.java index 5e6f04df9..4a12f052b 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/ChatActionCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/ChatActionCell.java @@ -12,7 +12,6 @@ import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Paint; -import android.graphics.RectF; import android.text.Layout; import android.text.Spannable; import android.text.StaticLayout; @@ -44,7 +43,6 @@ public class ChatActionCell extends BaseCell { private static TextPaint textPaint; private static Paint backPaint; - private static RectF rect; private URLSpan pressedLink; @@ -74,7 +72,6 @@ public class ChatActionCell extends BaseCell { textPaint.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); backPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - rect = new RectF(); } backPaint.setColor(ApplicationLoader.getServiceMessageColor()); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/ChatMessageCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/ChatMessageCell.java index f59a1621e..47222194a 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/ChatMessageCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/ChatMessageCell.java @@ -113,6 +113,7 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate private int totalHeight; private int keyboardHeight; private int linkBlockNum; + private int linkSelectionBlockNum; private Rect scrollRect = new Rect(); @@ -184,6 +185,7 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate private static TextPaint locationTitlePaint; private static TextPaint locationAddressPaint; private static Paint urlPaint; + private static Paint urlSelectionPaint; private static TextPaint durationPaint; private ClickableSpan pressedLink; @@ -191,6 +193,7 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate private boolean linkPreviewPressed; private ArrayList urlPathCache = new ArrayList<>(); private ArrayList urlPath = new ArrayList<>(); + private ArrayList urlPathSelection = new ArrayList<>(); private boolean useSeekBarWaweform; private SeekBar seekBar; @@ -352,6 +355,9 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate urlPaint = new Paint(); urlPaint.setColor(Theme.MSG_LINK_SELECT_BACKGROUND_COLOR); + urlSelectionPaint = new Paint(); + urlSelectionPaint.setColor(Theme.MSG_TEXT_SELECT_BACKGROUND_COLOR); + audioTimePaint = new TextPaint(TextPaint.ANTI_ALIAS_FLAG); audioTimePaint.setTextSize(dp(12)); @@ -421,21 +427,29 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate if (pressedLink == null || pressedLinkType != type && type != -1) { return; } - resetUrlPaths(); + resetUrlPaths(false); pressedLink = null; pressedLinkType = -1; invalidate(); } - private void resetUrlPaths() { - if (urlPath.isEmpty()) { - return; + private void resetUrlPaths(boolean text) { + if (text) { + if (urlPathSelection.isEmpty()) { + return; + } + urlPathCache.addAll(urlPathSelection); + urlPathSelection.clear(); + } else { + if (urlPath.isEmpty()) { + return; + } + urlPathCache.addAll(urlPath); + urlPath.clear(); } - urlPathCache.addAll(urlPath); - urlPath.clear(); } - private LinkPath obtainNewUrlPath() { + private LinkPath obtainNewUrlPath(boolean text) { LinkPath linkPath; if (!urlPathCache.isEmpty()) { linkPath = urlPathCache.get(0); @@ -443,7 +457,11 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate } else { linkPath = new LinkPath(); } - urlPath.add(linkPath); + if (text) { + urlPathSelection.add(linkPath); + } else { + urlPath.add(linkPath); + } return linkPath; } @@ -483,9 +501,9 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate pressedLink = link[0]; linkBlockNum = blockNum; pressedLinkType = 1; - resetUrlPaths(); + resetUrlPaths(false); try { - LinkPath path = obtainNewUrlPath(); + LinkPath path = obtainNewUrlPath(false); int start = buffer.getSpanStart(pressedLink) - block.charactersOffset; int end = buffer.getSpanEnd(pressedLink); int length = block.textLayout.getText().length(); @@ -499,7 +517,7 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate if (nextLink == null || nextLink.length == 0 || nextLink[0] != pressedLink) { break; } - path = obtainNewUrlPath(); + path = obtainNewUrlPath(false); path.setCurrentLayout(nextBlock.textLayout, 0, nextBlock.height); nextBlock.textLayout.getSelectionPath(0, end - nextBlock.charactersOffset, path); if (end < block.charactersOffset + length - 1) { @@ -515,7 +533,7 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate if (nextLink == null || nextLink.length == 0 || nextLink[0] != pressedLink) { break; } - path = obtainNewUrlPath(); + path = obtainNewUrlPath(false); start = buffer.getSpanStart(pressedLink) - nextBlock.charactersOffset; path.setCurrentLayout(nextBlock.textLayout, start, -nextBlock.height); nextBlock.textLayout.getSelectionPath(start, buffer.getSpanEnd(pressedLink) - nextBlock.charactersOffset, path); @@ -574,9 +592,9 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate if (!ignore) { pressedLink = link[0]; pressedLinkType = 3; - resetUrlPaths(); + resetUrlPaths(false); try { - LinkPath path = obtainNewUrlPath(); + LinkPath path = obtainNewUrlPath(false); int start = buffer.getSpanStart(pressedLink); path.setCurrentLayout(captionLayout, start, 0); captionLayout.getSelectionPath(start, buffer.getSpanEnd(pressedLink), path); @@ -615,11 +633,13 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate if (drawImageButton && buttonState != -1 && x >= buttonX && x <= buttonX + dp(48) && y >= buttonY && y <= buttonY + dp(48)) { buttonPressed = 1; return true; - } else if (documentAttachType == DOCUMENT_ATTACH_TYPE_GIF && buttonState == -1 && MediaController.getInstance().canAutoplayGifs()) { - linkPreviewPressed = false; - return false; } else { linkPreviewPressed = true; + TLRPC.WebPage webPage = currentMessageObject.messageOwner.media.webpage; + if (documentAttachType == DOCUMENT_ATTACH_TYPE_GIF && buttonState == -1 && MediaController.getInstance().canAutoplayGifs() && (photoImage.getAnimation() == null || !TextUtils.isEmpty(webPage.embed_url))) { + linkPreviewPressed = false; + return false; + } return true; } } else if (descriptionLayout != null && y >= descriptionY) { @@ -641,9 +661,9 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate pressedLink = link[0]; linkBlockNum = -10; pressedLinkType = 2; - resetUrlPaths(); + resetUrlPaths(false); try { - LinkPath path = obtainNewUrlPath(); + LinkPath path = obtainNewUrlPath(false); int start = buffer.getSpanStart(pressedLink); path.setCurrentLayout(descriptionLayout, start, 0); descriptionLayout.getSelectionPath(start, buffer.getSpanEnd(pressedLink), path); @@ -681,13 +701,17 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate if (drawImageButton) { if (documentAttachType == DOCUMENT_ATTACH_TYPE_GIF) { if (buttonState == -1) { - buttonState = 2; - currentMessageObject.audioProgress = 1; - photoImage.setAllowStartAnimation(false); - photoImage.stopAnimation(); - radialProgress.setBackground(getDrawableForCurrentState(), false, false); - invalidate(); - playSoundEffect(SoundEffectConstants.CLICK); + if (MediaController.getInstance().canAutoplayGifs()) { + delegate.didPressedImage(this); + } else { + buttonState = 2; + currentMessageObject.audioProgress = 1; + photoImage.setAllowStartAnimation(false); + photoImage.stopAnimation(); + radialProgress.setBackground(getDrawableForCurrentState(), false, false); + invalidate(); + playSoundEffect(SoundEffectConstants.CLICK); + } } else if (buttonState == 2 || buttonState == 0) { didPressedButton(false); playSoundEffect(SoundEffectConstants.CLICK); @@ -699,7 +723,7 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate } else { TLRPC.WebPage webPage = currentMessageObject.messageOwner.media.webpage; if (webPage != null) { - if (Build.VERSION.SDK_INT >= 16 && webPage.embed_url != null && webPage.embed_url.length() != 0) { + if (Build.VERSION.SDK_INT >= 16 && !TextUtils.isEmpty(webPage.embed_url)) { delegate.needOpenWebView(webPage.embed_url, webPage.site_name, webPage.description, webPage.url, webPage.embed_width, webPage.embed_height); } else { Browser.openUrl(getContext(), webPage.url); @@ -718,7 +742,7 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate } private boolean checkOtherButtonMotionEvent(MotionEvent event) { - if (documentAttachType != DOCUMENT_ATTACH_TYPE_DOCUMENT && currentMessageObject.type != 12 && documentAttachType != DOCUMENT_ATTACH_TYPE_MUSIC && documentAttachType != DOCUMENT_ATTACH_TYPE_VIDEO) { + if (documentAttachType != DOCUMENT_ATTACH_TYPE_DOCUMENT && currentMessageObject.type != 12 && documentAttachType != DOCUMENT_ATTACH_TYPE_MUSIC && documentAttachType != DOCUMENT_ATTACH_TYPE_VIDEO && documentAttachType != DOCUMENT_ATTACH_TYPE_GIF && currentMessageObject.type != 8) { return false; } @@ -777,14 +801,16 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate } } } - if (imagePressed && currentMessageObject.isSecretPhoto()) { - imagePressed = false; - } else if (imagePressed && currentMessageObject.isSendError()) { - imagePressed = false; - result = false; - } else if (imagePressed && currentMessageObject.type == 8 && buttonState == -1 && MediaController.getInstance().canAutoplayGifs()) { - imagePressed = false; - result = false; + if (imagePressed) { + if (currentMessageObject.isSecretPhoto()) { + imagePressed = false; + } else if (currentMessageObject.isSendError()) { + imagePressed = false; + result = false; + } else if (currentMessageObject.type == 8 && buttonState == -1 && MediaController.getInstance().canAutoplayGifs() && photoImage.getAnimation() == null) { + imagePressed = false; + result = false; + } } } else { if (event.getAction() == MotionEvent.ACTION_UP) { @@ -792,6 +818,7 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate buttonPressed = 0; playSoundEffect(SoundEffectConstants.CLICK); didPressedButton(false); + radialProgress.swapBackground(getDrawableForCurrentState()); invalidate(); } else if (imagePressed) { imagePressed = false; @@ -1192,7 +1219,7 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate StaticLayout layout = new StaticLayout(text, paint, smallWidth, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); for (int a = 0; a < linesCount; a++) { Layout.Directions directions = layout.getLineDirections(a); - if (layout.getLineLeft(a) != 0 || Build.VERSION.SDK_INT >= 14 && (layout.isRtlCharAt(layout.getLineStart(a)) || layout.isRtlCharAt(layout.getLineEnd(a)))) { + if (layout.getLineLeft(a) != 0 || layout.isRtlCharAt(layout.getLineStart(a)) || layout.isRtlCharAt(layout.getLineEnd(a))) { maxWidth = smallWidth; } int pos = layout.getLineEnd(a); @@ -1225,12 +1252,16 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate delegate.didPressedUserAvatar(this, user); } else if (currentMessageObject.type == 8) { if (buttonState == -1) { - buttonState = 2; - currentMessageObject.audioProgress = 1; - photoImage.setAllowStartAnimation(false); - photoImage.stopAnimation(); - radialProgress.setBackground(getDrawableForCurrentState(), false, false); - invalidate(); + if (MediaController.getInstance().canAutoplayGifs()) { + delegate.didPressedImage(this); + } else { + buttonState = 2; + currentMessageObject.audioProgress = 1; + photoImage.setAllowStartAnimation(false); + photoImage.stopAnimation(); + radialProgress.setBackground(getDrawableForCurrentState(), false, false); + invalidate(); + } } else if (buttonState == 2 || buttonState == 0) { didPressedButton(false); } @@ -1639,6 +1670,56 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate } } + public void setHighlightedText(String text) { + if (currentMessageObject.messageOwner.message == null || currentMessageObject == null || currentMessageObject.type != 0 || TextUtils.isEmpty(currentMessageObject.messageText) || text == null) { + if (!urlPathSelection.isEmpty()) { + linkSelectionBlockNum = -1; + resetUrlPaths(true); + invalidate(); + } + return; + } + int start = TextUtils.indexOf(currentMessageObject.messageOwner.message.toLowerCase(), text.toLowerCase()); + if (start == -1) { + if (!urlPathSelection.isEmpty()) { + linkSelectionBlockNum = -1; + resetUrlPaths(true); + invalidate(); + } + return; + } + int end = start + text.length(); + for (int c = 0; c < currentMessageObject.textLayoutBlocks.size(); c++) { + MessageObject.TextLayoutBlock block = currentMessageObject.textLayoutBlocks.get(c); + if (start >= block.charactersOffset && start < block.charactersOffset + block.textLayout.getText().length()) { + linkSelectionBlockNum = c; + resetUrlPaths(true); + try { + LinkPath path = obtainNewUrlPath(true); + int length = block.textLayout.getText().length(); + path.setCurrentLayout(block.textLayout, start, 0); + block.textLayout.getSelectionPath(start, end - block.charactersOffset, path); + if (end >= block.charactersOffset + length) { + for (int a = c + 1; a < currentMessageObject.textLayoutBlocks.size(); a++) { + MessageObject.TextLayoutBlock nextBlock = currentMessageObject.textLayoutBlocks.get(a); + length = nextBlock.textLayout.getText().length(); + path = obtainNewUrlPath(true); + path.setCurrentLayout(nextBlock.textLayout, 0, nextBlock.height); + nextBlock.textLayout.getSelectionPath(0, end - nextBlock.charactersOffset, path); + if (end < block.charactersOffset + length - 1) { + break; + } + } + } + } catch (Exception e) { + FileLog.e("tmessages", e); + } + invalidate(); + break; + } + } + } + public void setMessageObject(MessageObject messageObject) { boolean messageIdChanged = currentMessageObject == null || currentMessageObject.getId() != messageObject.getId(); boolean messageChanged = currentMessageObject != messageObject || messageObject.forceUpdate; @@ -1817,7 +1898,7 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate boolean titleIsRTL = false; if (webPage.title != null) { try { - titleX = 0; + titleX = Integer.MAX_VALUE; if (linkPreviewHeight != 0) { linkPreviewHeight += dp(2); totalHeight += dp(2); @@ -1833,15 +1914,16 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate int height = titleLayout.getLineBottom(titleLayout.getLineCount() - 1); linkPreviewHeight += height; totalHeight += height; + boolean checkForRtl = true; for (int a = 0; a < titleLayout.getLineCount(); a++) { int lineLeft = (int) titleLayout.getLineLeft(a); if (lineLeft != 0) { titleIsRTL = true; - if (titleX == 0) { - titleX = -lineLeft; - } else { - titleX = Math.max(titleX, -lineLeft); - } + } + if (titleX == Integer.MAX_VALUE) { + titleX = -lineLeft; + } else { + titleX = Math.max(titleX, -lineLeft); } int width; if (lineLeft != 0) { @@ -1949,14 +2031,14 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate } maxWebWidth = width + additinalWidth; } - if (restLines == 0 || !isSmallImage) { + /*if (restLines == 0 || !isSmallImage) { if (titleIsRTL) { titleX = -dp(4); } if (authorIsRTL) { authorX = -dp(4); } - } + }*/ maxChildWidth = Math.max(maxChildWidth, width + additinalWidth); } } catch (Exception e) { @@ -2213,9 +2295,6 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate String phone = messageObject.messageOwner.media.phone_number; if (phone != null && phone.length() != 0) { - if (!phone.startsWith("+")) { - phone = "+" + phone; - } phone = PhoneFormat.getInstance().format(phone); } else { phone = LocaleController.getString("NumberUnknown", R.string.NumberUnknown); @@ -2294,16 +2373,28 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate backgroundWidth -= dp(20); } int maxWidth = backgroundWidth - dp(86 + 52); - createDocumentLayout(maxWidth, messageObject); + if (!TextUtils.isEmpty(messageObject.caption)) { + maxWidth += AndroidUtilities.dp(86); + } if (drawPhotoImage) { photoWidth = dp(86); photoHeight = dp(86); } else { photoWidth = dp(56); photoHeight = dp(56); + maxWidth += AndroidUtilities.dp(TextUtils.isEmpty(messageObject.caption) ? 51 : 21); } availableTimeWidth = maxWidth; + if (!drawPhotoImage) { + if (TextUtils.isEmpty(messageObject.caption) && infoLayout.getLineCount() > 0) { + measureTime(messageObject); + int timeLeft = backgroundWidth - AndroidUtilities.dp(40 + 18 + 56 + 8) - (int) Math.ceil(infoLayout.getLineWidth(0)); + if (timeLeft < timeWidth) { + photoHeight += AndroidUtilities.dp(8); + } + } + } } else if (messageObject.type == 4) { //geo double lat = messageObject.messageOwner.media.geo.lat; double lon = messageObject.messageOwner.media.geo._long; @@ -2881,6 +2972,11 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate canvas.drawPath(urlPath.get(b), urlPaint); } } + if (a == linkSelectionBlockNum && !urlPathSelection.isEmpty()) { + for (int b = 0; b < urlPathSelection.size(); b++) { + canvas.drawPath(urlPathSelection.get(b), urlSelectionPaint); + } + } try { block.textLayout.draw(canvas); } catch (Exception e) { @@ -3017,7 +3113,12 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate } } - if (documentAttachType == DOCUMENT_ATTACH_TYPE_MUSIC) { + if (documentAttachType == DOCUMENT_ATTACH_TYPE_GIF || currentMessageObject.type == 8) { + if (photoImage.getVisible()) { + setDrawableBounds(Theme.docMenuDrawable[3], otherX = photoImage.getImageX() + photoImage.getImageWidth() - dp(14), otherY = photoImage.getImageY() + dp(8.1f)); + Theme.docMenuDrawable[3].draw(canvas); + } + } else if (documentAttachType == DOCUMENT_ATTACH_TYPE_MUSIC) { if (currentMessageObject.isOutOwner()) { audioTitlePaint.setColor(Theme.MSG_OUT_AUDIO_TITLE_TEXT_COLOR); audioPerformerPaint.setColor(Theme.MSG_OUT_AUDIO_PERFORMER_TEXT_COLOR); @@ -3091,20 +3192,22 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate } if (currentMessageObject.type == 1 || documentAttachType == DOCUMENT_ATTACH_TYPE_VIDEO) { - if (documentAttachType == DOCUMENT_ATTACH_TYPE_VIDEO) { - setDrawableBounds(Theme.docMenuDrawable[3], otherX = photoImage.getImageX() + photoImage.getImageWidth() - dp(14), otherY = photoImage.getImageY() + dp(8.1f)); - Theme.docMenuDrawable[3].draw(canvas); - } + if (photoImage.getVisible()) { + if (documentAttachType == DOCUMENT_ATTACH_TYPE_VIDEO) { + setDrawableBounds(Theme.docMenuDrawable[3], otherX = photoImage.getImageX() + photoImage.getImageWidth() - dp(14), otherY = photoImage.getImageY() + dp(8.1f)); + Theme.docMenuDrawable[3].draw(canvas); + } - if (infoLayout != null && (buttonState == 1 || buttonState == 0 || buttonState == 3 || currentMessageObject.isSecretPhoto())) { - infoPaint.setColor(Theme.MSG_MEDIA_INFO_TEXT_COLOR); - setDrawableBounds(Theme.timeBackgroundDrawable, photoImage.getImageX() + dp(4), photoImage.getImageY() + dp(4), infoWidth + dp(8), dp(16.5f)); - Theme.timeBackgroundDrawable.draw(canvas); + if (infoLayout != null && (buttonState == 1 || buttonState == 0 || buttonState == 3 || currentMessageObject.isSecretPhoto())) { + infoPaint.setColor(Theme.MSG_MEDIA_INFO_TEXT_COLOR); + setDrawableBounds(Theme.timeBackgroundDrawable, photoImage.getImageX() + dp(4), photoImage.getImageY() + dp(4), infoWidth + dp(8), dp(16.5f)); + Theme.timeBackgroundDrawable.draw(canvas); - canvas.save(); - canvas.translate(photoImage.getImageX() + dp(8), photoImage.getImageY() + dp(5.5f)); - infoLayout.draw(canvas); - canvas.restore(); + canvas.save(); + canvas.translate(photoImage.getImageX() + dp(8), photoImage.getImageY() + dp(5.5f)); + infoLayout.draw(canvas); + canvas.restore(); + } } } else { if (currentMessageObject.type == 4) { @@ -3160,7 +3263,7 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate canvas.save(); if (currentMessageObject.type == 1 || documentAttachType == DOCUMENT_ATTACH_TYPE_VIDEO || currentMessageObject.type == 8) { canvas.translate(captionX = photoImage.getImageX() + dp(5), captionY = photoImage.getImageY() + photoImage.getImageHeight() + dp(6)); - } else {//TODO + } else { canvas.translate(captionX = currentBackgroundDrawable.getBounds().left + dp(currentMessageObject.isOutOwner() ? 11 : 17), captionY = totalHeight - captionHeight - dp(10)); } if (pressedLink != null) { @@ -3266,7 +3369,7 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate FileLog.e("tmessages", e); } } - if (drawImageButton) { + if (drawImageButton && photoImage.getVisible()) { radialProgress.draw(canvas); } @@ -3953,11 +4056,11 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate currentForwardUser = null; currentForwardNameString = null; + currentForwardChannel = null; forwardedNameLayout[0] = null; forwardedNameLayout[1] = null; forwardedNameWidth = 0; if (drawForwardedName && messageObject.isForwarded()) { - currentForwardChannel = null; if (messageObject.messageOwner.fwd_from.channel_id != 0) { currentForwardChannel = MessagesController.getInstance().getChat(messageObject.messageOwner.fwd_from.channel_id); } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/ContextLinkCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/ContextLinkCell.java index 8418b0b63..48755d591 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/ContextLinkCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/ContextLinkCell.java @@ -233,7 +233,7 @@ public class ContextLinkCell extends View implements MediaController.FileDownloa } } - int width = 1; + int width; int w = 0; int h = 0; @@ -268,7 +268,7 @@ public class ContextLinkCell extends View implements MediaController.FileDownloa if (mediaWebpage) { width = (int) (w / (h / (float) AndroidUtilities.dp(80))); - if (Build.VERSION.SDK_INT >= 11 && documentAttachType == DOCUMENT_ATTACH_TYPE_GIF) { + if (documentAttachType == DOCUMENT_ATTACH_TYPE_GIF) { currentPhotoFilterThumb = currentPhotoFilter = String.format(Locale.US, "%d_%d_b", (int) (width / AndroidUtilities.density), 80); } else { currentPhotoFilter = String.format(Locale.US, "%d_%d", (int) (width / AndroidUtilities.density), 80); @@ -277,9 +277,10 @@ public class ContextLinkCell extends View implements MediaController.FileDownloa } else { currentPhotoFilter = "52_52"; } + linkImageView.setAspectFit(documentAttachType == DOCUMENT_ATTACH_TYPE_STICKER); if (documentAttachType == DOCUMENT_ATTACH_TYPE_GIF) { - if (documentAttach != null && Build.VERSION.SDK_INT >= 11) { + if (documentAttach != null) { linkImageView.setImage(documentAttach, null, currentPhotoObject != null ? currentPhotoObject.location : null, currentPhotoFilter, documentAttach.size, ext, false); } else { linkImageView.setImage(null, url, null, null, currentPhotoObject != null ? currentPhotoObject.location : null, currentPhotoFilter, -1, ext, true); @@ -554,7 +555,7 @@ public class ContextLinkCell extends View implements MediaController.FileDownloa } public void updateButtonState(boolean animated) { - if (!mediaWebpage || Build.VERSION.SDK_INT < 11) { + if (!mediaWebpage) { return; } String fileName = null; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/DialogCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/DialogCell.java index 496864ad6..59c30be3a 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/DialogCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/DialogCell.java @@ -29,6 +29,7 @@ import org.telegram.messenger.LocaleController; import org.telegram.messenger.MessageObject; import org.telegram.messenger.UserObject; import org.telegram.messenger.FileLog; +import org.telegram.messenger.query.DraftQuery; import org.telegram.tgnet.TLRPC; import org.telegram.messenger.ContactsController; import org.telegram.messenger.Emoji; @@ -85,6 +86,7 @@ public class DialogCell extends BaseCell { private TLRPC.Chat chat = null; private TLRPC.EncryptedChat encryptedChat = null; private CharSequence lastPrintString = null; + private TLRPC.DraftMessage draftMessage; public boolean useSeparator = false; @@ -188,7 +190,7 @@ public class DialogCell extends BaseCell { avatarDrawable = new AvatarDrawable(); } - public void setDialog(TLRPC.Dialog dialog, int i, int type) { + public void setDialog(TLRPC.TL_dialog dialog, int i, int type) { currentDialogId = dialog.id; isDialogCell = true; index = i; @@ -322,117 +324,149 @@ public class DialogCell extends BaseCell { } } - if (message == null) { - if (printingString != null) { - lastPrintString = messageString = printingString; - currentMessagePaint = messagePrintingPaint; + int lastDate = lastMessageDate; + if (lastMessageDate == 0 && message != null) { + lastDate = message.messageOwner.date; + } + + if (isDialogCell) { + draftMessage = DraftQuery.getDraft(currentDialogId); + if (draftMessage != null && (TextUtils.isEmpty(draftMessage.message) && draftMessage.reply_to_msg_id == 0 || lastDate > draftMessage.date && unreadCount != 0) || + ChatObject.isChannel(chat) && !chat.megagroup && !chat.creator && !chat.editor || + chat != null && (chat.left || chat.kicked)) { + draftMessage = null; + } + } else { + draftMessage = null; + } + + if (printingString != null) { + lastPrintString = messageString = printingString; + currentMessagePaint = messagePrintingPaint; + } else { + lastPrintString = null; + + if (draftMessage != null) { + checkMessage = false; + if (TextUtils.isEmpty(draftMessage.message)) { + String draftString = LocaleController.getString("Draft", R.string.Draft); + SpannableStringBuilder stringBuilder = SpannableStringBuilder.valueOf(draftString); + stringBuilder.setSpan(new ForegroundColorSpan(Theme.DIALOGS_DRAFT_TEXT_COLOR), 0, draftString.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + messageString = stringBuilder; + } else { + String mess = draftMessage.message; + if (mess.length() > 150) { + mess = mess.substring(0, 150); + } + String draftString = LocaleController.getString("Draft", R.string.Draft); + SpannableStringBuilder stringBuilder = SpannableStringBuilder.valueOf(String.format("%s: %s", draftString, mess.replace('\n', ' '))); + stringBuilder.setSpan(new ForegroundColorSpan(Theme.DIALOGS_DRAFT_TEXT_COLOR), 0, draftString.length() + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + messageString = Emoji.replaceEmoji(stringBuilder, messagePaint.getFontMetricsInt(), AndroidUtilities.dp(20), false); + } } else { - lastPrintString = null; - if (encryptedChat != null) { - currentMessagePaint = messagePrintingPaint; - if (encryptedChat instanceof TLRPC.TL_encryptedChatRequested) { - messageString = LocaleController.getString("EncryptionProcessing", R.string.EncryptionProcessing); - } else if (encryptedChat instanceof TLRPC.TL_encryptedChatWaiting) { - if (user != null && user.first_name != null) { - messageString = LocaleController.formatString("AwaitingEncryption", R.string.AwaitingEncryption, user.first_name); - } else { - messageString = LocaleController.formatString("AwaitingEncryption", R.string.AwaitingEncryption, ""); - } - } else if (encryptedChat instanceof TLRPC.TL_encryptedChatDiscarded) { - messageString = LocaleController.getString("EncryptionRejected", R.string.EncryptionRejected); - } else if (encryptedChat instanceof TLRPC.TL_encryptedChat) { - if (encryptedChat.admin_id == UserConfig.getClientUserId()) { + if (message == null) { + if (encryptedChat != null) { + currentMessagePaint = messagePrintingPaint; + if (encryptedChat instanceof TLRPC.TL_encryptedChatRequested) { + messageString = LocaleController.getString("EncryptionProcessing", R.string.EncryptionProcessing); + } else if (encryptedChat instanceof TLRPC.TL_encryptedChatWaiting) { if (user != null && user.first_name != null) { - messageString = LocaleController.formatString("EncryptedChatStartedOutgoing", R.string.EncryptedChatStartedOutgoing, user.first_name); + messageString = LocaleController.formatString("AwaitingEncryption", R.string.AwaitingEncryption, user.first_name); } else { - messageString = LocaleController.formatString("EncryptedChatStartedOutgoing", R.string.EncryptedChatStartedOutgoing, ""); + messageString = LocaleController.formatString("AwaitingEncryption", R.string.AwaitingEncryption, ""); } + } else if (encryptedChat instanceof TLRPC.TL_encryptedChatDiscarded) { + messageString = LocaleController.getString("EncryptionRejected", R.string.EncryptionRejected); + } else if (encryptedChat instanceof TLRPC.TL_encryptedChat) { + if (encryptedChat.admin_id == UserConfig.getClientUserId()) { + if (user != null && user.first_name != null) { + messageString = LocaleController.formatString("EncryptedChatStartedOutgoing", R.string.EncryptedChatStartedOutgoing, user.first_name); + } else { + messageString = LocaleController.formatString("EncryptedChatStartedOutgoing", R.string.EncryptedChatStartedOutgoing, ""); + } + } else { + messageString = LocaleController.getString("EncryptedChatStartedIncoming", R.string.EncryptedChatStartedIncoming); + } + } + } + } else { + TLRPC.User fromUser = null; + TLRPC.Chat fromChat = null; + if (message.isFromUser()) { + fromUser = MessagesController.getInstance().getUser(message.messageOwner.from_id); + } else { + fromChat = MessagesController.getInstance().getChat(message.messageOwner.to_id.channel_id); + } + if (message.messageOwner instanceof TLRPC.TL_messageService) { + messageString = message.messageText; + currentMessagePaint = messagePrintingPaint; + } else { + if (chat != null && chat.id > 0 && fromChat == null) { + String name; + if (message.isOutOwner()) { + name = LocaleController.getString("FromYou", R.string.FromYou); + } else if (fromUser != null) { + name = UserObject.getFirstName(fromUser).replace("\n", ""); + } else if (fromChat != null) { + name = fromChat.title.replace("\n", ""); + } else { + name = "DELETED"; + } + checkMessage = false; + SpannableStringBuilder stringBuilder; + if (message.caption != null) { + String mess = message.caption.toString(); + if (mess.length() > 150) { + mess = mess.substring(0, 150); + } + stringBuilder = SpannableStringBuilder.valueOf(String.format("%s: %s", name, mess.replace('\n', ' '))); + } else if (message.messageOwner.media != null && !message.isMediaEmpty()) { + currentMessagePaint = messagePrintingPaint; + stringBuilder = SpannableStringBuilder.valueOf(String.format("%s: %s", name, message.messageText)); + stringBuilder.setSpan(new ForegroundColorSpan(Theme.DIALOGS_ATTACH_TEXT_COLOR), name.length() + 2, stringBuilder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } else if (message.messageOwner.message != null) { + String mess = message.messageOwner.message; + if (mess.length() > 150) { + mess = mess.substring(0, 150); + } + stringBuilder = SpannableStringBuilder.valueOf(String.format("%s: %s", name, mess.replace('\n', ' '))); + } else { + stringBuilder = SpannableStringBuilder.valueOf(""); + } + if (stringBuilder.length() > 0) { + stringBuilder.setSpan(new ForegroundColorSpan(Theme.DIALOGS_NAME_TEXT_COLOR), 0, name.length() + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + messageString = Emoji.replaceEmoji(stringBuilder, messagePaint.getFontMetricsInt(), AndroidUtilities.dp(20), false); } else { - messageString = LocaleController.getString("EncryptedChatStartedIncoming", R.string.EncryptedChatStartedIncoming); + if (message.caption != null) { + messageString = message.caption; + } else { + messageString = message.messageText; + if (message.messageOwner.media != null && !message.isMediaEmpty()) { + currentMessagePaint = messagePrintingPaint; + } + } } } } } - if (lastMessageDate != 0) { - timeString = LocaleController.stringForMessageListDate(lastMessageDate); - } + } + + if (draftMessage != null) { + timeString = LocaleController.stringForMessageListDate(draftMessage.date); + } else if (lastMessageDate != 0) { + timeString = LocaleController.stringForMessageListDate(lastMessageDate); + } else if (message != null) { + timeString = LocaleController.stringForMessageListDate(message.messageOwner.date); + } + + if (message == null) { drawCheck1 = false; drawCheck2 = false; drawClock = false; drawCount = false; drawError = false; } else { - TLRPC.User fromUser = null; - TLRPC.Chat fromChat = null; - if (message.isFromUser()) { - fromUser = MessagesController.getInstance().getUser(message.messageOwner.from_id); - } else { - fromChat = MessagesController.getInstance().getChat(message.messageOwner.to_id.channel_id); - } - - if (lastMessageDate != 0) { - timeString = LocaleController.stringForMessageListDate(lastMessageDate); - } else { - timeString = LocaleController.stringForMessageListDate(message.messageOwner.date); - } - if (printingString != null) { - lastPrintString = messageString = printingString; - currentMessagePaint = messagePrintingPaint; - } else { - lastPrintString = null; - if (message.messageOwner instanceof TLRPC.TL_messageService) { - messageString = message.messageText; - currentMessagePaint = messagePrintingPaint; - } else { - if (chat != null && chat.id > 0 && fromChat == null) { - String name; - if (message.isOutOwner()) { - name = LocaleController.getString("FromYou", R.string.FromYou); - } else if (fromUser != null) { - name = UserObject.getFirstName(fromUser).replace("\n", ""); - } else if (fromChat != null) { - name = fromChat.title.replace("\n", ""); - } else { - name = "DELETED"; - } - checkMessage = false; - SpannableStringBuilder stringBuilder; - if (message.caption != null) { - String mess = message.caption.toString(); - if (mess.length() > 150) { - mess = mess.substring(0, 150); - } - stringBuilder = SpannableStringBuilder.valueOf(String.format("%s: %s", name, mess.replace('\n', ' '))); - } else if (message.messageOwner.media != null && !message.isMediaEmpty()) { - currentMessagePaint = messagePrintingPaint; - stringBuilder = SpannableStringBuilder.valueOf(String.format("%s: %s", name, message.messageText)); - stringBuilder.setSpan(new ForegroundColorSpan(Theme.DIALOGS_ATTACH_TEXT_COLOR), name.length() + 2, stringBuilder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - } else if (message.messageOwner.message != null) { - String mess = message.messageOwner.message; - if (mess.length() > 150) { - mess = mess.substring(0, 150); - } - stringBuilder = SpannableStringBuilder.valueOf(String.format("%s: %s", name, mess.replace('\n', ' '))); - } else { - stringBuilder = SpannableStringBuilder.valueOf(""); - } - if (stringBuilder.length() > 0) { - stringBuilder.setSpan(new ForegroundColorSpan(Theme.DIALOGS_NAME_TEXT_COLOR), 0, name.length() + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - } - messageString = Emoji.replaceEmoji(stringBuilder, messagePaint.getFontMetricsInt(), AndroidUtilities.dp(20), false); - } else { - if (message.caption != null) { - messageString = message.caption; - } else { - messageString = message.messageText; - if (message.messageOwner.media != null && !message.isMediaEmpty()) { - currentMessagePaint = messagePrintingPaint; - } - } - } - } - } - if (unreadCount != 0) { drawCount = true; countString = String.format("%d", unreadCount); @@ -440,7 +474,7 @@ public class DialogCell extends BaseCell { drawCount = false; } - if (message.isOut()) { + if (message.isOut() && draftMessage == null) { if (message.isSending()) { drawCheck1 = false; drawCheck2 = false; @@ -453,13 +487,8 @@ public class DialogCell extends BaseCell { drawError = true; drawCount = false; } else if (message.isSent()) { - if (!message.isUnread()) { - drawCheck1 = true; - drawCheck2 = true; - } else { - drawCheck1 = false; - drawCheck2 = true; - } + drawCheck1 = !message.isUnread() || ChatObject.isChannel(chat) && !chat.megagroup; + drawCheck2 = true; drawClock = false; drawError = false; } @@ -686,7 +715,7 @@ public class DialogCell extends BaseCell { isSelected = value; } - private ArrayList getDialogsArray() { + private ArrayList getDialogsArray() { if (dialogsType == 0) { return MessagesController.getInstance().dialogs; } else if (dialogsType == 1) { @@ -699,13 +728,15 @@ public class DialogCell extends BaseCell { public void checkCurrentDialogIndex() { if (index < getDialogsArray().size()) { - TLRPC.Dialog dialog = getDialogsArray().get(index); + TLRPC.TL_dialog dialog = getDialogsArray().get(index); + TLRPC.DraftMessage newDraftMessage = DraftQuery.getDraft(currentDialogId); MessageObject newMessageObject = MessagesController.getInstance().dialogMessage.get(dialog.id); if (currentDialogId != dialog.id || message != null && message.getId() != dialog.top_message || newMessageObject != null && newMessageObject.messageOwner.edit_date != currentEditDate || unreadCount != dialog.unread_count || - message == null && newMessageObject != null) { + message != newMessageObject || + message == null && newMessageObject != null || newDraftMessage != draftMessage) { currentDialogId = dialog.id; update(0); } @@ -714,7 +745,7 @@ public class DialogCell extends BaseCell { public void update(int mask) { if (isDialogCell) { - TLRPC.Dialog dialog = MessagesController.getInstance().dialogs_dict.get(currentDialogId); + TLRPC.TL_dialog dialog = MessagesController.getInstance().dialogs_dict.get(currentDialogId); if (dialog != null && mask == 0) { message = MessagesController.getInstance().dialogMessage.get(dialog.id); lastUnreadState = message != null && message.isUnread(); @@ -762,7 +793,7 @@ public class DialogCell extends BaseCell { lastUnreadState = message.isUnread(); continueUpdate = true; } else if (isDialogCell) { - TLRPC.Dialog dialog = MessagesController.getInstance().dialogs_dict.get(currentDialogId); + TLRPC.TL_dialog dialog = MessagesController.getInstance().dialogs_dict.get(currentDialogId); if (dialog != null && unreadCount != dialog.unread_count) { unreadCount = dialog.unread_count; continueUpdate = true; @@ -931,4 +962,9 @@ public class DialogCell extends BaseCell { avatarImage.draw(canvas); } + + @Override + public boolean hasOverlappingRendering() { + return false; + } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/DrawerProfileCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/DrawerProfileCell.java index d2219fc22..2bd32cb5c 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/DrawerProfileCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/DrawerProfileCell.java @@ -92,6 +92,7 @@ public class DrawerProfileCell extends FrameLayout { try { super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(148), MeasureSpec.EXACTLY)); } catch (Exception e) { + setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), AndroidUtilities.dp(148)); FileLog.e("tmessages", e); } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/HintDialogCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/HintDialogCell.java index e6ad8ce91..671397db9 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/HintDialogCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/HintDialogCell.java @@ -95,7 +95,7 @@ public class HintDialogCell extends FrameLayout { if (mask != 0 && (mask & MessagesController.UPDATE_MASK_READ_DIALOG_MESSAGE) == 0 && (mask & MessagesController.UPDATE_MASK_NEW_MESSAGE) == 0) { return; } - TLRPC.Dialog dialog = MessagesController.getInstance().dialogs_dict.get(dialog_id); + TLRPC.TL_dialog dialog = MessagesController.getInstance().dialogs_dict.get(dialog_id); if (dialog != null && dialog.unread_count != 0) { if (lastUnreadCount != dialog.unread_count) { lastUnreadCount = dialog.unread_count; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/PhotoEditToolCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/PhotoEditToolCell.java index 89584407d..d4f4c065a 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/PhotoEditToolCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/PhotoEditToolCell.java @@ -12,14 +12,14 @@ import android.content.Context; import android.text.TextUtils; import android.util.TypedValue; import android.view.Gravity; +import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.TextView; import org.telegram.messenger.AndroidUtilities; -import org.telegram.ui.Components.FrameLayoutFixed; import org.telegram.ui.Components.LayoutHelper; -public class PhotoEditToolCell extends FrameLayoutFixed { +public class PhotoEditToolCell extends FrameLayout { private ImageView iconImage; private TextView nameTextView; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/PhotoPickerAlbumsCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/PhotoPickerAlbumsCell.java index 7fc1d5ec8..a559cdc5c 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/PhotoPickerAlbumsCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/PhotoPickerAlbumsCell.java @@ -15,6 +15,7 @@ import android.util.TypedValue; import android.view.Gravity; import android.view.MotionEvent; import android.view.View; +import android.widget.FrameLayout; import android.widget.LinearLayout; import android.widget.TextView; @@ -22,10 +23,9 @@ import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.MediaController; import org.telegram.messenger.R; import org.telegram.ui.Components.BackupImageView; -import org.telegram.ui.Components.FrameLayoutFixed; import org.telegram.ui.Components.LayoutHelper; -public class PhotoPickerAlbumsCell extends FrameLayoutFixed { +public class PhotoPickerAlbumsCell extends FrameLayout { public interface PhotoPickerAlbumsCellDelegate { void didSelectAlbum(MediaController.AlbumEntry albumEntry); @@ -36,7 +36,7 @@ public class PhotoPickerAlbumsCell extends FrameLayoutFixed { private int albumsCount; private PhotoPickerAlbumsCellDelegate delegate; - private class AlbumView extends FrameLayoutFixed { + private class AlbumView extends FrameLayout { private BackupImageView imageView; private TextView nameTextView; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/PhotoPickerPhotoCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/PhotoPickerPhotoCell.java index c4d28b1d4..01744fa9b 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/PhotoPickerPhotoCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/PhotoPickerPhotoCell.java @@ -8,15 +8,15 @@ package org.telegram.ui.Cells; +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; import android.content.Context; import android.view.Gravity; import android.widget.FrameLayout; import org.telegram.messenger.AndroidUtilities; -import org.telegram.messenger.AnimationCompat.AnimatorListenerAdapterProxy; -import org.telegram.messenger.AnimationCompat.AnimatorSetProxy; -import org.telegram.messenger.AnimationCompat.ObjectAnimatorProxy; -import org.telegram.messenger.AnimationCompat.ViewProxy; +import org.telegram.messenger.AnimatorListenerAdapterProxy; import org.telegram.messenger.R; import org.telegram.ui.Components.BackupImageView; import org.telegram.ui.Components.CheckBox; @@ -27,7 +27,7 @@ public class PhotoPickerPhotoCell extends FrameLayout { public BackupImageView photoImage; public FrameLayout checkFrame; public CheckBox checkBox; - private AnimatorSetProxy animator; + private AnimatorSet animator; public int itemWidth; public PhotoPickerPhotoCell(Context context) { @@ -62,13 +62,13 @@ public class PhotoPickerPhotoCell extends FrameLayout { if (checked) { setBackgroundColor(0xff0A0A0A); } - animator = new AnimatorSetProxy(); - animator.playTogether(ObjectAnimatorProxy.ofFloat(photoImage, "scaleX", checked ? 0.85f : 1.0f), - ObjectAnimatorProxy.ofFloat(photoImage, "scaleY", checked ? 0.85f : 1.0f)); + animator = new AnimatorSet(); + animator.playTogether(ObjectAnimator.ofFloat(photoImage, "scaleX", checked ? 0.85f : 1.0f), + ObjectAnimator.ofFloat(photoImage, "scaleY", checked ? 0.85f : 1.0f)); animator.setDuration(200); animator.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (animator != null && animator.equals(animation)) { animator = null; if (!checked) { @@ -78,7 +78,7 @@ public class PhotoPickerPhotoCell extends FrameLayout { } @Override - public void onAnimationCancel(Object animation) { + public void onAnimationCancel(Animator animation) { if (animator != null && animator.equals(animation)) { animator = null; } @@ -87,8 +87,8 @@ public class PhotoPickerPhotoCell extends FrameLayout { animator.start(); } else { setBackgroundColor(checked ? 0xff0A0A0A : 0); - ViewProxy.setScaleX(photoImage, checked ? 0.85f : 1.0f); - ViewProxy.setScaleY(photoImage, checked ? 0.85f : 1.0f); + photoImage.setScaleX(checked ? 0.85f : 1.0f); + photoImage.setScaleY(checked ? 0.85f : 1.0f); } } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/PhotoPickerSearchCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/PhotoPickerSearchCell.java index bcb61072b..3b1b58b37 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/PhotoPickerSearchCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/PhotoPickerSearchCell.java @@ -21,7 +21,6 @@ import android.widget.LinearLayout; import android.widget.TextView; import org.telegram.messenger.AndroidUtilities; -import org.telegram.messenger.AnimationCompat.ViewProxy; import org.telegram.messenger.LocaleController; import org.telegram.messenger.R; import org.telegram.ui.Components.LayoutHelper; @@ -136,7 +135,7 @@ public class PhotoPickerSearchCell extends LinearLayout { } }); } else { - ViewProxy.setAlpha(searchButton, 0.5f); + searchButton.setAlpha(0.5f); } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/ProfileSearchCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/ProfileSearchCell.java index 7fa7af144..aa518b5f6 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/ProfileSearchCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/ProfileSearchCell.java @@ -297,7 +297,7 @@ public class ProfileSearchCell extends BaseCell { } if (drawCount) { - TLRPC.Dialog dialog = MessagesController.getInstance().dialogs_dict.get(dialog_id); + TLRPC.TL_dialog dialog = MessagesController.getInstance().dialogs_dict.get(dialog_id); if (dialog != null && dialog.unread_count != 0) { lastUnreadCount = dialog.unread_count; String countString = String.format("%d", dialog.unread_count); @@ -454,7 +454,7 @@ public class ProfileSearchCell extends BaseCell { } } if (!continueUpdate && drawCount && (mask & MessagesController.UPDATE_MASK_READ_DIALOG_MESSAGE) != 0) { - TLRPC.Dialog dialog = MessagesController.getInstance().dialogs_dict.get(dialog_id); + TLRPC.TL_dialog dialog = MessagesController.getInstance().dialogs_dict.get(dialog_id); if (dialog != null && dialog.unread_count != lastUnreadCount) { continueUpdate = true; } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/RadioButtonCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/RadioButtonCell.java index 6a6199f01..3d3525923 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/RadioButtonCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/RadioButtonCell.java @@ -14,15 +14,15 @@ import android.graphics.Paint; import android.util.TypedValue; import android.view.Gravity; import android.view.View; +import android.widget.FrameLayout; import android.widget.TextView; import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.LocaleController; -import org.telegram.ui.Components.FrameLayoutFixed; import org.telegram.ui.Components.LayoutHelper; import org.telegram.ui.Components.RadioButton; -public class RadioButtonCell extends FrameLayoutFixed { +public class RadioButtonCell extends FrameLayout { private TextView textView; private TextView valueTextView; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/SharedPhotoVideoCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/SharedPhotoVideoCell.java index 333a86877..9ac990c88 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/SharedPhotoVideoCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/SharedPhotoVideoCell.java @@ -8,21 +8,22 @@ package org.telegram.ui.Cells; +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; import android.content.Context; import android.os.Build; import android.util.TypedValue; import android.view.Gravity; import android.view.MotionEvent; import android.view.View; +import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; import org.telegram.messenger.AndroidUtilities; -import org.telegram.messenger.AnimationCompat.AnimatorListenerAdapterProxy; -import org.telegram.messenger.AnimationCompat.AnimatorSetProxy; -import org.telegram.messenger.AnimationCompat.ObjectAnimatorProxy; -import org.telegram.messenger.AnimationCompat.ViewProxy; +import org.telegram.messenger.AnimatorListenerAdapterProxy; import org.telegram.messenger.MessageObject; import org.telegram.messenger.ApplicationLoader; import org.telegram.messenger.FileLoader; @@ -30,11 +31,10 @@ import org.telegram.messenger.R; import org.telegram.tgnet.TLRPC; import org.telegram.ui.Components.BackupImageView; import org.telegram.ui.Components.CheckBox; -import org.telegram.ui.Components.FrameLayoutFixed; import org.telegram.ui.Components.LayoutHelper; import org.telegram.ui.PhotoViewer; -public class SharedPhotoVideoCell extends FrameLayoutFixed { +public class SharedPhotoVideoCell extends FrameLayout { private PhotoVideoView[] photoVideoViews; private MessageObject[] messageObjects; @@ -48,20 +48,20 @@ public class SharedPhotoVideoCell extends FrameLayoutFixed { boolean didLongClickItem(SharedPhotoVideoCell cell, int index, MessageObject messageObject, int a); } - private class PhotoVideoView extends FrameLayoutFixed { + private class PhotoVideoView extends FrameLayout { private BackupImageView imageView; private TextView videoTextView; private LinearLayout videoInfoContainer; private View selector; private CheckBox checkBox; - private FrameLayoutFixed container; - private AnimatorSetProxy animator; + private FrameLayout container; + private AnimatorSet animator; public PhotoVideoView(Context context) { super(context); - container = new FrameLayoutFixed(context); + container = new FrameLayout(context); addView(container, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); imageView = new BackupImageView(context); @@ -116,13 +116,13 @@ public class SharedPhotoVideoCell extends FrameLayoutFixed { if (checked) { setBackgroundColor(0xfff5f5f5); } - animator = new AnimatorSetProxy(); - animator.playTogether(ObjectAnimatorProxy.ofFloat(container, "scaleX", checked ? 0.85f : 1.0f), - ObjectAnimatorProxy.ofFloat(container, "scaleY", checked ? 0.85f : 1.0f)); + animator = new AnimatorSet(); + animator.playTogether(ObjectAnimator.ofFloat(container, "scaleX", checked ? 0.85f : 1.0f), + ObjectAnimator.ofFloat(container, "scaleY", checked ? 0.85f : 1.0f)); animator.setDuration(200); animator.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (animator != null && animator.equals(animation)) { animator = null; if (!checked) { @@ -132,7 +132,7 @@ public class SharedPhotoVideoCell extends FrameLayoutFixed { } @Override - public void onAnimationCancel(Object animation) { + public void onAnimationCancel(Animator animation) { if (animator != null && animator.equals(animation)) { animator = null; } @@ -141,8 +141,8 @@ public class SharedPhotoVideoCell extends FrameLayoutFixed { animator.start(); } else { setBackgroundColor(checked ? 0xfff5f5f5 : 0); - ViewProxy.setScaleX(container, checked ? 0.85f : 1.0f); - ViewProxy.setScaleY(container, checked ? 0.85f : 1.0f); + container.setScaleX(checked ? 0.85f : 1.0f); + container.setScaleY(checked ? 0.85f : 1.0f); } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/StickerCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/StickerCell.java index 99261561a..fa44fa0b6 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/StickerCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/StickerCell.java @@ -10,19 +10,18 @@ package org.telegram.ui.Cells; import android.content.Context; import android.graphics.Canvas; -import android.os.Build; import android.view.Gravity; import android.view.View; import android.view.animation.AccelerateInterpolator; +import android.widget.FrameLayout; import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.R; import org.telegram.tgnet.TLRPC; import org.telegram.ui.Components.BackupImageView; -import org.telegram.ui.Components.FrameLayoutFixed; import org.telegram.ui.Components.LayoutHelper; -public class StickerCell extends FrameLayoutFixed { +public class StickerCell extends FrameLayout { private BackupImageView imageView; private TLRPC.Document sticker; @@ -109,10 +108,8 @@ public class StickerCell extends FrameLayoutFixed { scale = 1.0f; } } - if (Build.VERSION.SDK_INT >= 11) { - imageView.setScaleX(scale); - imageView.setScaleY(scale); - } + imageView.setScaleX(scale); + imageView.setScaleY(scale); imageView.invalidate(); invalidate(); } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/StickerEmojiCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/StickerEmojiCell.java index af2825e4a..c18629711 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/StickerEmojiCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/StickerEmojiCell.java @@ -10,7 +10,6 @@ package org.telegram.ui.Cells; import android.content.Context; import android.graphics.Canvas; -import android.os.Build; import android.util.TypedValue; import android.view.Gravity; import android.view.View; @@ -136,10 +135,8 @@ public class StickerEmojiCell extends FrameLayout { scale = 1.0f; } } - if (Build.VERSION.SDK_INT >= 11) { - imageView.setScaleX(scale); - imageView.setScaleY(scale); - } + imageView.setScaleX(scale); + imageView.setScaleY(scale); imageView.invalidate(); invalidate(); } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/StickerSetCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/StickerSetCell.java index d8dc1d564..99ff06caa 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/StickerSetCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/StickerSetCell.java @@ -22,7 +22,6 @@ import android.widget.ImageView; import android.widget.TextView; import org.telegram.messenger.AndroidUtilities; -import org.telegram.messenger.AnimationCompat.ViewProxy; import org.telegram.messenger.LocaleController; import org.telegram.messenger.R; import org.telegram.tgnet.TLRPC; @@ -94,13 +93,13 @@ public class StickerSetCell extends FrameLayout { textView.setText(stickersSet.set.title); if (stickersSet.set.disabled) { - ViewProxy.setAlpha(textView, 0.5f); - ViewProxy.setAlpha(valueTextView, 0.5f); - ViewProxy.setAlpha(imageView, 0.5f); + textView.setAlpha(0.5f); + valueTextView.setAlpha(0.5f); + imageView.setAlpha(0.5f); } else { - ViewProxy.setAlpha(textView, 1.0f); - ViewProxy.setAlpha(valueTextView, 1.0f); - ViewProxy.setAlpha(imageView, 1.0f); + textView.setAlpha(1.0f); + valueTextView.setAlpha(1.0f); + imageView.setAlpha(1.0f); } ArrayList documents = set.documents; if (documents != null && !documents.isEmpty()) { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/TextBlockCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/TextBlockCell.java index 37b6f197e..8a25f0efd 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/TextBlockCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/TextBlockCell.java @@ -13,13 +13,13 @@ import android.graphics.Canvas; import android.graphics.Paint; import android.util.TypedValue; import android.view.Gravity; +import android.widget.FrameLayout; import android.widget.TextView; import org.telegram.messenger.LocaleController; -import org.telegram.ui.Components.FrameLayoutFixed; import org.telegram.ui.Components.LayoutHelper; -public class TextBlockCell extends FrameLayoutFixed { +public class TextBlockCell extends FrameLayout { private TextView textView; private static Paint paint; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/TextCheckCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/TextCheckCell.java index 71af0bb96..7456eb30d 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/TextCheckCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/TextCheckCell.java @@ -11,19 +11,18 @@ package org.telegram.ui.Cells; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; -import android.os.Build; import android.text.TextUtils; import android.util.TypedValue; import android.view.Gravity; +import android.widget.FrameLayout; import android.widget.TextView; import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.LocaleController; -import org.telegram.ui.Components.FrameLayoutFixed; import org.telegram.ui.Components.LayoutHelper; import org.telegram.ui.Components.Switch; -public class TextCheckCell extends FrameLayoutFixed { +public class TextCheckCell extends FrameLayout { private TextView textView; private TextView valueTextView; @@ -81,10 +80,6 @@ public class TextCheckCell extends FrameLayoutFixed { public void setTextAndCheck(String text, boolean checked, boolean divider) { textView.setText(text); - if (Build.VERSION.SDK_INT < 11) { - checkBox.resetLayout(); - checkBox.requestLayout(); - } isMultiline = false; checkBox.setChecked(checked); needDivider = divider; @@ -99,10 +94,6 @@ public class TextCheckCell extends FrameLayoutFixed { public void setTextAndValueAndCheck(String text, String value, boolean checked, boolean multiline, boolean divider) { textView.setText(text); valueTextView.setText(value); - if (Build.VERSION.SDK_INT < 11) { - checkBox.resetLayout(); - checkBox.requestLayout(); - } checkBox.setChecked(checked); needDivider = divider; valueTextView.setVisibility(VISIBLE); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/TextDetailSettingsCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/TextDetailSettingsCell.java index 2bef5ad32..f1a8bd800 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Cells/TextDetailSettingsCell.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/TextDetailSettingsCell.java @@ -13,14 +13,14 @@ import android.graphics.Canvas; import android.graphics.Paint; import android.util.TypedValue; import android.view.Gravity; +import android.widget.FrameLayout; import android.widget.TextView; import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.LocaleController; -import org.telegram.ui.Components.FrameLayoutFixed; import org.telegram.ui.Components.LayoutHelper; -public class TextDetailSettingsCell extends FrameLayoutFixed { +public class TextDetailSettingsCell extends FrameLayout { private TextView textView; private TextView valueTextView; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ChangePhoneActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/ChangePhoneActivity.java index 4add7d3e1..4ab801a98 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ChangePhoneActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ChangePhoneActivity.java @@ -9,7 +9,9 @@ package org.telegram.ui; import android.Manifest; -import android.annotation.SuppressLint; +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; @@ -61,10 +63,7 @@ import org.telegram.messenger.UserConfig; import org.telegram.ui.ActionBar.ActionBar; import org.telegram.ui.ActionBar.ActionBarMenu; import org.telegram.ui.ActionBar.BaseFragment; -import org.telegram.messenger.AnimationCompat.AnimatorListenerAdapterProxy; -import org.telegram.messenger.AnimationCompat.AnimatorSetProxy; -import org.telegram.messenger.AnimationCompat.ObjectAnimatorProxy; -import org.telegram.messenger.AnimationCompat.ViewProxy; +import org.telegram.messenger.AnimatorListenerAdapterProxy; import org.telegram.ui.Components.HintEditText; import org.telegram.ui.Components.LayoutHelper; import org.telegram.ui.Components.SlideView; @@ -241,44 +240,34 @@ public class ChangePhoneActivity extends BaseFragment { } doneButton.setVisibility(View.VISIBLE); } - if(android.os.Build.VERSION.SDK_INT > 10) { - final SlideView outView = views[currentViewNum]; - final SlideView newView = views[page]; - currentViewNum = page; + final SlideView outView = views[currentViewNum]; + final SlideView newView = views[page]; + currentViewNum = page; - newView.setParams(params); - actionBar.setTitle(newView.getHeaderName()); - newView.onShow(); - ViewProxy.setX(newView, back ? -AndroidUtilities.displaySize.x : AndroidUtilities.displaySize.x); + newView.setParams(params); + actionBar.setTitle(newView.getHeaderName()); + newView.onShow(); + newView.setX(back ? -AndroidUtilities.displaySize.x : AndroidUtilities.displaySize.x); - AnimatorSetProxy animatorSet = new AnimatorSetProxy(); - animatorSet.setInterpolator(new AccelerateDecelerateInterpolator()); - animatorSet.setDuration(300); - animatorSet.playTogether( - ObjectAnimatorProxy.ofFloat(outView, "translationX", back ? AndroidUtilities.displaySize.x : -AndroidUtilities.displaySize.x), - ObjectAnimatorProxy.ofFloat(newView, "translationX", 0)); - animatorSet.addListener(new AnimatorListenerAdapterProxy() { - @Override - public void onAnimationStart(Object animation) { - newView.setVisibility(View.VISIBLE); - } + AnimatorSet animatorSet = new AnimatorSet(); + animatorSet.setInterpolator(new AccelerateDecelerateInterpolator()); + animatorSet.setDuration(300); + animatorSet.playTogether( + ObjectAnimator.ofFloat(outView, "translationX", back ? AndroidUtilities.displaySize.x : -AndroidUtilities.displaySize.x), + ObjectAnimator.ofFloat(newView, "translationX", 0)); + animatorSet.addListener(new AnimatorListenerAdapterProxy() { + @Override + public void onAnimationStart(Animator animation) { + newView.setVisibility(View.VISIBLE); + } - @SuppressLint("NewApi") - @Override - public void onAnimationEnd(Object animation) { - outView.setVisibility(View.GONE); - outView.setX(0); - } - }); - animatorSet.start(); - } else { - views[currentViewNum].setVisibility(View.GONE); - currentViewNum = page; - views[page].setParams(params); - views[page].setVisibility(View.VISIBLE); - actionBar.setTitle(views[page].getHeaderName()); - views[page].onShow(); - } + @Override + public void onAnimationEnd(Animator animation) { + outView.setVisibility(View.GONE); + outView.setX(0); + } + }); + animatorSet.start(); } private void fillNextCodeParams(Bundle params, TLRPC.TL_auth_sentCode res) { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ChannelCreateActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/ChannelCreateActivity.java index 13b2b705b..938228083 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ChannelCreateActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ChannelCreateActivity.java @@ -17,7 +17,6 @@ import android.content.Intent; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.drawable.BitmapDrawable; -import android.os.Build; import android.os.Bundle; import android.os.Vibrator; import android.text.Editable; @@ -75,7 +74,6 @@ import org.telegram.ui.Components.AvatarDrawable; import org.telegram.ui.Components.AvatarUpdater; import org.telegram.ui.Components.BackupImageView; import org.telegram.ui.Components.ChipSpan; -import org.telegram.ui.Components.FrameLayoutFixed; import org.telegram.ui.Components.LayoutHelper; import org.telegram.ui.Components.LetterSectionsListView; @@ -125,7 +123,6 @@ public class ChannelCreateActivity extends BaseFragment implements NotificationC private int currentStep; private int chatId; - private boolean allowComments = false; private boolean canCreatePublic = true; private TLRPC.InputFile uploadedAvatar; @@ -292,9 +289,6 @@ public class ChannelCreateActivity extends BaseFragment implements NotificationC } } } - if (allowComments) { - MessagesController.getInstance().toogleChannelComments(chatId, allowComments); - } Bundle args = new Bundle(); args.putInt("step", 2); args.putInt("chat_id", chatId); @@ -342,7 +336,7 @@ public class ChannelCreateActivity extends BaseFragment implements NotificationC if (currentStep == 0) { actionBar.setTitle(LocaleController.getString("NewChannel", R.string.NewChannel)); fragmentView.setBackgroundColor(0xffffffff); - FrameLayout frameLayout = new FrameLayoutFixed(context); + FrameLayout frameLayout = new FrameLayout(context); linearLayout.addView(frameLayout, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); avatarImage = new BackupImageView(context); @@ -469,23 +463,6 @@ public class ChannelCreateActivity extends BaseFragment implements NotificationC helpTextView.setGravity(LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT); helpTextView.setText(LocaleController.getString("DescriptionInfo", R.string.DescriptionInfo)); linearLayout.addView(helpTextView, LayoutHelper.createLinear(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT, 24, 10, 24, 20)); - - /*helpTextView = new TextView(context); - helpTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 15); - helpTextView.setTextColor(0xff3d93d5); - helpTextView.setGravity(LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT); - helpTextView.setText(LocaleController.getString("ChannelAlertTitle", R.string.ChannelAlertTitle)); - linearLayout.addView(helpTextView, LayoutHelper.createLinear(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT, 24, 14, 24, 20)); - helpTextView.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - AlertDialog.Builder builder = new AlertDialog.Builder(getParentActivity()); - builder.setTitle(LocaleController.getString("ChannelAlertTitle", R.string.ChannelAlertTitle)); - builder.setMessage(LocaleController.getString("ChannelAlertText", R.string.ChannelAlertText)); - builder.setPositiveButton(LocaleController.getString("Close", R.string.Close), null); - showDialog(builder.create()); - } - });*/ } else if (currentStep == 1) { actionBar.setTitle(LocaleController.getString("ChannelSettings", R.string.ChannelSettings)); fragmentView.setBackgroundColor(0xfff0f0f0); @@ -604,14 +581,9 @@ public class ChannelCreateActivity extends BaseFragment implements NotificationC return; } try { - if (Build.VERSION.SDK_INT < 11) { - android.text.ClipboardManager clipboard = (android.text.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); - clipboard.setText(invite.link); - } else { - android.content.ClipboardManager clipboard = (android.content.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); - android.content.ClipData clip = android.content.ClipData.newPlainText("label", invite.link); - clipboard.setPrimaryClip(clip); - } + android.content.ClipboardManager clipboard = (android.content.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); + android.content.ClipData clip = android.content.ClipData.newPlainText("label", invite.link); + clipboard.setPrimaryClip(clip); Toast.makeText(getParentActivity(), LocaleController.getString("LinkCopied", R.string.LinkCopied), Toast.LENGTH_SHORT).show(); } catch (Exception e) { FileLog.e("tmessages", e); @@ -626,30 +598,9 @@ public class ChannelCreateActivity extends BaseFragment implements NotificationC linkContainer.addView(checkTextView, LayoutHelper.createLinear(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT, 17, 3, 17, 7)); typeInfoCell = new TextInfoPrivacyCell(context); - //typeInfoCell.setBackgroundResource(R.drawable.greydivider); typeInfoCell.setBackgroundResource(R.drawable.greydivider_bottom); linearLayout.addView(typeInfoCell, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); - /*FrameLayout frameLayout = new FrameLayoutFixed(context); - frameLayout.setBackgroundColor(0xffffffff); - linearLayout.addView(frameLayout, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); - - TextCheckCell commentsCell = new TextCheckCell(context); - commentsCell.setTextAndCheck(LocaleController.getString("Comments", R.string.Comments), allowComments, false); - commentsCell.setBackgroundResource(R.drawable.list_selector); - frameLayout.addView(commentsCell, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); - commentsCell.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - allowComments = !allowComments; - ((TextCheckCell) v).setChecked(allowComments); - } - }); - - TextInfoPrivacyCell infoCell = new TextInfoPrivacyCell(context); - infoCell.setText(LocaleController.getString("CommentsInfo", R.string.CommentsInfo)); - infoCell.setBackgroundResource(R.drawable.greydivider_bottom); - linearLayout.addView(infoCell, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT));*/ updatePrivatePublic(); } else if (currentStep == 2) { actionBar.setTitle(LocaleController.getString("ChannelAddMembers", R.string.ChannelAddMembers)); @@ -677,9 +628,7 @@ public class ChannelCreateActivity extends BaseFragment implements NotificationC nameTextView.setHorizontalScrollBarEnabled(false); nameTextView.setPadding(0, 0, 0, 0); nameTextView.setHint(LocaleController.getString("AddMutual", R.string.AddMutual)); - if (Build.VERSION.SDK_INT >= 11) { - nameTextView.setTextIsSelectable(false); - } + nameTextView.setTextIsSelectable(false); nameTextView.setImeOptions(EditorInfo.IME_ACTION_DONE | EditorInfo.IME_FLAG_NO_EXTRACT_UI); nameTextView.setGravity((LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT) | Gravity.CENTER_VERTICAL); AndroidUtilities.clearCursorDrawable(nameTextView); @@ -739,9 +688,7 @@ public class ChannelCreateActivity extends BaseFragment implements NotificationC if (listView != null) { listView.setAdapter(searchListViewAdapter); searchListViewAdapter.notifyDataSetChanged(); - if (android.os.Build.VERSION.SDK_INT >= 11) { - listView.setFastScrollAlwaysVisible(false); - } + listView.setFastScrollAlwaysVisible(false); listView.setFastScrollEnabled(false); listView.setVerticalScrollBarEnabled(true); } @@ -755,9 +702,7 @@ public class ChannelCreateActivity extends BaseFragment implements NotificationC searchWas = false; listView.setAdapter(listViewAdapter); listViewAdapter.notifyDataSetChanged(); - if (android.os.Build.VERSION.SDK_INT >= 11) { - listView.setFastScrollAlwaysVisible(true); - } + listView.setFastScrollAlwaysVisible(true); listView.setFastScrollEnabled(true); listView.setVerticalScrollBarEnabled(false); emptyTextView.setText(LocaleController.getString("NoContacts", R.string.NoContacts)); @@ -796,10 +741,8 @@ public class ChannelCreateActivity extends BaseFragment implements NotificationC listView.setFastScrollEnabled(true); listView.setScrollBarStyle(View.SCROLLBARS_OUTSIDE_OVERLAY); listView.setAdapter(listViewAdapter); - if (Build.VERSION.SDK_INT >= 11) { - listView.setFastScrollAlwaysVisible(true); - listView.setVerticalScrollbarPosition(LocaleController.isRTL ? ListView.SCROLLBAR_POSITION_LEFT : ListView.SCROLLBAR_POSITION_RIGHT); - } + listView.setFastScrollAlwaysVisible(true); + listView.setVerticalScrollbarPosition(LocaleController.isRTL ? ListView.SCROLLBAR_POSITION_LEFT : ListView.SCROLLBAR_POSITION_RIGHT); linearLayout.addView(listView, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override @@ -863,9 +806,7 @@ public class ChannelCreateActivity extends BaseFragment implements NotificationC searchWas = false; listView.setAdapter(listViewAdapter); listViewAdapter.notifyDataSetChanged(); - if (android.os.Build.VERSION.SDK_INT >= 11) { - listView.setFastScrollAlwaysVisible(true); - } + listView.setFastScrollAlwaysVisible(true); listView.setFastScrollEnabled(true); listView.setVerticalScrollBarEnabled(false); emptyTextView.setText(LocaleController.getString("NoContacts", R.string.NoContacts)); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ChannelEditActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/ChannelEditActivity.java index 825fb18e1..47d402c4a 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ChannelEditActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ChannelEditActivity.java @@ -50,7 +50,6 @@ import org.telegram.ui.Cells.TextSettingsCell; import org.telegram.ui.Components.AvatarDrawable; import org.telegram.ui.Components.AvatarUpdater; import org.telegram.ui.Components.BackupImageView; -import org.telegram.ui.Components.FrameLayoutFixed; import org.telegram.ui.Components.LayoutHelper; import java.util.concurrent.Semaphore; @@ -71,7 +70,6 @@ public class ChannelEditActivity extends BaseFragment implements AvatarUpdater.A private TLRPC.Chat currentChat; private TLRPC.ChatFull info; private int chatId; - private boolean allowComments = true; private TLRPC.InputFile uploadedAvatar; private boolean signMessages; @@ -124,7 +122,6 @@ public class ChannelEditActivity extends BaseFragment implements AvatarUpdater.A } avatarUpdater.parentFragment = this; avatarUpdater.delegate = this; - allowComments = !currentChat.broadcast; signMessages = currentChat.signatures; NotificationCenter.getInstance().addObserver(this, NotificationCenter.chatInfoDidLoaded); NotificationCenter.getInstance().addObserver(this, NotificationCenter.updateInterfaces); @@ -194,10 +191,6 @@ public class ChannelEditActivity extends BaseFragment implements AvatarUpdater.A progressDialog.show(); return; } - boolean currentAllowComments = !currentChat.broadcast; - if (allowComments != currentAllowComments) { - MessagesController.getInstance().toogleChannelComments(chatId, allowComments); - } if (!currentChat.title.equals(nameTextView.getText().toString())) { MessagesController.getInstance().changeChatTitle(chatId, nameTextView.getText().toString()); } @@ -239,7 +232,7 @@ public class ChannelEditActivity extends BaseFragment implements AvatarUpdater.A linearLayout2.setBackgroundColor(0xffffffff); linearLayout.addView(linearLayout2, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); - FrameLayout frameLayout = new FrameLayoutFixed(context); + FrameLayout frameLayout = new FrameLayout(context); linearLayout2.addView(frameLayout, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); avatarImage = new BackupImageView(context); @@ -374,7 +367,7 @@ public class ChannelEditActivity extends BaseFragment implements AvatarUpdater.A linearLayout.addView(sectionCell, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); if (currentChat.megagroup || !currentChat.megagroup) { - frameLayout = new FrameLayoutFixed(context); + frameLayout = new FrameLayout(context); frameLayout.setBackgroundColor(0xffffffff); linearLayout.addView(frameLayout, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); @@ -382,13 +375,12 @@ public class ChannelEditActivity extends BaseFragment implements AvatarUpdater.A updateTypeCell(); typeCell.setBackgroundResource(R.drawable.list_selector); frameLayout.addView(typeCell, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); - //TODO lineView = new View(context); lineView.setBackgroundColor(0xffcfcfcf); linearLayout.addView(lineView, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 1)); - frameLayout = new FrameLayoutFixed(context); + frameLayout = new FrameLayout(context); frameLayout.setBackgroundColor(0xffffffff); linearLayout.addView(frameLayout, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); @@ -434,7 +426,7 @@ public class ChannelEditActivity extends BaseFragment implements AvatarUpdater.A } if (currentChat.creator) { - frameLayout = new FrameLayoutFixed(context); + frameLayout = new FrameLayout(context); frameLayout.setBackgroundColor(0xffffffff); linearLayout.addView(frameLayout, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); @@ -485,27 +477,6 @@ public class ChannelEditActivity extends BaseFragment implements AvatarUpdater.A linearLayout.addView(infoCell, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); } - /*frameLayout = new FrameLayoutFixed(context); - frameLayout.setBackgroundColor(0xffffffff); - linearLayout.addView(frameLayout, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); - - TextCheckCell commentsCell = new TextCheckCell(context); - commentsCell.setTextAndCheck(LocaleController.getString("Comments", R.string.Comments), allowComments, false); - commentsCell.setBackgroundResource(R.drawable.list_selector); - frameLayout.addView(commentsCell, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT)); - commentsCell.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - allowComments = !allowComments; - ((TextCheckCell) v).setChecked(allowComments); - } - }); - - infoCell = new TextInfoPrivacyCell(context); - infoCell.setText(LocaleController.getString("CommentsInfo", R.string.CommentsInfo)); - infoCell.setBackgroundResource(R.drawable.greydivider); - linearLayout.addView(infoCell, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT));*/ - nameTextView.setText(currentChat.title); nameTextView.setSelection(nameTextView.length()); if (info != null) { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ChannelEditTypeActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/ChannelEditTypeActivity.java index a389ec5cf..c317236ac 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ChannelEditTypeActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ChannelEditTypeActivity.java @@ -10,7 +10,6 @@ package org.telegram.ui; import android.app.AlertDialog; import android.content.Context; -import android.os.Build; import android.os.Bundle; import android.os.Vibrator; import android.text.Editable; @@ -298,14 +297,9 @@ public class ChannelEditTypeActivity extends BaseFragment implements Notificatio return; } try { - if (Build.VERSION.SDK_INT < 11) { - android.text.ClipboardManager clipboard = (android.text.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); - clipboard.setText(invite.link); - } else { - android.content.ClipboardManager clipboard = (android.content.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); - android.content.ClipData clip = android.content.ClipData.newPlainText("label", invite.link); - clipboard.setPrimaryClip(clip); - } + android.content.ClipboardManager clipboard = (android.content.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); + android.content.ClipData clip = android.content.ClipData.newPlainText("label", invite.link); + clipboard.setPrimaryClip(clip); Toast.makeText(getParentActivity(), LocaleController.getString("LinkCopied", R.string.LinkCopied), Toast.LENGTH_SHORT).show(); } catch (Exception e) { FileLog.e("tmessages", e); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ChannelUsersActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/ChannelUsersActivity.java index 78c2ff5df..958a1e7ad 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ChannelUsersActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ChannelUsersActivity.java @@ -11,7 +11,6 @@ package org.telegram.ui; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; -import android.os.Build; import android.os.Bundle; import android.view.View; import android.view.ViewGroup; @@ -143,9 +142,7 @@ public class ChannelUsersActivity extends BaseFragment implements NotificationCe listView.setDividerHeight(0); listView.setDrawSelectorOnTop(true); listView.setAdapter(listViewAdapter = new ListAdapter(context)); - if (Build.VERSION.SDK_INT >= 11) { - listView.setVerticalScrollbarPosition(LocaleController.isRTL ? ListView.SCROLLBAR_POSITION_LEFT : ListView.SCROLLBAR_POSITION_RIGHT); - } + listView.setVerticalScrollbarPosition(LocaleController.isRTL ? ListView.SCROLLBAR_POSITION_LEFT : ListView.SCROLLBAR_POSITION_RIGHT); frameLayout.addView(listView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ChatActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/ChatActivity.java index eb0b0fa07..86aeb29c2 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ChatActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ChatActivity.java @@ -9,6 +9,9 @@ package org.telegram.ui; import android.Manifest; +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; @@ -31,11 +34,11 @@ import android.provider.ContactsContract; import android.provider.MediaStore; import android.text.Spannable; import android.text.SpannableString; +import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.TextUtils; import android.text.style.ClickableSpan; import android.text.style.URLSpan; -import android.util.Base64; import android.util.SparseArray; import android.util.SparseIntArray; import android.util.TypedValue; @@ -45,7 +48,6 @@ import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.view.WindowManager; -import android.webkit.MimeTypeMap; import android.widget.EditText; import android.widget.FrameLayout; import android.widget.ImageView; @@ -70,6 +72,7 @@ import org.telegram.messenger.Utilities; import org.telegram.messenger.VideoEditedInfo; import org.telegram.messenger.browser.Browser; import org.telegram.messenger.query.BotQuery; +import org.telegram.messenger.query.DraftQuery; import org.telegram.messenger.query.MessagesSearchQuery; import org.telegram.messenger.query.MessagesQuery; import org.telegram.messenger.query.SearchQuery; @@ -81,7 +84,6 @@ import org.telegram.messenger.ApplicationLoader; import org.telegram.messenger.FileLoader; import org.telegram.tgnet.ConnectionsManager; import org.telegram.tgnet.RequestDelegate; -import org.telegram.tgnet.SerializedData; import org.telegram.tgnet.TLObject; import org.telegram.tgnet.TLRPC; import org.telegram.messenger.ContactsController; @@ -97,10 +99,7 @@ import org.telegram.ui.ActionBar.BottomSheet; import org.telegram.ui.ActionBar.SimpleTextView; import org.telegram.ui.Adapters.MentionsAdapter; import org.telegram.ui.Adapters.StickersAdapter; -import org.telegram.messenger.AnimationCompat.AnimatorListenerAdapterProxy; -import org.telegram.messenger.AnimationCompat.AnimatorSetProxy; -import org.telegram.messenger.AnimationCompat.ObjectAnimatorProxy; -import org.telegram.messenger.AnimationCompat.ViewProxy; +import org.telegram.messenger.AnimatorListenerAdapterProxy; import org.telegram.ui.Cells.ChatActionCell; import org.telegram.ui.Cells.ChatLoadingCell; import org.telegram.ui.ActionBar.ActionBar; @@ -120,7 +119,6 @@ import org.telegram.ui.Components.ChatAvatarContainer; import org.telegram.ui.Components.ContextProgressView; import org.telegram.ui.Components.ExtendedGridLayoutManager; import org.telegram.ui.Components.PlayerView; -import org.telegram.ui.Components.FrameLayoutFixed; import org.telegram.ui.Components.LayoutHelper; import org.telegram.ui.Components.NumberTextView; import org.telegram.ui.Components.RecyclerListView; @@ -163,11 +161,9 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not private ActionBarMenuItem attachItem; private ActionBarMenuItem headerItem; private ActionBarMenuItem searchItem; - private ActionBarMenuItem searchUpItem; - private ActionBarMenuItem searchDownItem; private ActionBarMenuItem editDoneItem; private ContextProgressView editDoneItemProgress; - private AnimatorSetProxy editDoneItemAnimation; + private AnimatorSet editDoneItemAnimation; private TextView addContactItem; private RecyclerListView chatListView; private LinearLayoutManager chatLayoutManager; @@ -189,7 +185,9 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not private StickersAdapter stickersAdapter; private FrameLayout stickersPanel; private TextView muteItem; - private ImageView pagedownButton; + private FrameLayout pagedownButton; + private boolean pagedownButtonShowedByScroll; + private TextView pagedownButtonCounter; private BackupImageView replyImageView; private SimpleTextView replyNameTextView; private SimpleTextView replyObjectTextView; @@ -199,10 +197,10 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not private RecyclerListView mentionListView; private LinearLayoutManager mentionLayoutManager; private ExtendedGridLayoutManager mentionGridLayoutManager; - private AnimatorSetProxy mentionListAnimation; + private AnimatorSet mentionListAnimation; private ChatAttachAlert chatAttachAlert; private LinearLayout reportSpamView; - private AnimatorSetProxy reportSpamViewAnimator; + private AnimatorSet reportSpamViewAnimator; private TextView addToContactsButton; private TextView reportSpamButton; private FrameLayout reportSpamContainer; @@ -210,14 +208,18 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not private TextView gifHintTextView; private View emojiButtonRed; private FrameLayout pinnedMessageView; - private AnimatorSetProxy pinnedMessageViewAnimator; + private AnimatorSet pinnedMessageViewAnimator; private SimpleTextView pinnedMessageNameTextView; private SimpleTextView pinnedMessageTextView; private FrameLayout alertView; private Runnable hideAlertViewRunnable; private TextView alertNameTextView; private TextView alertTextView; - private AnimatorSetProxy alertViewAnimator; + private AnimatorSet alertViewAnimator; + private FrameLayout searchContainer; + private ImageView searchUpButton; + private ImageView searchDownButton; + private SimpleTextView searchCountText; private boolean mentionListViewIgnoreLayout; private int mentionListViewScrollOffsetY; @@ -228,20 +230,17 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not private MessageObject pinnedMessageObject; private int loadingPinnedMessage; - private ObjectAnimatorProxy pagedownButtonAnimation; - private AnimatorSetProxy replyButtonAnimation; + private ObjectAnimator pagedownButtonAnimation; + private AnimatorSet replyButtonAnimation; private boolean openSearchKeyboard; - private int channelMessagesImportant; - private boolean waitingForImportantLoad; - private boolean waitingForReplyMessageLoad; private boolean allowStickersPanel; private boolean allowContextBotPanel; private boolean allowContextBotPanelSecond = true; - private AnimatorSetProxy runningAnimation; + private AnimatorSet runningAnimation; private MessageObject selectedObject; private ArrayList forwardingMessages; @@ -274,6 +273,8 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not private int cantDeleteMessagesCount; private ArrayList waitingForLoad = new ArrayList<>(); + private int newUnreadMessageCount; + private HashMap[] messagesDict = new HashMap[]{new HashMap<>(), new HashMap<>()}; private HashMap> messagesByDays = new HashMap<>(); protected ArrayList messages = new ArrayList<>(); @@ -306,8 +307,6 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not private String currentPicturePath; - private Rect scrollRect = new Rect(); - protected TLRPC.ChatFull info = null; private HashMap botInfo = new HashMap<>(); @@ -351,10 +350,6 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not private final static int attach_location = 6; private final static int search = 40; - private final static int search_up = 41; - private final static int search_down = 42; - - private final static int open_channel_profile = 50; private final static int id_chat_compose_panel = 1000; @@ -424,12 +419,6 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not dialog_id = AndroidUtilities.makeBroadcastId(chatId); } if (ChatObject.isChannel(currentChat)) { - if (!currentChat.megagroup) { - SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE); - channelMessagesImportant = preferences.getInt("important_" + dialog_id, 2); - } else { - channelMessagesImportant = 1; - } MessagesController.getInstance().startShortPoll(chatId, false); } } else if (userId != 0) { @@ -547,6 +536,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not NotificationCenter.getInstance().addObserver(this, NotificationCenter.chatInfoCantLoad); NotificationCenter.getInstance().addObserver(this, NotificationCenter.didLoadedPinnedMessage); NotificationCenter.getInstance().addObserver(this, NotificationCenter.peerSettingsDidLoaded); + NotificationCenter.getInstance().addObserver(this, NotificationCenter.newDraftReceived); super.onFragmentCreate(); @@ -562,13 +552,13 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not waitingForLoad.add(lastLoadIndex); if (migrated_to != 0) { mergeDialogId = migrated_to; - MessagesController.getInstance().loadMessages(mergeDialogId, AndroidUtilities.isTablet() ? 30 : 20, startLoadFromMessageId, true, 0, classGuid, 3, 0, 0, lastLoadIndex++); + MessagesController.getInstance().loadMessages(mergeDialogId, AndroidUtilities.isTablet() ? 30 : 20, startLoadFromMessageId, true, 0, classGuid, 3, 0, ChatObject.isChannel(currentChat), lastLoadIndex++); } else { - MessagesController.getInstance().loadMessages(dialog_id, AndroidUtilities.isTablet() ? 30 : 20, startLoadFromMessageId, true, 0, classGuid, 3, 0, channelMessagesImportant, lastLoadIndex++); + MessagesController.getInstance().loadMessages(dialog_id, AndroidUtilities.isTablet() ? 30 : 20, startLoadFromMessageId, true, 0, classGuid, 3, 0, ChatObject.isChannel(currentChat), lastLoadIndex++); } } else { waitingForLoad.add(lastLoadIndex); - MessagesController.getInstance().loadMessages(dialog_id, AndroidUtilities.isTablet() ? 30 : 20, 0, true, 0, classGuid, 2, 0, channelMessagesImportant, lastLoadIndex++); + MessagesController.getInstance().loadMessages(dialog_id, AndroidUtilities.isTablet() ? 30 : 20, 0, true, 0, classGuid, 2, 0, ChatObject.isChannel(currentChat), lastLoadIndex++); } if (currentChat != null) { @@ -660,6 +650,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not NotificationCenter.getInstance().removeObserver(this, NotificationCenter.chatInfoCantLoad); NotificationCenter.getInstance().removeObserver(this, NotificationCenter.didLoadedPinnedMessage); NotificationCenter.getInstance().removeObserver(this, NotificationCenter.peerSettingsDidLoaded); + NotificationCenter.getInstance().removeObserver(this, NotificationCenter.newDraftReceived); if (AndroidUtilities.isTablet()) { NotificationCenter.getInstance().postNotificationName(NotificationCenter.openedChatChanged, dialog_id, true); @@ -873,35 +864,17 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not chatAttachAlert.init(ChatActivity.this); showDialog(chatAttachAlert); } else if (id == bot_help) { - SendMessagesHelper.getInstance().sendMessage("/help", dialog_id, null, null, false, chatActivityEnterView == null || chatActivityEnterView.asAdmin(), null, null, null); + SendMessagesHelper.getInstance().sendMessage("/help", dialog_id, null, null, false, null, null, null); } else if (id == bot_settings) { - SendMessagesHelper.getInstance().sendMessage("/settings", dialog_id, null, null, false, chatActivityEnterView == null || chatActivityEnterView.asAdmin(), null, null, null); + SendMessagesHelper.getInstance().sendMessage("/settings", dialog_id, null, null, false, null, null, null); } else if (id == search) { openSearchWithText(null); - } else if (id == search_up) { - MessagesSearchQuery.searchMessagesInChat(null, dialog_id, mergeDialogId, classGuid, 1); - } else if (id == search_down) { - MessagesSearchQuery.searchMessagesInChat(null, dialog_id, mergeDialogId, classGuid, 2); - } else if (id == open_channel_profile) { - Bundle args = new Bundle(); - args.putInt("chat_id", currentChat.id); - ProfileActivity fragment = new ProfileActivity(args); - fragment.setChatInfo(info); - fragment.setPlayProfileAnimation(true); - presentFragment(fragment); } } }); - avatarContainer = new ChatAvatarContainer(context, this, ChatObject.isChannel(currentChat) && !currentChat.megagroup && !(currentChat instanceof TLRPC.TL_channelForbidden), currentEncryptedChat != null); - avatarContainer.setRadioChecked(channelMessagesImportant == 1, false); + avatarContainer = new ChatAvatarContainer(context, this, currentEncryptedChat != null); actionBar.addView(avatarContainer, 0, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.MATCH_PARENT, Gravity.TOP | Gravity.LEFT, 56, 0, 40, 0)); - avatarContainer.setDelegate(new ChatAvatarContainer.ChatAvatarContainerDelegate() { - @Override - public void didPressedRadioButton() { - switchImportantMode(null); - } - }); if (currentChat != null) { if (!ChatObject.isChannel(currentChat)) { @@ -918,7 +891,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not ActionBarMenu menu = actionBar.createMenu(); if (currentEncryptedChat == null && !isBroadcast) { - searchItem = menu.addItem(0, R.drawable.ic_ab_search).setIsSearchField(true, false).setActionBarMenuItemSearchListener(new ActionBarMenuItem.ActionBarMenuItemSearchListener() { + searchItem = menu.addItem(0, R.drawable.ic_ab_search).setIsSearchField(true).setActionBarMenuItemSearchListener(new ActionBarMenuItem.ActionBarMenuItemSearchListener() { @Override public void onSearchCollapse() { @@ -939,14 +912,10 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } } searchItem.setVisibility(View.GONE); - //chatActivityEnterView.setVisibility(View.VISIBLE); - searchUpItem.clearAnimation(); - searchDownItem.clearAnimation(); - searchUpItem.setVisibility(View.GONE); - searchDownItem.setVisibility(View.GONE); highlightMessageId = Integer.MAX_VALUE; updateVisibleRows(); scrollToLastMessage(false); + updateBottomOverlay(); } @Override @@ -965,23 +934,15 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not @Override public void onSearchPressed(EditText editText) { - updateSearchButtons(0); + updateSearchButtons(0, 0, 0); MessagesSearchQuery.searchMessagesInChat(editText.getText().toString(), dialog_id, mergeDialogId, classGuid, 0); } }); searchItem.getSearchField().setHint(LocaleController.getString("Search", R.string.Search)); searchItem.setVisibility(View.GONE); - - searchUpItem = menu.addItem(search_up, R.drawable.search_up); - searchUpItem.setVisibility(View.GONE); - searchDownItem = menu.addItem(search_down, R.drawable.search_down); - searchDownItem.setVisibility(View.GONE); } headerItem = menu.addItem(0, R.drawable.ic_ab_other); - if (channelMessagesImportant != 0 && !currentChat.megagroup) { - headerItem.addSubItem(open_channel_profile, LocaleController.getString("OpenChannelProfile", R.string.OpenChannelProfile), 0); - } if (searchItem != null) { headerItem.addSubItem(search, LocaleController.getString("Search", R.string.Search), 0); } @@ -994,7 +955,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not if (currentEncryptedChat != null) { timeItem2 = headerItem.addSubItem(chat_enc_timer, LocaleController.getString("SetTimer", R.string.SetTimer), 0); } - if (channelMessagesImportant == 0) { + if (!ChatObject.isChannel(currentChat)) { headerItem.addSubItem(clear_history, LocaleController.getString("ClearHistory", R.string.ClearHistory), 0); if (currentChat != null && !isBroadcast) { headerItem.addSubItem(delete_chat, LocaleController.getString("DeleteAndExit", R.string.DeleteAndExit), 0); @@ -1141,59 +1102,55 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not if (child == null || child.getVisibility() == GONE || child == chatActivityEnterView) { continue; } - try { - if (child == chatListView || child == progressView) { - int contentWidthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY); - int contentHeightSpec = MeasureSpec.makeMeasureSpec(Math.max(AndroidUtilities.dp(10), heightSize - inputFieldHeight + AndroidUtilities.dp(2)), MeasureSpec.EXACTLY); - child.measure(contentWidthSpec, contentHeightSpec); - } else if (child == emptyViewContainer) { - int contentWidthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY); - int contentHeightSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY); - child.measure(contentWidthSpec, contentHeightSpec); - } else if (chatActivityEnterView.isPopupView(child)) { - child.measure(MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(child.getLayoutParams().height, MeasureSpec.EXACTLY)); - } else if (child == mentionContainer) { - FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) mentionContainer.getLayoutParams(); - int height; - mentionListViewIgnoreLayout = true; + if (child == chatListView || child == progressView) { + int contentWidthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY); + int contentHeightSpec = MeasureSpec.makeMeasureSpec(Math.max(AndroidUtilities.dp(10), heightSize - inputFieldHeight + AndroidUtilities.dp(2 + (chatActivityEnterView.isTopViewVisible() ? 48 : 0))), MeasureSpec.EXACTLY); + child.measure(contentWidthSpec, contentHeightSpec); + } else if (child == emptyViewContainer) { + int contentWidthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY); + int contentHeightSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY); + child.measure(contentWidthSpec, contentHeightSpec); + } else if (chatActivityEnterView.isPopupView(child)) { + child.measure(MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(child.getLayoutParams().height, MeasureSpec.EXACTLY)); + } else if (child == mentionContainer) { + FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) mentionContainer.getLayoutParams(); + int height; + mentionListViewIgnoreLayout = true; - if (mentionsAdapter.isBotContext() && mentionsAdapter.isMediaLayout()) { - int size = mentionGridLayoutManager.getRowsCount(widthSize); - int maxHeight = size * 102; - if (mentionsAdapter.isBotContext()) { - if (mentionsAdapter.getBotContextSwitch() != null) { - maxHeight += 34; - } + if (mentionsAdapter.isBotContext() && mentionsAdapter.isMediaLayout()) { + int size = mentionGridLayoutManager.getRowsCount(widthSize); + int maxHeight = size * 102; + if (mentionsAdapter.isBotContext()) { + if (mentionsAdapter.getBotContextSwitch() != null) { + maxHeight += 34; } - height = heightSize - chatActivityEnterView.getMeasuredHeight() + (maxHeight != 0 ? AndroidUtilities.dp(2) : 0); - mentionListView.setPadding(0, Math.max(0, height - AndroidUtilities.dp(Math.min(maxHeight, 68 * 1.8f))), 0, 0); - } else { - int size = mentionsAdapter.getItemCount(); - int maxHeight = 0; - if (mentionsAdapter.isBotContext()) { - if (mentionsAdapter.getBotContextSwitch() != null) { - maxHeight += 36; - size -= 1; - } - maxHeight += size * 68; - } else { - maxHeight += size * 36; - } - height = heightSize - chatActivityEnterView.getMeasuredHeight() + (maxHeight != 0 ? AndroidUtilities.dp(2) : 0); - mentionListView.setPadding(0, Math.max(0, height - AndroidUtilities.dp(Math.min(maxHeight, 68 * 1.8f))), 0, 0); } - - layoutParams.height = height; - layoutParams.topMargin = 0; - - mentionListViewIgnoreLayout = false; - child.measure(MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(layoutParams.height, MeasureSpec.EXACTLY)); + height = heightSize - chatActivityEnterView.getMeasuredHeight() + (maxHeight != 0 ? AndroidUtilities.dp(2) : 0); + mentionListView.setPadding(0, Math.max(0, height - AndroidUtilities.dp(Math.min(maxHeight, 68 * 1.8f))), 0, 0); } else { - measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0); + int size = mentionsAdapter.getItemCount(); + int maxHeight = 0; + if (mentionsAdapter.isBotContext()) { + if (mentionsAdapter.getBotContextSwitch() != null) { + maxHeight += 36; + size -= 1; + } + maxHeight += size * 68; + } else { + maxHeight += size * 36; + } + height = heightSize - chatActivityEnterView.getMeasuredHeight() + (maxHeight != 0 ? AndroidUtilities.dp(2) : 0); + mentionListView.setPadding(0, Math.max(0, height - AndroidUtilities.dp(Math.min(maxHeight, 68 * 1.8f))), 0, 0); } - } catch (Exception e) { - FileLog.e("tmessages", e); + + layoutParams.height = height; + layoutParams.topMargin = 0; + + mentionListViewIgnoreLayout = false; + child.measure(MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(layoutParams.height, MeasureSpec.EXACTLY)); + } else { + measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0); } } } @@ -1262,6 +1219,10 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not childTop = chatActivityEnterView.getBottom(); } else if (child == gifHintTextView) { childTop -= inputFieldHeight; + } else if (child == chatListView || child == progressView) { + if (chatActivityEnterView.isTopViewVisible()) { + childTop -= AndroidUtilities.dp(48); + } } child.layout(childLeft, childTop, childLeft + width, childTop + height); } @@ -1395,6 +1356,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } } }; + chatListView.setTag(1); chatListView.setVerticalScrollBarEnabled(true); chatListView.setAdapter(chatAdapter = new ChatActivityAdapter(context)); chatListView.setClipToPadding(false); @@ -1415,6 +1377,9 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not chatListView.setOnItemClickListener(onItemClickListener); chatListView.setOnScrollListener(new RecyclerView.OnScrollListener() { + private float totalDy = 0; + private final int scrollValue = AndroidUtilities.dp(100); + @Override public void onScrollStateChanged(RecyclerView recyclerView, int newState) { if (newState == RecyclerView.SCROLL_STATE_DRAGGING && highlightMessageId != Integer.MAX_VALUE) { @@ -1432,6 +1397,25 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not int totalItemCount = chatAdapter.getItemCount(); if (firstVisibleItem + visibleItemCount == totalItemCount && forwardEndReached[0]) { showPagedownButton(false, true); + } else { + if (dy > 0) { + if (pagedownButton.getTag() == null) { + totalDy += dy; + if (totalDy > scrollValue) { + totalDy = 0; + showPagedownButton(true, true); + pagedownButtonShowedByScroll = true; + } + } + } else { + if (pagedownButtonShowedByScroll && pagedownButton.getTag() != null) { + totalDy += dy; + if (totalDy < -scrollValue) { + showPagedownButton(false, true); + totalDy = 0; + } + } + } } } updateMessagesVisisblePart(); @@ -1568,10 +1552,9 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not progressView.addView(progressBar, LayoutHelper.createFrame(32, 32, Gravity.CENTER)); if (ChatObject.isChannel(currentChat)) { - pinnedMessageView = new FrameLayoutFixed(context); + pinnedMessageView = new FrameLayout(context); pinnedMessageView.setTag(1); - ViewProxy.setTranslationY(pinnedMessageView, -AndroidUtilities.dp(50)); - pinnedMessageView.clearAnimation(); + pinnedMessageView.setTranslationY(-AndroidUtilities.dp(50)); pinnedMessageView.setVisibility(View.GONE); pinnedMessageView.setBackgroundResource(R.drawable.blockpanel); contentView.addView(pinnedMessageView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 50, Gravity.TOP | Gravity.LEFT)); @@ -1630,8 +1613,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not reportSpamView = new LinearLayout(context); reportSpamView.setTag(1); - ViewProxy.setTranslationY(reportSpamView, -AndroidUtilities.dp(50)); - reportSpamView.clearAnimation(); + reportSpamView.setTranslationY(-AndroidUtilities.dp(50)); reportSpamView.setVisibility(View.GONE); reportSpamView.setBackgroundResource(R.drawable.blockpanel); contentView.addView(reportSpamView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 50, Gravity.TOP | Gravity.LEFT)); @@ -1726,10 +1708,9 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } }); - alertView = new FrameLayoutFixed(context); + alertView = new FrameLayout(context); alertView.setTag(1); - ViewProxy.setTranslationY(alertView, -AndroidUtilities.dp(50)); - alertView.clearAnimation(); + alertView.setTranslationY(-AndroidUtilities.dp(50)); alertView.setVisibility(View.GONE); alertView.setBackgroundResource(R.drawable.blockpanel); contentView.addView(alertView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 50, Gravity.TOP | Gravity.LEFT)); @@ -1752,7 +1733,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not alertView.addView(alertTextView, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.TOP | Gravity.LEFT, 8, 23, 8, 0)); if (!isBroadcast) { - mentionContainer = new FrameLayoutFixed(context) { + mentionContainer = new FrameLayout(context) { private Drawable background; @@ -1854,6 +1835,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not return StickerPreviewViewer.getInstance().onTouch(event, mentionListView, 0, mentionsOnItemClickListener); } }); + mentionListView.setTag(2); mentionLayoutManager = new LinearLayoutManager(context) { @Override public boolean supportsPredictiveItemAnimations() { @@ -1960,7 +1942,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } if (mentionContainer.getVisibility() == View.VISIBLE) { - ViewProxy.setAlpha(mentionContainer, 1.0f); + mentionContainer.setAlpha(1.0f); return; } if (mentionsAdapter.isBotContext() && mentionsAdapter.isMediaLayout()) { @@ -1982,21 +1964,20 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } mentionContainer.setVisibility(View.VISIBLE); mentionContainer.setTag(null); - mentionListAnimation = new AnimatorSetProxy(); + mentionListAnimation = new AnimatorSet(); mentionListAnimation.playTogether( - ObjectAnimatorProxy.ofFloat(mentionContainer, "alpha", 0.0f, 1.0f) + ObjectAnimator.ofFloat(mentionContainer, "alpha", 0.0f, 1.0f) ); mentionListAnimation.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (mentionListAnimation != null && mentionListAnimation.equals(animation)) { - mentionContainer.clearAnimation(); mentionListAnimation = null; } } @Override - public void onAnimationCancel(Object animation) { + public void onAnimationCancel(Animator animation) { if (mentionListAnimation != null && mentionListAnimation.equals(animation)) { mentionListAnimation = null; } @@ -2005,8 +1986,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not mentionListAnimation.setDuration(200); mentionListAnimation.start(); } else { - ViewProxy.setAlpha(mentionContainer, 1.0f); - mentionContainer.clearAnimation(); + mentionContainer.setAlpha(1.0f); mentionContainer.setVisibility(View.INVISIBLE); } } else { @@ -2019,15 +1999,14 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not return; } if (allowStickersPanel) { - mentionListAnimation = new AnimatorSetProxy(); + mentionListAnimation = new AnimatorSet(); mentionListAnimation.playTogether( - ObjectAnimatorProxy.ofFloat(mentionContainer, "alpha", 0.0f) + ObjectAnimator.ofFloat(mentionContainer, "alpha", 0.0f) ); mentionListAnimation.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (mentionListAnimation != null && mentionListAnimation.equals(animation)) { - mentionContainer.clearAnimation(); mentionContainer.setVisibility(View.GONE); mentionContainer.setTag(null); mentionListAnimation = null; @@ -2035,7 +2014,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } @Override - public void onAnimationCancel(Object animation) { + public void onAnimationCancel(Animator animation) { if (mentionListAnimation != null && mentionListAnimation.equals(animation)) { mentionListAnimation = null; } @@ -2045,7 +2024,6 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not mentionListAnimation.start(); } else { mentionContainer.setTag(null); - mentionContainer.clearAnimation(); mentionContainer.setVisibility(View.GONE); } } @@ -2105,14 +2083,13 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } } else if (object instanceof String) { if (mentionsAdapter.isBotCommands()) { - SendMessagesHelper.getInstance().sendMessage((String) object, dialog_id, null, null, false, chatActivityEnterView == null || chatActivityEnterView.asAdmin(), null, null, null); + SendMessagesHelper.getInstance().sendMessage((String) object, dialog_id, null, null, false, null, null, null); chatActivityEnterView.setFieldText(""); } else { chatActivityEnterView.replaceWithText(start, len, object + " "); } } else if (object instanceof TLRPC.BotInlineResult) { - String text = chatActivityEnterView.getFieldText(); - if (text == null) { + if (chatActivityEnterView.getFieldText() == null) { return; } int uid = mentionsAdapter.getContextBotId(); @@ -2122,7 +2099,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not params.put("query_id", "" + result.query_id); params.put("bot", "" + uid); params.put("bot_name", mentionsAdapter.getContextBotName()); - SendMessagesHelper.prepareSendingBotContextResult(result, params, dialog_id, replyingMessageObject, chatActivityEnterView == null || chatActivityEnterView.asAdmin()); + SendMessagesHelper.prepareSendingBotContextResult(result, params, dialog_id, replyingMessageObject); chatActivityEnterView.setFieldText(""); showReplyPanel(false, null, null, null, false, true); SearchQuery.increaseInlineRaiting(uid); @@ -2135,7 +2112,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not mentionListView.setOnItemLongClickListener(new RecyclerListView.OnItemLongClickListener() { @Override public boolean onItemClick(View view, int position) { - if (!mentionsAdapter.isLongClickEnabled()) { + if (getParentActivity() == null || !mentionsAdapter.isLongClickEnabled()) { return false; } Object object = mentionsAdapter.getItem(position); @@ -2190,10 +2167,9 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not }); } - pagedownButton = new ImageView(context); + pagedownButton = new FrameLayout(context); pagedownButton.setVisibility(View.INVISIBLE); - pagedownButton.setImageResource(R.drawable.pagedown); - contentView.addView(pagedownButton, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.RIGHT | Gravity.BOTTOM, 0, 0, 6, 4)); + contentView.addView(pagedownButton, LayoutHelper.createFrame(46, 59, Gravity.RIGHT | Gravity.BOTTOM, 0, 0, 7, 5)); pagedownButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { @@ -2205,6 +2181,21 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } }); + ImageView pagedownButtonImage = new ImageView(context); + pagedownButtonImage.setImageResource(R.drawable.pagedown); + pagedownButton.addView(pagedownButtonImage, LayoutHelper.createFrame(46, 46, Gravity.LEFT | Gravity.BOTTOM)); + + pagedownButtonCounter = new TextView(context); + pagedownButtonCounter.setVisibility(View.INVISIBLE); + pagedownButtonCounter.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); + pagedownButtonCounter.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 13); + pagedownButtonCounter.setTextColor(0xffffffff); + pagedownButtonCounter.setGravity(Gravity.CENTER); + pagedownButtonCounter.setBackgroundResource(R.drawable.chat_badge); + pagedownButtonCounter.setMinWidth(AndroidUtilities.dp(23)); + pagedownButtonCounter.setPadding(AndroidUtilities.dp(8), 0, AndroidUtilities.dp(8), AndroidUtilities.dp(1)); + pagedownButton.addView(pagedownButtonCounter, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, 23, Gravity.TOP | Gravity.CENTER_HORIZONTAL)); + chatActivityEnterView = new ChatActivityEnterView(getParentActivity(), contentView, this, true); chatActivityEnterView.setDialogId(dialog_id); chatActivityEnterView.addToAttachLayout(menuItem); @@ -2313,21 +2304,17 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not if (size < AndroidUtilities.dp(72) + ActionBar.getCurrentActionBarHeight()) { allowStickersPanel = false; if (stickersPanel.getVisibility() == View.VISIBLE) { - stickersPanel.clearAnimation(); stickersPanel.setVisibility(View.INVISIBLE); } if (mentionContainer != null && mentionContainer.getVisibility() == View.VISIBLE) { - mentionContainer.clearAnimation(); mentionContainer.setVisibility(View.INVISIBLE); } } else { allowStickersPanel = true; if (stickersPanel.getVisibility() == View.INVISIBLE) { - stickersPanel.clearAnimation(); stickersPanel.setVisibility(View.VISIBLE); } if (mentionContainer != null && mentionContainer.getVisibility() == View.INVISIBLE && (!mentionsAdapter.isBotContext() || (allowContextBotPanel || allowContextBotPanelSecond))) { - mentionContainer.clearAnimation(); mentionContainer.setVisibility(View.VISIBLE); mentionContainer.setTag(null); } @@ -2347,7 +2334,54 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } }); - FrameLayout replyLayout = new FrameLayout(context); + FrameLayout replyLayout = new FrameLayout(context) { + @Override + public void setTranslationY(float translationY) { + super.setTranslationY(translationY); + if (chatActivityEnterView != null) { + chatActivityEnterView.invalidate(); + } + if (getVisibility() != GONE) { + int height = getLayoutParams().height; + if (chatListView != null) { + chatListView.setTranslationY(translationY); + } + if (progressView != null) { + progressView.setTranslationY(translationY); + } + if (mentionContainer != null) { + mentionContainer.setTranslationY(translationY); + } + if (pagedownButton != null) { + pagedownButton.setTranslationY(translationY); + } + } + } + + @Override + public boolean hasOverlappingRendering() { + return false; + } + + @Override + public void setVisibility(int visibility) { + super.setVisibility(visibility); + if (visibility == GONE) { + if (chatListView != null) { + chatListView.setTranslationY(0); + } + if (progressView != null) { + progressView.setTranslationY(0); + } + if (mentionContainer != null) { + mentionContainer.setTranslationY(0); + } + if (pagedownButton != null) { + pagedownButton.setTranslationY(pagedownButton.getTag() == null ? AndroidUtilities.dp(100) : 0); + } + } + } + }; replyLayout.setClickable(true); chatActivityEnterView.addTopView(replyLayout, 48); @@ -2398,6 +2432,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not return super.onInterceptTouchEvent(event) || result; } }; + stickersListView.setTag(3); stickersListView.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { @@ -2409,9 +2444,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not layoutManager.setOrientation(LinearLayoutManager.HORIZONTAL); stickersListView.setLayoutManager(layoutManager); stickersListView.setClipToPadding(false); - if (Build.VERSION.SDK_INT >= 9) { - stickersListView.setOverScrollMode(RecyclerListView.OVER_SCROLL_NEVER); - } + stickersListView.setOverScrollMode(RecyclerListView.OVER_SCROLL_NEVER); stickersPanel.addView(stickersListView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 78)); initStickers(); @@ -2419,6 +2452,44 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not imageView.setImageResource(R.drawable.stickers_back_arrow); stickersPanel.addView(imageView, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.BOTTOM | Gravity.LEFT, 53, 0, 0, 0)); + searchContainer = new FrameLayout(context); + searchContainer.setBackgroundResource(R.drawable.compose_panel); + searchContainer.setVisibility(View.INVISIBLE); + searchContainer.setFocusable(true); + searchContainer.setFocusableInTouchMode(true); + searchContainer.setClickable(true); + searchContainer.setBackgroundResource(R.drawable.compose_panel); + searchContainer.setPadding(0, AndroidUtilities.dp(3), 0, 0); + contentView.addView(searchContainer, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 51, Gravity.BOTTOM)); + + searchUpButton = new ImageView(context); + searchUpButton.setScaleType(ImageView.ScaleType.CENTER); + searchUpButton.setImageResource(R.drawable.search_up); + searchContainer.addView(searchUpButton, LayoutHelper.createFrame(48, 48)); + searchUpButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + MessagesSearchQuery.searchMessagesInChat(null, dialog_id, mergeDialogId, classGuid, 1); + } + }); + + searchDownButton = new ImageView(context); + searchDownButton.setScaleType(ImageView.ScaleType.CENTER); + searchDownButton.setImageResource(R.drawable.search_down); + searchContainer.addView(searchDownButton, LayoutHelper.createFrame(48, 48, Gravity.LEFT | Gravity.TOP, 48, 0, 0, 0)); + searchDownButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + MessagesSearchQuery.searchMessagesInChat(null, dialog_id, mergeDialogId, classGuid, 2); + } + }); + + searchCountText = new SimpleTextView(context); + searchCountText.setTextColor(Theme.CHAT_SEARCH_COUNT_TEXT_COLOR); + searchCountText.setTextSize(15); + searchCountText.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); + searchContainer.addView(searchCountText, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, Gravity.LEFT | Gravity.CENTER_VERTICAL, 108, 0, 0, 0)); + bottomOverlay = new FrameLayout(context); bottomOverlay.setVisibility(View.INVISIBLE); bottomOverlay.setFocusable(true); @@ -2453,7 +2524,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not if (botUserLast != null && botUserLast.length() != 0) { MessagesController.getInstance().sendBotStart(currentUser, botUserLast); } else { - SendMessagesHelper.getInstance().sendMessage("/start", dialog_id, null, null, false, chatActivityEnterView == null || chatActivityEnterView.asAdmin(), null, null, null); + SendMessagesHelper.getInstance().sendMessage("/start", dialog_id, null, null, false, null, null, null); } } else { builder = new AlertDialog.Builder(getParentActivity()); @@ -2469,7 +2540,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not if (botUser.length() != 0) { MessagesController.getInstance().sendBotStart(currentUser, botUser); } else { - SendMessagesHelper.getInstance().sendMessage("/start", dialog_id, null, null, false, chatActivityEnterView == null || chatActivityEnterView.asAdmin(), null, null, null); + SendMessagesHelper.getInstance().sendMessage("/start", dialog_id, null, null, false, null, null, null); } botUser = null; updateBottomOverlay(); @@ -2615,6 +2686,9 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not chatAttachAlert.setDelegate(new ChatAttachAlert.ChatAttachViewDelegate() { @Override public void didPressedButton(int button) { + if (getParentActivity() == null) { + return; + } if (button == 7) { chatAttachAlert.dismiss(); HashMap selectedPhotos = chatAttachAlert.getSelectedPhotos(); @@ -2634,8 +2708,9 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not photoEntry.thumbPath = null; photoEntry.caption = null; } - SendMessagesHelper.prepareSendingPhotos(photos, null, dialog_id, replyingMessageObject, captions, chatActivityEnterView == null || chatActivityEnterView.asAdmin()); + SendMessagesHelper.prepareSendingPhotos(photos, null, dialog_id, replyingMessageObject, captions); showReplyPanel(false, null, null, null, false, true); + DraftQuery.cleanDraft(dialog_id, true); } return; } else if (chatAttachAlert != null) { @@ -2707,7 +2782,6 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } if (show) { stickersListView.scrollToPosition(0); - stickersPanel.clearAnimation(); stickersPanel.setVisibility(allowStickersPanel ? View.VISIBLE : View.INVISIBLE); } if (runningAnimation != null) { @@ -2715,18 +2789,17 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not runningAnimation = null; } if (stickersPanel.getVisibility() != View.INVISIBLE) { - runningAnimation = new AnimatorSetProxy(); + runningAnimation = new AnimatorSet(); runningAnimation.playTogether( - ObjectAnimatorProxy.ofFloat(stickersPanel, "alpha", show ? 0.0f : 1.0f, show ? 1.0f : 0.0f) + ObjectAnimator.ofFloat(stickersPanel, "alpha", show ? 0.0f : 1.0f, show ? 1.0f : 0.0f) ); runningAnimation.setDuration(150); runningAnimation.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (runningAnimation != null && runningAnimation.equals(animation)) { if (!show) { stickersAdapter.clearStickers(); - stickersPanel.clearAnimation(); stickersPanel.setVisibility(View.GONE); if (StickerPreviewViewer.getInstance().isVisible()) { StickerPreviewViewer.getInstance().close(); @@ -2738,7 +2811,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } @Override - public void onAnimationCancel(Object animation) { + public void onAnimationCancel(Animator animation) { if (runningAnimation != null && runningAnimation.equals(animation)) { runningAnimation = null; } @@ -2755,7 +2828,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not public void onItemClick(View view, int position) { TLRPC.Document document = stickersAdapter.getItem(position); if (document instanceof TLRPC.TL_document) { - SendMessagesHelper.getInstance().sendSticker(document, dialog_id, replyingMessageObject, chatActivityEnterView == null || chatActivityEnterView.asAdmin()); + SendMessagesHelper.getInstance().sendSticker(document, dialog_id, replyingMessageObject); showReplyPanel(false, null, null, null, false, true); chatActivityEnterView.addStickerToRecent(document); } @@ -2779,7 +2852,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not builder.setPositiveButton(LocaleController.getString("OK", R.string.OK), new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { - SendMessagesHelper.getInstance().sendMessage(UserConfig.getCurrentUser(), dialog_id, messageObject, chatActivityEnterView == null || chatActivityEnterView.asAdmin(), null, null); + SendMessagesHelper.getInstance().sendMessage(UserConfig.getCurrentUser(), dialog_id, messageObject, null, null); moveScrollToLastMessage(); showReplyPanel(false, null, null, null, false, true); } @@ -2823,41 +2896,40 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not gifHintTextView.setGravity(Gravity.CENTER_VERTICAL); frameLayout.addView(gifHintTextView, index + 1, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, 32, Gravity.LEFT | Gravity.BOTTOM, 5, 0, 0, 3)); - AnimatorSetProxy animatorSetProxy = new AnimatorSetProxy(); - animatorSetProxy.playTogether( - ObjectAnimatorProxy.ofFloat(gifHintTextView, "alpha", 0.0f, 1.0f), - ObjectAnimatorProxy.ofFloat(emojiButtonRed, "alpha", 0.0f, 1.0f) + AnimatorSet AnimatorSet = new AnimatorSet(); + AnimatorSet.playTogether( + ObjectAnimator.ofFloat(gifHintTextView, "alpha", 0.0f, 1.0f), + ObjectAnimator.ofFloat(emojiButtonRed, "alpha", 0.0f, 1.0f) ); - animatorSetProxy.addListener(new AnimatorListenerAdapterProxy() { + AnimatorSet.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { if (gifHintTextView == null) { return; } - AnimatorSetProxy animatorSetProxy = new AnimatorSetProxy(); - animatorSetProxy.playTogether( - ObjectAnimatorProxy.ofFloat(gifHintTextView, "alpha", 0.0f) + AnimatorSet AnimatorSet = new AnimatorSet(); + AnimatorSet.playTogether( + ObjectAnimator.ofFloat(gifHintTextView, "alpha", 0.0f) ); - animatorSetProxy.addListener(new AnimatorListenerAdapterProxy() { + AnimatorSet.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (gifHintTextView != null) { - gifHintTextView.clearAnimation(); gifHintTextView.setVisibility(View.GONE); } } }); - animatorSetProxy.setDuration(300); - animatorSetProxy.start(); + AnimatorSet.setDuration(300); + AnimatorSet.start(); } }, 2000); } }); - animatorSetProxy.setDuration(300); - animatorSetProxy.start(); + AnimatorSet.setDuration(300); + AnimatorSet.start(); } private void checkContextBotPanel() { @@ -2869,22 +2941,21 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } mentionContainer.setTag(1); - mentionListAnimation = new AnimatorSetProxy(); + mentionListAnimation = new AnimatorSet(); mentionListAnimation.playTogether( - ObjectAnimatorProxy.ofFloat(mentionContainer, "alpha", 0.0f) + ObjectAnimator.ofFloat(mentionContainer, "alpha", 0.0f) ); mentionListAnimation.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (mentionListAnimation != null && mentionListAnimation.equals(animation)) { - mentionContainer.clearAnimation(); mentionContainer.setVisibility(View.INVISIBLE); mentionListAnimation = null; } } @Override - public void onAnimationCancel(Object animation) { + public void onAnimationCancel(Animator animation) { if (mentionListAnimation != null && mentionListAnimation.equals(animation)) { mentionListAnimation = null; } @@ -2900,21 +2971,20 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } mentionContainer.setTag(null); mentionContainer.setVisibility(View.VISIBLE); - mentionListAnimation = new AnimatorSetProxy(); + mentionListAnimation = new AnimatorSet(); mentionListAnimation.playTogether( - ObjectAnimatorProxy.ofFloat(mentionContainer, "alpha", 0.0f, 1.0f) + ObjectAnimator.ofFloat(mentionContainer, "alpha", 0.0f, 1.0f) ); mentionListAnimation.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (mentionListAnimation != null && mentionListAnimation.equals(animation)) { - mentionContainer.clearAnimation(); mentionListAnimation = null; } } @Override - public void onAnimationCancel(Object animation) { + public void onAnimationCancel(Animator animation) { if (mentionListAnimation != null && mentionListAnimation.equals(animation)) { mentionListAnimation = null; } @@ -2946,24 +3016,24 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not loading = true; waitingForLoad.add(lastLoadIndex); if (messagesByDays.size() != 0) { - MessagesController.getInstance().loadMessages(dialog_id, 50, maxMessageId[0], !cacheEndReached[0], minDate[0], classGuid, 0, 0, channelMessagesImportant, lastLoadIndex++); + MessagesController.getInstance().loadMessages(dialog_id, 50, maxMessageId[0], !cacheEndReached[0], minDate[0], classGuid, 0, 0, ChatObject.isChannel(currentChat), lastLoadIndex++); } else { - MessagesController.getInstance().loadMessages(dialog_id, 50, 0, !cacheEndReached[0], minDate[0], classGuid, 0, 0, channelMessagesImportant, lastLoadIndex++); + MessagesController.getInstance().loadMessages(dialog_id, 50, 0, !cacheEndReached[0], minDate[0], classGuid, 0, 0, ChatObject.isChannel(currentChat), lastLoadIndex++); } } else if (mergeDialogId != 0 && !endReached[1]) { loading = true; waitingForLoad.add(lastLoadIndex); - MessagesController.getInstance().loadMessages(mergeDialogId, 50, maxMessageId[1], !cacheEndReached[1], minDate[1], classGuid, 0, 0, 0, lastLoadIndex++); + MessagesController.getInstance().loadMessages(mergeDialogId, 50, maxMessageId[1], !cacheEndReached[1], minDate[1], classGuid, 0, 0, ChatObject.isChannel(currentChat), lastLoadIndex++); } } if (!loadingForward && firstVisibleItem + visibleItemCount >= totalItemCount - 10) { if (mergeDialogId != 0 && !forwardEndReached[1]) { waitingForLoad.add(lastLoadIndex); - MessagesController.getInstance().loadMessages(mergeDialogId, 50, minMessageId[1], true, maxDate[1], classGuid, 1, 0, 0, lastLoadIndex++); + MessagesController.getInstance().loadMessages(mergeDialogId, 50, minMessageId[1], true, maxDate[1], classGuid, 1, 0, ChatObject.isChannel(currentChat), lastLoadIndex++); loadingForward = true; } else if (!forwardEndReached[0]) { waitingForLoad.add(lastLoadIndex); - MessagesController.getInstance().loadMessages(dialog_id, 50, minMessageId[0], true, maxDate[0], classGuid, 1, 0, channelMessagesImportant, lastLoadIndex++); + MessagesController.getInstance().loadMessages(dialog_id, 50, minMessageId[0], true, maxDate[0], classGuid, 1, 0, ChatObject.isChannel(currentChat), lastLoadIndex++); loadingForward = true; } } @@ -3020,9 +3090,10 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not fragment.setDelegate(new PhotoAlbumPickerActivity.PhotoAlbumPickerActivityDelegate() { @Override public void didSelectPhotos(ArrayList photos, ArrayList captions, ArrayList webPhotos) { - SendMessagesHelper.prepareSendingPhotos(photos, null, dialog_id, replyingMessageObject, captions, chatActivityEnterView == null || chatActivityEnterView.asAdmin()); - SendMessagesHelper.prepareSendingPhotosSearch(webPhotos, dialog_id, replyingMessageObject, chatActivityEnterView == null || chatActivityEnterView.asAdmin()); + SendMessagesHelper.prepareSendingPhotos(photos, null, dialog_id, replyingMessageObject, captions); + SendMessagesHelper.prepareSendingPhotosSearch(webPhotos, dialog_id, replyingMessageObject); showReplyPanel(false, null, null, null, false, true); + DraftQuery.cleanDraft(dialog_id, true); } @Override @@ -3049,8 +3120,9 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not if (Build.VERSION.SDK_INT >= 16) { return !openVideoEditor(path, true, true); } else { - SendMessagesHelper.prepareSendingVideo(path, 0, 0, 0, 0, null, dialog_id, replyingMessageObject, chatActivityEnterView == null || chatActivityEnterView.asAdmin()); + SendMessagesHelper.prepareSendingVideo(path, 0, 0, 0, 0, null, dialog_id, replyingMessageObject); showReplyPanel(false, null, null, null, false, true); + DraftQuery.cleanDraft(dialog_id, true); return true; } } @@ -3079,9 +3151,10 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not fragment.setDelegate(new LocationActivity.LocationActivityDelegate() { @Override public void didSelectLocation(TLRPC.MessageMedia location) { - SendMessagesHelper.getInstance().sendMessage(location, dialog_id, replyingMessageObject, chatActivityEnterView == null || chatActivityEnterView.asAdmin(), null, null); + SendMessagesHelper.getInstance().sendMessage(location, dialog_id, replyingMessageObject, null, null); moveScrollToLastMessage(); showReplyPanel(false, null, null, null, false, true); + DraftQuery.cleanDraft(dialog_id, true); if (paused) { scrollToTopOnResume = true; } @@ -3098,8 +3171,9 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not @Override public void didSelectFiles(DocumentSelectActivity activity, ArrayList files) { activity.finishFragment(); - SendMessagesHelper.prepareSendingDocuments(files, files, null, null, dialog_id, replyingMessageObject, chatActivityEnterView == null || chatActivityEnterView.asAdmin()); + SendMessagesHelper.prepareSendingDocuments(files, files, null, null, dialog_id, replyingMessageObject); showReplyPanel(false, null, null, null, false, true); + DraftQuery.cleanDraft(dialog_id, true); } @Override @@ -3123,8 +3197,9 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not fragment.setDelegate(new AudioSelectActivity.AudioSelectActivityDelegate() { @Override public void didSelectAudio(ArrayList audios) { - SendMessagesHelper.prepareSendingAudioDocuments(audios, dialog_id, replyingMessageObject, chatActivityEnterView == null || chatActivityEnterView.asAdmin()); + SendMessagesHelper.prepareSendingAudioDocuments(audios, dialog_id, replyingMessageObject); showReplyPanel(false, null, null, null, false, true); + DraftQuery.cleanDraft(dialog_id, true); } }); presentFragment(fragment); @@ -3318,10 +3393,10 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not return; } if (!fromMyName) { - SendMessagesHelper.getInstance().sendMessage(arrayList, dialog_id, chatActivityEnterView == null || chatActivityEnterView.asAdmin()); + SendMessagesHelper.getInstance().sendMessage(arrayList, dialog_id); } else { for (MessageObject object : arrayList) { - SendMessagesHelper.getInstance().processForwardFromMyName(object, dialog_id, chatActivityEnterView == null || chatActivityEnterView.asAdmin()); + SendMessagesHelper.getInstance().processForwardFromMyName(object, dialog_id); } } } @@ -3334,6 +3409,10 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not if (messageObjectToReply == null && messageObjectsToForward == null && webPage == null) { return; } + if (searchItem != null && actionBar.isSearchFieldVisible()) { + actionBar.closeSearchField(); + chatActivityEnterView.setFieldFocused(); + } boolean openKeyboard = false; if (messageObjectToReply != null && messageObjectToReply.getDialogId() != dialog_id) { messageObjectsToForward = new ArrayList<>(); @@ -3564,8 +3643,6 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not replyingMessageObject = null; forwardingMessages = null; replyImageLocation = null; - SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE); - preferences.edit().remove("reply_" + dialog_id).commit(); } } @@ -3609,7 +3686,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not first = true; firstLoading = true; loading = true; - waitingForImportantLoad = false; + loadingForward = false; waitingForReplyMessageLoad = false; startLoadFromMessageId = 0; last_message_id = 0; @@ -3629,7 +3706,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } else { clearChatData(); waitingForLoad.add(lastLoadIndex); - MessagesController.getInstance().loadMessages(dialog_id, 30, 0, true, 0, classGuid, 0, 0, channelMessagesImportant, lastLoadIndex++); + MessagesController.getInstance().loadMessages(dialog_id, 30, 0, true, 0, classGuid, 0, 0, ChatObject.isChannel(currentChat), lastLoadIndex++); } } @@ -3638,12 +3715,20 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not return; } int count = chatListView.getChildCount(); + int additionalTop = chatActivityEnterView.isTopViewVisible() ? AndroidUtilities.dp(48) : 0; + int height = chatListView.getMeasuredHeight(); for (int a = 0; a < count; a++) { View view = chatListView.getChildAt(a); if (view instanceof ChatMessageCell) { ChatMessageCell messageCell = (ChatMessageCell) view; - messageCell.getLocalVisibleRect(scrollRect); - messageCell.setVisiblePart(scrollRect.top, scrollRect.bottom - scrollRect.top); + int top = messageCell.getTop(); + int bottom = messageCell.getBottom(); + int viewTop = top >= 0 ? 0 : -top; + int viewBottom = messageCell.getMeasuredHeight(); + if (viewBottom > height) { + viewBottom = viewTop + height; + } + messageCell.setVisiblePart(viewTop, viewBottom - viewTop); } } } @@ -3659,7 +3744,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not flags = 1; MessagesStorage.getInstance().setDialogFlags(dialog_id, flags); editor.commit(); - TLRPC.Dialog dialog = MessagesController.getInstance().dialogs_dict.get(dialog_id); + TLRPC.TL_dialog dialog = MessagesController.getInstance().dialogs_dict.get(dialog_id); if (dialog != null) { dialog.notify_settings = new TLRPC.TL_peerNotifySettings(); dialog.notify_settings.mute_until = Integer.MAX_VALUE; @@ -3675,7 +3760,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not editor.putInt("notify2_" + dialog_id, 0); MessagesStorage.getInstance().setDialogFlags(dialog_id, 0); editor.commit(); - TLRPC.Dialog dialog = MessagesController.getInstance().dialogs_dict.get(dialog_id); + TLRPC.TL_dialog dialog = MessagesController.getInstance().dialogs_dict.get(dialog_id); if (dialog != null) { dialog.notify_settings = new TLRPC.TL_peerNotifySettings(); } @@ -3743,12 +3828,13 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not unreadMessageObject = null; scrollToMessage = null;*/ + waitingForLoad.clear(); waitingForReplyMessageLoad = true; highlightMessageId = Integer.MAX_VALUE; scrollToMessagePosition = -10000; startLoadFromMessageId = id; waitingForLoad.add(lastLoadIndex); - MessagesController.getInstance().loadMessages(loadIndex == 0 ? dialog_id : mergeDialogId, AndroidUtilities.isTablet() ? 30 : 20, startLoadFromMessageId, true, 0, classGuid, 3, 0, loadIndex == 0 ? channelMessagesImportant : 0, lastLoadIndex++); + MessagesController.getInstance().loadMessages(loadIndex == 0 ? dialog_id : mergeDialogId, AndroidUtilities.isTablet() ? 30 : 20, startLoadFromMessageId, true, 0, classGuid, 3, 0, ChatObject.isChannel(currentChat), lastLoadIndex++); //emptyViewContainer.setVisibility(View.INVISIBLE); } returnToMessageId = fromMessageId; @@ -3761,24 +3847,27 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not return; } if (show) { + pagedownButtonShowedByScroll = false; if (pagedownButton.getTag() == null) { if (pagedownButtonAnimation != null) { pagedownButtonAnimation.cancel(); pagedownButtonAnimation = null; } if (animated) { - if (ViewProxy.getTranslationY(pagedownButton) == 0) { - ViewProxy.setTranslationY(pagedownButton, AndroidUtilities.dp(100)); + if (pagedownButton.getTranslationY() == 0) { + pagedownButton.setTranslationY(AndroidUtilities.dp(100)); } pagedownButton.setVisibility(View.VISIBLE); pagedownButton.setTag(1); - pagedownButtonAnimation = ObjectAnimatorProxy.ofFloatProxy(pagedownButton, "translationY", 0).setDuration(200).start(); + pagedownButtonAnimation = ObjectAnimator.ofFloat(pagedownButton, "translationY", 0).setDuration(200); + pagedownButtonAnimation.start(); } else { pagedownButton.setVisibility(View.VISIBLE); } } } else { returnToMessageId = 0; + newUnreadMessageCount = 0; if (pagedownButton.getTag() != null) { pagedownButton.setTag(null); if (pagedownButtonAnimation != null) { @@ -3786,15 +3875,16 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not pagedownButtonAnimation = null; } if (animated) { - pagedownButtonAnimation = ObjectAnimatorProxy.ofFloatProxy(pagedownButton, "translationY", AndroidUtilities.dp(100)).setDuration(200).addListener(new AnimatorListenerAdapterProxy() { + pagedownButtonAnimation = ObjectAnimator.ofFloat(pagedownButton, "translationY", AndroidUtilities.dp(100)).setDuration(200); + pagedownButtonAnimation.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { - pagedownButton.clearAnimation(); + public void onAnimationEnd(Animator animation) { + pagedownButtonCounter.setVisibility(View.INVISIBLE); pagedownButton.setVisibility(View.INVISIBLE); } - }).start(); + }); + pagedownButtonAnimation.start(); } else { - pagedownButton.clearAnimation(); pagedownButton.setVisibility(View.INVISIBLE); } } @@ -3822,8 +3912,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not bottomOverlayText.setText(LocaleController.getString("EncryptionRejected", R.string.EncryptionRejected)); bottomOverlay.setVisibility(View.VISIBLE); chatActivityEnterView.setFieldText(""); - SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE); - preferences.edit().remove("dialog_" + dialog_id).commit(); + DraftQuery.cleanDraft(dialog_id, false); hideKeyboard = true; } else if (currentEncryptedChat instanceof TLRPC.TL_encryptedChat) { bottomOverlay.setVisibility(View.INVISIBLE); @@ -4055,35 +4144,33 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } if (copyVisible != newCopyVisible) { if (newVisibility == View.VISIBLE) { - ViewProxy.setAlpha(replyItem, 1.0f); - ViewProxy.setScaleX(replyItem, 1.0f); + replyItem.setAlpha(1.0f); + replyItem.setScaleX(1.0f); } else { - ViewProxy.setAlpha(replyItem, 0.0f); - ViewProxy.setScaleX(replyItem, 0.0f); + replyItem.setAlpha(0.0f); + replyItem.setScaleX(0.0f); } replyItem.setVisibility(newVisibility); - replyItem.clearAnimation(); } else { - replyButtonAnimation = new AnimatorSetProxy(); - ViewProxy.setPivotX(replyItem, AndroidUtilities.dp(54)); + replyButtonAnimation = new AnimatorSet(); + replyItem.setPivotX(AndroidUtilities.dp(54)); if (newVisibility == View.VISIBLE) { replyItem.setVisibility(newVisibility); replyButtonAnimation.playTogether( - ObjectAnimatorProxy.ofFloat(replyItem, "alpha", 1.0f), - ObjectAnimatorProxy.ofFloat(replyItem, "scaleX", 1.0f) + ObjectAnimator.ofFloat(replyItem, "alpha", 1.0f), + ObjectAnimator.ofFloat(replyItem, "scaleX", 1.0f) ); } else { replyButtonAnimation.playTogether( - ObjectAnimatorProxy.ofFloat(replyItem, "alpha", 0.0f), - ObjectAnimatorProxy.ofFloat(replyItem, "scaleX", 0.0f) + ObjectAnimator.ofFloat(replyItem, "alpha", 0.0f), + ObjectAnimator.ofFloat(replyItem, "scaleX", 0.0f) ); } replyButtonAnimation.setDuration(100); replyButtonAnimation.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (replyButtonAnimation != null && replyButtonAnimation.equals(animation)) { - replyItem.clearAnimation(); if (newVisibility == View.GONE) { replyItem.setVisibility(View.GONE); } @@ -4091,7 +4178,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } @Override - public void onAnimationCancel(Object animation) { + public void onAnimationCancel(Animator animation) { if (replyButtonAnimation != null && replyButtonAnimation.equals(animation)) { replyButtonAnimation = null; } @@ -4234,14 +4321,16 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not videoEditedInfo.resultWidth = resultWidth; videoEditedInfo.resultHeight = resultHeight; videoEditedInfo.originalPath = videoPath; - SendMessagesHelper.prepareSendingVideo(videoPath, estimatedSize, estimatedDuration, resultWidth, resultHeight, videoEditedInfo, dialog_id, replyingMessageObject, chatActivityEnterView == null || chatActivityEnterView.asAdmin()); + SendMessagesHelper.prepareSendingVideo(videoPath, estimatedSize, estimatedDuration, resultWidth, resultHeight, videoEditedInfo, dialog_id, replyingMessageObject); showReplyPanel(false, null, null, null, false, true); + DraftQuery.cleanDraft(dialog_id, true); } }); if (parentLayout == null || !fragment.onFragmentCreate()) { - SendMessagesHelper.prepareSendingVideo(videoPath, 0, 0, 0, 0, null, dialog_id, replyingMessageObject, chatActivityEnterView == null || chatActivityEnterView.asAdmin()); + SendMessagesHelper.prepareSendingVideo(videoPath, 0, 0, 0, 0, null, dialog_id, replyingMessageObject); showReplyPanel(false, null, null, null, false, true); + DraftQuery.cleanDraft(dialog_id, true); return false; } parentLayout.presentFragment(fragment, removeLast, !animated, true); @@ -4313,13 +4402,13 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not openVideoEditor(videoPath, false, false); } } else { - SendMessagesHelper.prepareSendingVideo(videoPath, 0, 0, 0, 0, null, dialog_id, replyingMessageObject, chatActivityEnterView == null || chatActivityEnterView.asAdmin()); - showReplyPanel(false, null, null, null, false, true); + SendMessagesHelper.prepareSendingVideo(videoPath, 0, 0, 0, 0, null, dialog_id, replyingMessageObject); } } else { - SendMessagesHelper.prepareSendingPhoto(null, uri, dialog_id, replyingMessageObject, null, chatActivityEnterView == null || chatActivityEnterView.asAdmin()); + SendMessagesHelper.prepareSendingPhoto(null, uri, dialog_id, replyingMessageObject, null); } showReplyPanel(false, null, null, null, false, true); + DraftQuery.cleanDraft(dialog_id, true); } else if (requestCode == 2) { String videoPath = null; FileLog.d("tmessages", "pic path " + currentPicturePath); @@ -4357,8 +4446,9 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not openVideoEditor(videoPath, false, false); } } else { - SendMessagesHelper.prepareSendingVideo(videoPath, 0, 0, 0, 0, null, dialog_id, replyingMessageObject, chatActivityEnterView == null || chatActivityEnterView.asAdmin()); + SendMessagesHelper.prepareSendingVideo(videoPath, 0, 0, 0, 0, null, dialog_id, replyingMessageObject); showReplyPanel(false, null, null, null, false, true); + DraftQuery.cleanDraft(dialog_id, true); } } else if (requestCode == 21) { if (data == null || data.getData() == null) { @@ -4391,8 +4481,9 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not showAttachmentError(); return; } - SendMessagesHelper.prepareSendingDocument(tempPath, originalPath, null, null, dialog_id, replyingMessageObject, chatActivityEnterView == null || chatActivityEnterView.asAdmin()); + SendMessagesHelper.prepareSendingDocument(tempPath, originalPath, null, null, dialog_id, replyingMessageObject); showReplyPanel(false, null, null, null, false, true); + DraftQuery.cleanDraft(dialog_id, true); } else if (requestCode == 31) { if (data == null || data.getData() == null) { showAttachmentError(); @@ -4412,10 +4503,11 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not user.first_name = name; user.last_name = ""; user.phone = number; - SendMessagesHelper.getInstance().sendMessage(user, dialog_id, replyingMessageObject, chatActivityEnterView == null || chatActivityEnterView.asAdmin(), null, null); + SendMessagesHelper.getInstance().sendMessage(user, dialog_id, replyingMessageObject, null, null); } if (sent) { showReplyPanel(false, null, null, null, false, true); + DraftQuery.cleanDraft(dialog_id, true); } } } finally { @@ -4461,13 +4553,13 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not @Override public void didReceivedNotification(int id, final Object... args) { if (id == NotificationCenter.messagesDidLoaded) { - int guid = (Integer) args[11]; + int guid = (Integer) args[10]; if (guid == classGuid) { if (!openAnimationEnded) { NotificationCenter.getInstance().setAllowedNotificationsDutingAnimation(new int[]{NotificationCenter.chatInfoDidLoaded, NotificationCenter.dialogsNeedReload, NotificationCenter.closeChats, NotificationCenter.botKeyboardDidLoaded/*, NotificationCenter.botInfoDidLoaded*/}); } - int queryLoadIndex = (Integer) args[12]; + int queryLoadIndex = (Integer) args[11]; int index = waitingForLoad.indexOf(queryLoadIndex); if (index == -1) { return; @@ -4475,20 +4567,18 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not waitingForLoad.remove(index); } ArrayList messArr = (ArrayList) args[2]; - if (waitingForImportantLoad || waitingForReplyMessageLoad) { - if (waitingForReplyMessageLoad) { - boolean found = false; - for (int a = 0; a < messArr.size(); a++) { - if (messArr.get(a).getId() == startLoadFromMessageId) { - found = true; - break; - } - } - if (!found) { - startLoadFromMessageId = 0; - return; + if (waitingForReplyMessageLoad) { + boolean found = false; + for (int a = 0; a < messArr.size(); a++) { + if (messArr.get(a).getId() == startLoadFromMessageId) { + found = true; + break; } } + if (!found) { + startLoadFromMessageId = 0; + return; + } int startLoadFrom = startLoadFromMessageId; boolean needSelect = needSelectFromMessageId; clearChatData(); @@ -4512,15 +4602,6 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } else if (startLoadFromMessageId != 0 && load_type == 3) { last_message_id = (Integer) args[5]; } - ArrayList groups = (ArrayList) args[9]; - SparseArray groupsByStart = null; - if (groups != null && !groups.isEmpty()) { - groupsByStart = new SparseArray<>(); - for (int a = 0; a < groups.size(); a++) { - TLRPC.TL_messageGroup group = groups.get(a); - groupsByStart.put(group.min_id, group); - } - } int newRowsCount = 0; forwardEndReached[loadIndex] = startLoadFromMessageId == 0 && last_message_id == 0; @@ -4581,7 +4662,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not if (loadIndex == 1) { obj.setIsRead(); } - if (loadIndex == 0 && channelMessagesImportant != 0 && obj.getId() == 1) { + if (loadIndex == 0 && ChatObject.isChannel(currentChat) && obj.getId() == 1) { endReached[loadIndex] = true; cacheEndReached[loadIndex] = true; } @@ -4615,6 +4696,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not TLRPC.Message dateMsg = new TLRPC.Message(); dateMsg.message = LocaleController.formatDateChat(obj.messageOwner.date); dateMsg.id = 0; + dateMsg.date = obj.messageOwner.date; MessageObject dateObj = new MessageObject(dateMsg, null, false); dateObj.type = 10; dateObj.contentType = 1; @@ -4632,27 +4714,6 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not messages.add(0, obj); } - if (groupsByStart != null) { - TLRPC.TL_messageGroup group = groupsByStart.get(obj.getId()); - if (group != null) { - TLRPC.Message dateMsg = new TLRPC.Message(); - dateMsg.message = "+" + LocaleController.formatPluralString("comments", group.count); - dateMsg.id = 0; - dateMsg.date = group.min_id; - dateMsg.from_id = group.max_id; - MessageObject dateObj = new MessageObject(dateMsg, null, false); - dateObj.type = 10; - dateObj.contentType = 1; - dayArray.add(dateObj); - if (load_type == 1) { - messages.add(0, dateObj); - } else { - messages.add(messages.size() - 1, dateObj); - } - newRowsCount++; - } - } - if (load_type != 1) { dayArray.add(obj); messages.add(messages.size() - 1, obj); @@ -5023,7 +5084,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not scrollToLastMessage(false); return; } - if (messagesDict[0].containsKey(obj.getId())) { + if (obj.type < 0 || messagesDict[0].containsKey(obj.getId())) { continue; } obj.checkLayout(); @@ -5063,15 +5124,46 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not int oldCount = messages.size(); int addedCount = 0; HashMap> webpagesToReload = null; + int placeToPaste = -1; for (int a = 0; a < arr.size(); a++) { MessageObject obj = arr.get(a); + if (a == 0) { + if (obj.messageOwner.id < 0) { + placeToPaste = 0; + } else { + if (!messages.isEmpty()) { + int size = messages.size(); + for (int b = 0; b < size; b++) { + MessageObject lastMessage = messages.get(b); + if (lastMessage.type >= 0 && lastMessage.messageOwner.date > 0) { + if (lastMessage.messageOwner.id > 0 && obj.messageOwner.id > 0) { + if (lastMessage.messageOwner.id < obj.messageOwner.id) { + placeToPaste = b; + break; + } + } else { + if (lastMessage.messageOwner.date < obj.messageOwner.date) { + placeToPaste = b; + break; + } + } + } + } + if (placeToPaste == -1 || placeToPaste > messages.size()) { + placeToPaste = messages.size(); + } + } else { + placeToPaste = 0; + } + } + } if (currentUser != null && currentUser.bot && obj.isOut()) { obj.setIsRead(); } if (avatarContainer != null && currentEncryptedChat != null && obj.messageOwner.action != null && obj.messageOwner.action instanceof TLRPC.TL_messageEncryptedAction && obj.messageOwner.action.encryptedAction instanceof TLRPC.TL_decryptedMessageActionSetMessageTTL) { avatarContainer.setTime(((TLRPC.TL_decryptedMessageActionSetMessageTTL) obj.messageOwner.action.encryptedAction).ttl_seconds); } - if (messagesDict[0].containsKey(obj.getId())) { + if (obj.type < 0 || messagesDict[0].containsKey(obj.getId())) { continue; } if (currentEncryptedChat != null && obj.messageOwner.media instanceof TLRPC.TL_messageMediaWebPage && obj.messageOwner.media.webpage instanceof TLRPC.TL_webPageUrlPending) { @@ -5137,14 +5229,15 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not TLRPC.Message dateMsg = new TLRPC.Message(); dateMsg.message = LocaleController.formatDateChat(obj.messageOwner.date); dateMsg.id = 0; + dateMsg.date = obj.messageOwner.date; MessageObject dateObj = new MessageObject(dateMsg, null, false); dateObj.type = 10; dateObj.contentType = 1; - messages.add(0, dateObj); + messages.add(placeToPaste, dateObj); addedCount++; } if (!obj.isOut()) { - if (paused) { + if (paused && placeToPaste == 0) { if (!scrollToTopUnReadOnResume && unreadMessageObject != null) { removeMessageObject(unreadMessageObject); unreadMessageObject = null; @@ -5179,8 +5272,9 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } dayArray.add(0, obj); - messages.add(0, obj); + messages.add(placeToPaste, obj); addedCount++; + newUnreadMessageCount++; if (obj.type == 10 || obj.type == 11) { updateChat = true; } @@ -5197,7 +5291,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not chatAdapter.updateRowWithMessageObject(unreadMessageObject); } if (addedCount != 0) { - chatAdapter.notifyItemRangeInserted(chatAdapter.getItemCount(), addedCount); + chatAdapter.notifyItemRangeInserted(chatAdapter.getItemCount() - placeToPaste, addedCount); } } else { scrollToTopOnResume = true; @@ -5215,6 +5309,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not oldCount++; } if (lastVisible >= oldCount || hasFromMe) { + newUnreadMessageCount = 0; if (!firstLoading) { if (paused) { scrollToTopOnResume = true; @@ -5224,6 +5319,10 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } } } else { + if (newUnreadMessageCount != 0 && pagedownButtonCounter != null) { + pagedownButtonCounter.setVisibility(View.VISIBLE); + pagedownButtonCounter.setText(String.format("%d", newUnreadMessageCount)); + } showPagedownButton(true, true); } } else { @@ -5263,7 +5362,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } } else if (id == NotificationCenter.messagesRead) { SparseArray inbox = (SparseArray) args[0]; - SparseIntArray outbox = (SparseIntArray) args[1]; + SparseArray outbox = (SparseArray) args[1]; boolean updated = false; for (int b = 0; b < inbox.size(); b++) { int key = inbox.keyAt(b); @@ -5285,7 +5384,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } for (int b = 0; b < outbox.size(); b++) { int key = outbox.keyAt(b); - int messageId = outbox.get(key); + int messageId = (int) ((long) outbox.get(key)); if (key != dialog_id) { continue; } @@ -5366,7 +5465,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not maxDate[0] = maxDate[1] = Integer.MIN_VALUE; minDate[0] = minDate[1] = 0; waitingForLoad.add(lastLoadIndex); - MessagesController.getInstance().loadMessages(dialog_id, 30, 0, !cacheEndReached[0], minDate[0], classGuid, 0, 0, channelMessagesImportant, lastLoadIndex++); + MessagesController.getInstance().loadMessages(dialog_id, 30, 0, !cacheEndReached[0], minDate[0], classGuid, 0, 0, ChatObject.isChannel(currentChat), lastLoadIndex++); loading = true; } else { if (botButtons != null) { @@ -5434,8 +5533,10 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not if (chatAdapter != null) { chatAdapter.updateRowWithMessageObject(obj); } - if (mediaUpdated && chatLayoutManager.findLastVisibleItemPosition() >= messages.size() - 1) { - moveScrollToLastMessage(); + if (chatLayoutManager != null) { + if (mediaUpdated && chatLayoutManager.findLastVisibleItemPosition() >= messages.size() - 1) { + moveScrollToLastMessage(); + } } NotificationsController.getInstance().playOutChatSound(); } @@ -5686,11 +5787,10 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not first = true; firstLoading = true; loading = true; - waitingForImportantLoad = false; startLoadFromMessageId = 0; needSelectFromMessageId = false; waitingForLoad.add(lastLoadIndex); - MessagesController.getInstance().loadMessages(dialog_id, AndroidUtilities.isTablet() ? 30 : 20, 0, true, 0, classGuid, 2, 0, channelMessagesImportant, lastLoadIndex++); + MessagesController.getInstance().loadMessages(dialog_id, AndroidUtilities.isTablet() ? 30 : 20, 0, true, 0, classGuid, 2, 0, ChatObject.isChannel(currentChat), lastLoadIndex++); } else { if (progressView != null) { progressView.setVisibility(View.INVISIBLE); @@ -5930,7 +6030,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not if (messageId != 0) { scrollToMessageId(messageId, 0, true, did == dialog_id ? 0 : 1); } - updateSearchButtons((Integer) args[2]); + updateSearchButtons((Integer) args[2], (Integer) args[4], (Integer) args[5]); } } else if (id == NotificationCenter.didUpdatedMessagesViews) { SparseArray channelViews = (SparseArray) args[0]; @@ -5957,6 +6057,11 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not if (did == dialog_id) { updateSpamView(); } + } else if (id == NotificationCenter.newDraftReceived) { + long did = (Long) args[0]; + if (did == dialog_id) { + applyDraftMaybe(); + } } } @@ -5969,10 +6074,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not inlineReturn = 0; chatActivityEnterView.setFieldText(query); } else { - SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE); - SharedPreferences.Editor editor = preferences.edit(); - editor.putString("dialog_" + inlineReturn, query); - editor.commit(); + DraftQuery.saveDraft(inlineReturn, query, null, null, false); if (parentLayout.fragmentsStack.size() > 1) { BaseFragment prevFragment = parentLayout.fragmentsStack.get(parentLayout.fragmentsStack.size() - 2); if (prevFragment instanceof ChatActivity && ((ChatActivity) prevFragment).dialog_id == inlineReturn) { @@ -6002,12 +6104,17 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not return true; } - private void updateSearchButtons(int mask) { - if (searchUpItem != null) { - searchUpItem.setEnabled((mask & 1) != 0); - searchDownItem.setEnabled((mask & 2) != 0); - ViewProxy.setAlpha(searchUpItem, searchUpItem.isEnabled() ? 1.0f : 0.6f); - ViewProxy.setAlpha(searchDownItem, searchDownItem.isEnabled() ? 1.0f : 0.6f); + private void updateSearchButtons(int mask, int num, int count) { + if (searchUpButton != null) { + searchUpButton.setEnabled((mask & 1) != 0); + searchDownButton.setEnabled((mask & 2) != 0); + searchUpButton.setAlpha(searchUpButton.isEnabled() ? 1.0f : 0.5f); + searchDownButton.setAlpha(searchDownButton.isEnabled() ? 1.0f : 0.5f); + if (count == 0) { + searchCountText.setText(""); + } else { + searchCountText.setText(LocaleController.formatString("Of", R.string.Of, num + 1, count)); + } } } @@ -6099,21 +6206,30 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not bottomOverlayChatText.setText(LocaleController.getString("DeleteThisChat", R.string.DeleteThisChat)); } } - if (currentChat != null && (ChatObject.isNotInChat(currentChat) || !ChatObject.canWriteToChat(currentChat)) || - currentUser != null && (UserObject.isDeleted(currentUser) || userBlocked)) { - bottomOverlayChat.setVisibility(View.VISIBLE); - muteItem.setVisibility(View.GONE); + + if (searchItem != null && searchItem.getVisibility() == View.VISIBLE) { + searchContainer.setVisibility(View.VISIBLE); + bottomOverlayChat.setVisibility(View.INVISIBLE); chatActivityEnterView.setFieldFocused(false); chatActivityEnterView.setVisibility(View.INVISIBLE); } else { - if (botUser != null && currentUser.bot) { + searchContainer.setVisibility(View.INVISIBLE); + if (currentChat != null && (ChatObject.isNotInChat(currentChat) || !ChatObject.canWriteToChat(currentChat)) || + currentUser != null && (UserObject.isDeleted(currentUser) || userBlocked)) { bottomOverlayChat.setVisibility(View.VISIBLE); + muteItem.setVisibility(View.GONE); + chatActivityEnterView.setFieldFocused(false); chatActivityEnterView.setVisibility(View.INVISIBLE); } else { - chatActivityEnterView.setVisibility(View.VISIBLE); - bottomOverlayChat.setVisibility(View.INVISIBLE); + if (botUser != null && currentUser.bot) { + bottomOverlayChat.setVisibility(View.VISIBLE); + chatActivityEnterView.setVisibility(View.INVISIBLE); + } else { + chatActivityEnterView.setVisibility(View.VISIBLE); + bottomOverlayChat.setVisibility(View.INVISIBLE); + } + muteItem.setVisibility(View.VISIBLE); } - muteItem.setVisibility(View.VISIBLE); } checkRaiseSensors(); } @@ -6129,33 +6245,27 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not alertViewAnimator.cancel(); alertViewAnimator = null; } - if (Build.VERSION.SDK_INT >= 11) { - alertView.setVisibility(View.VISIBLE); - alertViewAnimator = new AnimatorSetProxy(); - alertViewAnimator.playTogether(ObjectAnimatorProxy.ofFloat(alertView, "translationY", 0)); - alertViewAnimator.setDuration(200); - alertViewAnimator.addListener(new AnimatorListenerAdapterProxy() { - @Override - public void onAnimationEnd(Object animation) { - if (alertViewAnimator != null && alertViewAnimator.equals(animation)) { - alertView.clearAnimation(); - alertViewAnimator = null; - } - } - @Override - public void onAnimationCancel(Object animation) { - if (alertViewAnimator != null && alertViewAnimator.equals(animation)) { - alertViewAnimator = null; - } + alertView.setVisibility(View.VISIBLE); + alertViewAnimator = new AnimatorSet(); + alertViewAnimator.playTogether(ObjectAnimator.ofFloat(alertView, "translationY", 0)); + alertViewAnimator.setDuration(200); + alertViewAnimator.addListener(new AnimatorListenerAdapterProxy() { + @Override + public void onAnimationEnd(Animator animation) { + if (alertViewAnimator != null && alertViewAnimator.equals(animation)) { + alertViewAnimator = null; } - }); - alertViewAnimator.start(); - } else { - ViewProxy.setTranslationY(alertView, 0); - alertView.clearAnimation(); - alertView.setVisibility(View.VISIBLE); - } + } + + @Override + public void onAnimationCancel(Animator animation) { + if (alertViewAnimator != null && alertViewAnimator.equals(animation)) { + alertViewAnimator = null; + } + } + }); + alertViewAnimator.start(); } alertNameTextView.setText(ContactsController.formatName(user.first_name, user.last_name)); alertTextView.setText(Emoji.replaceEmoji(message.replace('\n', ' '), alertTextView.getPaint().getFontMetricsInt(), AndroidUtilities.dp(14), false)); @@ -6174,33 +6284,26 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not alertViewAnimator.cancel(); alertViewAnimator = null; } - if (Build.VERSION.SDK_INT >= 11) { - alertViewAnimator = new AnimatorSetProxy(); - alertViewAnimator.playTogether(ObjectAnimatorProxy.ofFloat(alertView, "translationY", -AndroidUtilities.dp(50))); - alertViewAnimator.setDuration(200); - alertViewAnimator.addListener(new AnimatorListenerAdapterProxy() { - @Override - public void onAnimationEnd(Object animation) { - if (alertViewAnimator != null && alertViewAnimator.equals(animation)) { - alertView.clearAnimation(); - alertView.setVisibility(View.GONE); - alertViewAnimator = null; - } + alertViewAnimator = new AnimatorSet(); + alertViewAnimator.playTogether(ObjectAnimator.ofFloat(alertView, "translationY", -AndroidUtilities.dp(50))); + alertViewAnimator.setDuration(200); + alertViewAnimator.addListener(new AnimatorListenerAdapterProxy() { + @Override + public void onAnimationEnd(Animator animation) { + if (alertViewAnimator != null && alertViewAnimator.equals(animation)) { + alertView.setVisibility(View.GONE); + alertViewAnimator = null; } + } - @Override - public void onAnimationCancel(Object animation) { - if (alertViewAnimator != null && alertViewAnimator.equals(animation)) { - alertViewAnimator = null; - } + @Override + public void onAnimationCancel(Animator animation) { + if (alertViewAnimator != null && alertViewAnimator.equals(animation)) { + alertViewAnimator = null; } - }); - alertViewAnimator.start(); - } else { - ViewProxy.setTranslationY(alertView, -AndroidUtilities.dp(50)); - alertView.clearAnimation(); - alertView.setVisibility(View.GONE); - } + } + }); + alertViewAnimator.start(); } } }, 3000); @@ -6213,22 +6316,21 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not pinnedMessageViewAnimator.cancel(); pinnedMessageViewAnimator = null; } - if (Build.VERSION.SDK_INT >= 11 && animated) { - pinnedMessageViewAnimator = new AnimatorSetProxy(); - pinnedMessageViewAnimator.playTogether(ObjectAnimatorProxy.ofFloat(pinnedMessageView, "translationY", -AndroidUtilities.dp(50))); + if (animated) { + pinnedMessageViewAnimator = new AnimatorSet(); + pinnedMessageViewAnimator.playTogether(ObjectAnimator.ofFloat(pinnedMessageView, "translationY", -AndroidUtilities.dp(50))); pinnedMessageViewAnimator.setDuration(200); pinnedMessageViewAnimator.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (pinnedMessageViewAnimator != null && pinnedMessageViewAnimator.equals(animation)) { - pinnedMessageView.clearAnimation(); pinnedMessageView.setVisibility(View.GONE); pinnedMessageViewAnimator = null; } } @Override - public void onAnimationCancel(Object animation) { + public void onAnimationCancel(Animator animation) { if (pinnedMessageViewAnimator != null && pinnedMessageViewAnimator.equals(animation)) { pinnedMessageViewAnimator = null; } @@ -6236,8 +6338,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not }); pinnedMessageViewAnimator.start(); } else { - ViewProxy.setTranslationY(pinnedMessageView, -AndroidUtilities.dp(50)); - pinnedMessageView.clearAnimation(); + pinnedMessageView.setTranslationY(-AndroidUtilities.dp(50)); pinnedMessageView.setVisibility(View.GONE); } } @@ -6266,22 +6367,21 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not pinnedMessageViewAnimator.cancel(); pinnedMessageViewAnimator = null; } - if (Build.VERSION.SDK_INT >= 11 && animated) { + if (animated) { pinnedMessageView.setVisibility(View.VISIBLE); - pinnedMessageViewAnimator = new AnimatorSetProxy(); - pinnedMessageViewAnimator.playTogether(ObjectAnimatorProxy.ofFloat(pinnedMessageView, "translationY", 0)); + pinnedMessageViewAnimator = new AnimatorSet(); + pinnedMessageViewAnimator.playTogether(ObjectAnimator.ofFloat(pinnedMessageView, "translationY", 0)); pinnedMessageViewAnimator.setDuration(200); pinnedMessageViewAnimator.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (pinnedMessageViewAnimator != null && pinnedMessageViewAnimator.equals(animation)) { - pinnedMessageView.clearAnimation(); pinnedMessageViewAnimator = null; } } @Override - public void onAnimationCancel(Object animation) { + public void onAnimationCancel(Animator animation) { if (pinnedMessageViewAnimator != null && pinnedMessageViewAnimator.equals(animation)) { pinnedMessageViewAnimator = null; } @@ -6289,8 +6389,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not }); pinnedMessageViewAnimator.start(); } else { - ViewProxy.setTranslationY(pinnedMessageView, 0); - pinnedMessageView.clearAnimation(); + pinnedMessageView.setTranslationY(0); pinnedMessageView.setVisibility(View.VISIBLE); } } @@ -6337,66 +6436,57 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not if (!show) { if (reportSpamView.getTag() == null) { reportSpamView.setTag(1); - if (Build.VERSION.SDK_INT >= 11) { - if (reportSpamViewAnimator != null) { - reportSpamViewAnimator.cancel(); - } - reportSpamViewAnimator = new AnimatorSetProxy(); - reportSpamViewAnimator.playTogether(ObjectAnimatorProxy.ofFloat(reportSpamView, "translationY", -AndroidUtilities.dp(50))); - reportSpamViewAnimator.setDuration(200); - reportSpamViewAnimator.addListener(new AnimatorListenerAdapterProxy() { - @Override - public void onAnimationEnd(Object animation) { - if (reportSpamViewAnimator != null && reportSpamViewAnimator.equals(animation)) { - reportSpamView.clearAnimation(); - reportSpamView.setVisibility(View.GONE); - reportSpamViewAnimator = null; - } - } - @Override - public void onAnimationCancel(Object animation) { - if (reportSpamViewAnimator != null && reportSpamViewAnimator.equals(animation)) { - reportSpamViewAnimator = null; - } - } - }); - reportSpamViewAnimator.start(); - } else { - reportSpamView.setVisibility(View.GONE); + if (reportSpamViewAnimator != null) { + reportSpamViewAnimator.cancel(); } + reportSpamViewAnimator = new AnimatorSet(); + reportSpamViewAnimator.playTogether(ObjectAnimator.ofFloat(reportSpamView, "translationY", -AndroidUtilities.dp(50))); + reportSpamViewAnimator.setDuration(200); + reportSpamViewAnimator.addListener(new AnimatorListenerAdapterProxy() { + @Override + public void onAnimationEnd(Animator animation) { + if (reportSpamViewAnimator != null && reportSpamViewAnimator.equals(animation)) { + reportSpamView.setVisibility(View.GONE); + reportSpamViewAnimator = null; + } + } + + @Override + public void onAnimationCancel(Animator animation) { + if (reportSpamViewAnimator != null && reportSpamViewAnimator.equals(animation)) { + reportSpamViewAnimator = null; + } + } + }); + reportSpamViewAnimator.start(); } } else { if (reportSpamView.getTag() != null) { reportSpamView.setTag(null); - if (Build.VERSION.SDK_INT >= 11) { - reportSpamView.setVisibility(View.VISIBLE); - if (reportSpamViewAnimator != null) { - reportSpamViewAnimator.cancel(); - } - reportSpamViewAnimator = new AnimatorSetProxy(); - reportSpamViewAnimator.playTogether(ObjectAnimatorProxy.ofFloat(reportSpamView, "translationY", 0)); - reportSpamViewAnimator.setDuration(200); - reportSpamViewAnimator.addListener(new AnimatorListenerAdapterProxy() { - @Override - public void onAnimationEnd(Object animation) { - if (reportSpamViewAnimator != null && reportSpamViewAnimator.equals(animation)) { - reportSpamView.clearAnimation(); - reportSpamViewAnimator = null; - } - } - - @Override - public void onAnimationCancel(Object animation) { - if (reportSpamViewAnimator != null && reportSpamViewAnimator.equals(animation)) { - reportSpamViewAnimator = null; - } - } - }); - reportSpamViewAnimator.start(); - } else { - reportSpamView.setVisibility(View.VISIBLE); + reportSpamView.setVisibility(View.VISIBLE); + if (reportSpamViewAnimator != null) { + reportSpamViewAnimator.cancel(); } + reportSpamViewAnimator = new AnimatorSet(); + reportSpamViewAnimator.playTogether(ObjectAnimator.ofFloat(reportSpamView, "translationY", 0)); + reportSpamViewAnimator.setDuration(200); + reportSpamViewAnimator.addListener(new AnimatorListenerAdapterProxy() { + @Override + public void onAnimationEnd(Animator animation) { + if (reportSpamViewAnimator != null && reportSpamViewAnimator.equals(animation)) { + reportSpamViewAnimator = null; + } + } + + @Override + public void onAnimationCancel(Animator animation) { + if (reportSpamViewAnimator != null && reportSpamViewAnimator.equals(animation)) { + reportSpamViewAnimator = null; + } + } + }); + reportSpamViewAnimator.start(); } } checkListViewPaddings(); @@ -6470,7 +6560,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } private void checkRaiseSensors() { - if (!ApplicationLoader.mainInterfacePaused && (bottomOverlayChat == null || bottomOverlayChat.getVisibility() != View.VISIBLE) && (bottomOverlay == null || bottomOverlay.getVisibility() != View.VISIBLE)) { + if (!ApplicationLoader.mainInterfacePaused && (bottomOverlayChat == null || bottomOverlayChat.getVisibility() != View.VISIBLE) && (bottomOverlay == null || bottomOverlay.getVisibility() != View.VISIBLE) && (searchContainer == null || searchContainer.getVisibility() != View.VISIBLE)) { MediaController.getInstance().setAllowStartRecord(true); } else { MediaController.getInstance().setAllowStartRecord(false); @@ -6533,49 +6623,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } fixLayout(); - SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE); - if (chatActivityEnterView.getFieldText() == null) { - String lastMessageText = preferences.getString("dialog_" + dialog_id, null); - if (lastMessageText != null) { - preferences.edit().remove("dialog_" + dialog_id).commit(); - chatActivityEnterView.setFieldText(lastMessageText); - if (getArguments().getBoolean("hasUrl", false)) { - chatActivityEnterView.setSelection(lastMessageText.indexOf('\n') + 1); - AndroidUtilities.runOnUIThread(new Runnable() { - @Override - public void run() { - if (chatActivityEnterView != null) { - chatActivityEnterView.setFieldFocused(true); - chatActivityEnterView.openKeyboard(); - } - } - }, 700); - } - } - } else { - preferences.edit().remove("dialog_" + dialog_id).commit(); - } - if (replyingMessageObject == null) { - String lastReplyMessage = preferences.getString("reply_" + dialog_id, null); - if (lastReplyMessage != null && lastReplyMessage.length() != 0) { - preferences.edit().remove("reply_" + dialog_id).commit(); - try { - byte[] bytes = Base64.decode(lastReplyMessage, Base64.DEFAULT); - if (bytes != null) { - SerializedData data = new SerializedData(bytes); - TLRPC.Message message = TLRPC.Message.TLdeserialize(data, data.readInt32(false), false); - if (message != null) { - replyingMessageObject = new MessageObject(message, MessagesController.getInstance().getUsers(), false); - showReplyPanel(true, replyingMessageObject, null, null, false, false); - } - } - } catch (Exception e) { - FileLog.e("tmessages", e); - } - } - } else { - preferences.edit().remove("reply_" + dialog_id).commit(); - } + applyDraftMaybe(); if (bottomOverlayChat.getVisibility() != View.VISIBLE) { chatActivityEnterView.setFieldFocused(true); } @@ -6613,35 +6661,20 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not paused = true; wasPaused = true; NotificationsController.getInstance().setOpenedDialogId(0); + CharSequence draftMessage = null; + boolean searchWebpage = true; if (chatActivityEnterView != null) { chatActivityEnterView.onPause(); if (!chatActivityEnterView.isEditingMessage()) { - String text = chatActivityEnterView.getFieldText(); - if (text != null && !text.equals("@gif ")) { - SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE); - SharedPreferences.Editor editor = preferences.edit(); - editor.putString("dialog_" + dialog_id, text); - editor.commit(); + CharSequence text = AndroidUtilities.getTrimmedString(chatActivityEnterView.getFieldText()); + if (!TextUtils.isEmpty(text) && !TextUtils.equals(text, "@gif")) { + draftMessage = text; } } + searchWebpage = chatActivityEnterView.isMessageWebPageSearchEnabled(); chatActivityEnterView.setFieldFocused(false); } - if (replyingMessageObject != null) { - SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE); - SharedPreferences.Editor editor = preferences.edit(); - try { - SerializedData data = new SerializedData(); - replyingMessageObject.messageOwner.serializeToStream(data); - String string = Base64.encodeToString(data.toByteArray(), Base64.DEFAULT); - if (string.length() != 0) { - editor.putString("reply_" + dialog_id, string); - } - } catch (Exception e) { - editor.remove("reply_" + dialog_id); - FileLog.e("tmessages", e); - } - editor.commit(); - } + DraftQuery.saveDraft(dialog_id, draftMessage, MessagesQuery.getEntities(draftMessage), replyingMessageObject != null ? replyingMessageObject.messageOwner : null, !searchWebpage); MessagesController.getInstance().cancelTyping(0, dialog_id); @@ -6651,6 +6684,58 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } } + private void applyDraftMaybe() { + if (chatActivityEnterView == null) { + return; + } + TLRPC.DraftMessage draftMessage = DraftQuery.getDraft(dialog_id); + TLRPC.Message draftReplyMessage = draftMessage != null && draftMessage.reply_to_msg_id != 0 ? DraftQuery.getDraftMessage(dialog_id) : null; + if (chatActivityEnterView.getFieldText() == null) { + if (draftMessage != null) { + chatActivityEnterView.setWebPage(null, !draftMessage.no_webpage); + CharSequence message; + if (!draftMessage.entities.isEmpty()) { + SpannableStringBuilder stringBuilder = SpannableStringBuilder.valueOf(draftMessage.message); + for (int a = 0; a < draftMessage.entities.size(); a++) { + TLRPC.MessageEntity entity = draftMessage.entities.get(a); + if (entity instanceof TLRPC.TL_inputMessageEntityMentionName || entity instanceof TLRPC.TL_messageEntityMentionName) { + int user_id; + if (entity instanceof TLRPC.TL_inputMessageEntityMentionName) { + user_id = ((TLRPC.TL_inputMessageEntityMentionName) entity).user_id.user_id; + } else { + user_id = ((TLRPC.TL_messageEntityMentionName) entity).user_id; + } + if (entity.offset + entity.length < stringBuilder.length() && stringBuilder.charAt(entity.offset + entity.length) == ' ') { + entity.length++; + } + stringBuilder.setSpan(new URLSpanUserMention("" + user_id), entity.offset, entity.offset + entity.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + message = stringBuilder; + } else { + message = draftMessage.message; + } + chatActivityEnterView.setFieldText(message); + if (getArguments().getBoolean("hasUrl", false)) { + chatActivityEnterView.setSelection(draftMessage.message.indexOf('\n') + 1); + AndroidUtilities.runOnUIThread(new Runnable() { + @Override + public void run() { + if (chatActivityEnterView != null) { + chatActivityEnterView.setFieldFocused(true); + chatActivityEnterView.openKeyboard(); + } + } + }, 700); + } + } + } + if (replyingMessageObject == null && draftReplyMessage != null) { + replyingMessageObject = new MessageObject(draftReplyMessage, MessagesController.getInstance().getUsers(), false); + showReplyPanel(true, replyingMessageObject, null, null, false, false); + } + } + private void updateInformationForScreenshotDetector() { if (currentEncryptedChat == null) { return; @@ -6717,76 +6802,6 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not fixLayout(); } - private void switchImportantMode(MessageObject searchBeforeMessage) { - int count = chatListView.getChildCount(); - MessageObject messageObject = null; - if (searchBeforeMessage == null) { - for (int a = 0; a <= count; a++) { - View child = chatListView.getChildAt(a); - MessageObject message = null; - if (child instanceof ChatMessageCell) { - message = ((ChatMessageCell) child).getMessageObject(); - } else if (child instanceof ChatActionCell) { - message = ((ChatActionCell) child).getMessageObject(); - } - if (message != null && message.getId() > 0) { - if (message.isImportant()) { - messageObject = message; - break; - } else if (searchBeforeMessage == null) { - searchBeforeMessage = message; - } - } - } - } - - if (messageObject == null) { - int index = messages.indexOf(searchBeforeMessage); - if (index >= 0) { - for (int a = index + 1; a < messages.size(); a++) { - MessageObject message = messages.get(a); - if (message.getId() > 0 && message.isImportant()) { - messageObject = message; - break; - } - } - } - } - - if (messageObject != null) { - scrollToMessagePosition = -10000; - for (int a = 0; a < count; a++) { - View child = chatListView.getChildAt(a); - MessageObject message = null; - if (child instanceof ChatMessageCell) { - message = ((ChatMessageCell) child).getMessageObject(); - } else if (child instanceof ChatActionCell) { - message = ((ChatActionCell) child).getMessageObject(); - } - if (message == messageObject) { - scrollToMessagePosition = child.getTop() + AndroidUtilities.dp(7); - break; - } - } - if (scrollToMessagePosition == -10000) { - scrollToMessagePosition = chatListView.getPaddingTop(); - } - } - - avatarContainer.setRadioChecked(!avatarContainer.isRadioChecked(), true); - channelMessagesImportant = avatarContainer.isRadioChecked() ? 1 : 2; - SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE); - preferences.edit().putInt("important_" + dialog_id, channelMessagesImportant).commit(); - waitingForImportantLoad = true; - waitingForLoad.add(lastLoadIndex); - if (messageObject != null) { - startLoadFromMessageId = messageObject.getId(); - MessagesController.getInstance().loadMessages(dialog_id, AndroidUtilities.isTablet() ? 30 : 20, startLoadFromMessageId, true, 0, classGuid, 3, 0, channelMessagesImportant, lastLoadIndex++); - } else { - MessagesController.getInstance().loadMessages(dialog_id, 30, 0, true, 0, classGuid, 0, 0, channelMessagesImportant, lastLoadIndex++); - } - } - private void createDeleteMessagesAlert(final MessageObject finalSelectedObject) { AlertDialog.Builder builder = new AlertDialog.Builder(getParentActivity()); builder.setMessage(LocaleController.formatString("AreYouSureDeleteMessages", R.string.AreYouSureDeleteMessages, LocaleController.formatPluralString("messages", finalSelectedObject != null ? 1 : selectedMessagesIds[0].size() + selectedMessagesIds[1].size()))); @@ -6822,7 +6837,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } } if (user != null && user.id != UserConfig.getClientUserId()) { - FrameLayoutFixed frameLayout = new FrameLayoutFixed(getParentActivity()); + FrameLayout frameLayout = new FrameLayout(getParentActivity()); if (Build.VERSION.SDK_INT >= 21) { frameLayout.setPadding(0, AndroidUtilities.dp(8), 0, 0); } @@ -6830,9 +6845,6 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not CheckBoxCell cell = new CheckBoxCell(getParentActivity()); cell.setBackgroundResource(R.drawable.list_selector); cell.setTag(a); - if (Build.VERSION.SDK_INT < 11) { - cell.setTextColor(0xffffffff); - } if (a == 0) { cell.setText(LocaleController.getString("DeleteBanUser", R.string.DeleteBanUser), "", false, false); } else if (a == 1) { @@ -6937,10 +6949,6 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not return; } final int type = getMessageType(message); - if (channelMessagesImportant == 2 && message.getId() == 0 && message.contentType == 1 && message.type == 10 && message.messageOwner.from_id != 0) { - switchImportantMode(message); - return; - } if (single && message.messageOwner.action instanceof TLRPC.TL_messageActionPinMessage) { scrollToMessageId(message.messageOwner.reply_to_msg_id, 0, true, 0); return; @@ -7175,18 +7183,16 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not actionBar.showActionMode(); updatePinnedMessageView(true); - if (Build.VERSION.SDK_INT >= 11) { - AnimatorSetProxy animatorSet = new AnimatorSetProxy(); - ArrayList animators = new ArrayList<>(); - for (int a = 0; a < actionModeViews.size(); a++) { - View view = actionModeViews.get(a); - AndroidUtilities.clearDrawableAnimation(view); - animators.add(ObjectAnimatorProxy.ofFloat(view, "scaleY", 0.1f, 1.0f)); - } - animatorSet.playTogether(animators); - animatorSet.setDuration(250); - animatorSet.start(); + AnimatorSet animatorSet = new AnimatorSet(); + ArrayList animators = new ArrayList<>(); + for (int a = 0; a < actionModeViews.size(); a++) { + View view = actionModeViews.get(a); + AndroidUtilities.clearDrawableAnimation(view); + animators.add(ObjectAnimator.ofFloat(view, "scaleY", 0.1f, 1.0f)); } + animatorSet.playTogether(animators); + animatorSet.setDuration(250); + animatorSet.start(); addToSelectedMessages(message); selectedMessagesCountTextView.setNumber(1, false); @@ -7199,66 +7205,64 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } if (!animated) { if (show) { - ViewProxy.setScaleX(editDoneItem.getImageView(), 0.1f); - ViewProxy.setScaleY(editDoneItem.getImageView(), 0.1f); - ViewProxy.setAlpha(editDoneItem.getImageView(), 0.0f); - ViewProxy.setScaleX(editDoneItemProgress, 1.0f); - ViewProxy.setScaleY(editDoneItemProgress, 1.0f); - ViewProxy.setAlpha(editDoneItemProgress, 1.0f); + editDoneItem.getImageView().setScaleX(0.1f); + editDoneItem.getImageView().setScaleY(0.1f); + editDoneItem.getImageView().setAlpha(0.0f); + editDoneItemProgress.setScaleX(1.0f); + editDoneItemProgress.setScaleY(1.0f); + editDoneItemProgress.setAlpha(1.0f); editDoneItem.getImageView().setVisibility(View.INVISIBLE); editDoneItemProgress.setVisibility(View.VISIBLE); editDoneItem.setEnabled(false); } else { - ViewProxy.setScaleX(editDoneItemProgress, 0.1f); - ViewProxy.setScaleY(editDoneItemProgress, 0.1f); - ViewProxy.setAlpha(editDoneItemProgress, 0.0f); - ViewProxy.setScaleX(editDoneItem.getImageView(), 1.0f); - ViewProxy.setScaleY(editDoneItem.getImageView(), 1.0f); - ViewProxy.setAlpha(editDoneItem.getImageView(), 1.0f); + editDoneItemProgress.setScaleX(0.1f); + editDoneItemProgress.setScaleY(0.1f); + editDoneItemProgress.setAlpha(0.0f); + editDoneItem.getImageView().setScaleX(1.0f); + editDoneItem.getImageView().setScaleY(1.0f); + editDoneItem.getImageView().setAlpha(1.0f); editDoneItem.getImageView().setVisibility(View.VISIBLE); editDoneItemProgress.setVisibility(View.INVISIBLE); editDoneItem.setEnabled(true); } } else { - editDoneItemAnimation = new AnimatorSetProxy(); + editDoneItemAnimation = new AnimatorSet(); if (show) { editDoneItemProgress.setVisibility(View.VISIBLE); editDoneItem.setEnabled(false); editDoneItemAnimation.playTogether( - ObjectAnimatorProxy.ofFloat(editDoneItem.getImageView(), "scaleX", 0.1f), - ObjectAnimatorProxy.ofFloat(editDoneItem.getImageView(), "scaleY", 0.1f), - ObjectAnimatorProxy.ofFloat(editDoneItem.getImageView(), "alpha", 0.0f), - ObjectAnimatorProxy.ofFloat(editDoneItemProgress, "scaleX", 1.0f), - ObjectAnimatorProxy.ofFloat(editDoneItemProgress, "scaleY", 1.0f), - ObjectAnimatorProxy.ofFloat(editDoneItemProgress, "alpha", 1.0f)); + ObjectAnimator.ofFloat(editDoneItem.getImageView(), "scaleX", 0.1f), + ObjectAnimator.ofFloat(editDoneItem.getImageView(), "scaleY", 0.1f), + ObjectAnimator.ofFloat(editDoneItem.getImageView(), "alpha", 0.0f), + ObjectAnimator.ofFloat(editDoneItemProgress, "scaleX", 1.0f), + ObjectAnimator.ofFloat(editDoneItemProgress, "scaleY", 1.0f), + ObjectAnimator.ofFloat(editDoneItemProgress, "alpha", 1.0f)); } else { editDoneItem.getImageView().setVisibility(View.VISIBLE); editDoneItem.setEnabled(true); editDoneItemAnimation.playTogether( - ObjectAnimatorProxy.ofFloat(editDoneItemProgress, "scaleX", 0.1f), - ObjectAnimatorProxy.ofFloat(editDoneItemProgress, "scaleY", 0.1f), - ObjectAnimatorProxy.ofFloat(editDoneItemProgress, "alpha", 0.0f), - ObjectAnimatorProxy.ofFloat(editDoneItem.getImageView(), "scaleX", 1.0f), - ObjectAnimatorProxy.ofFloat(editDoneItem.getImageView(), "scaleY", 1.0f), - ObjectAnimatorProxy.ofFloat(editDoneItem.getImageView(), "alpha", 1.0f)); + ObjectAnimator.ofFloat(editDoneItemProgress, "scaleX", 0.1f), + ObjectAnimator.ofFloat(editDoneItemProgress, "scaleY", 0.1f), + ObjectAnimator.ofFloat(editDoneItemProgress, "alpha", 0.0f), + ObjectAnimator.ofFloat(editDoneItem.getImageView(), "scaleX", 1.0f), + ObjectAnimator.ofFloat(editDoneItem.getImageView(), "scaleY", 1.0f), + ObjectAnimator.ofFloat(editDoneItem.getImageView(), "alpha", 1.0f)); } editDoneItemAnimation.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (editDoneItemAnimation != null && editDoneItemAnimation.equals(animation)) { if (!show) { - editDoneItemProgress.clearAnimation(); editDoneItemProgress.setVisibility(View.INVISIBLE); } else { - editDoneItem.getImageView().clearAnimation(); editDoneItem.getImageView().setVisibility(View.INVISIBLE); } } } @Override - public void onAnimationCancel(Object animation) { + public void onAnimationCancel(Animator animation) { if (editDoneItemAnimation != null && editDoneItemAnimation.equals(animation)) { editDoneItemAnimation = null; } @@ -7459,6 +7463,10 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not selectedObject = null; return; } + if (searchItem != null && actionBar.isSearchFieldVisible()) { + actionBar.closeSearchField(); + chatActivityEnterView.setFieldFocused(); + } mentionsAdapter.setNeedBotContext(false); chatListView.setOnItemLongClickListener(null); @@ -7530,9 +7538,6 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } CheckBoxCell cell = new CheckBoxCell(getParentActivity()); cell.setBackgroundResource(R.drawable.list_selector); - if (Build.VERSION.SDK_INT < 11) { - cell.setTextColor(0xffffffff); - } cell.setText(LocaleController.getString("PinNotify", R.string.PinNotify), "", true, false); cell.setPadding(LocaleController.isRTL ? AndroidUtilities.dp(8) : 0, 0, LocaleController.isRTL ? 0 : AndroidUtilities.dp(8), 0); frameLayout.addView(cell, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 48, Gravity.TOP | Gravity.LEFT, 8, 0, 8, 0)); @@ -7710,6 +7715,11 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not cell.setMessageObject(cell.getMessageObject()); cell.setCheckPressed(!disableSelection, disableSelection && selected); cell.setHighlighted(highlightMessageId != Integer.MAX_VALUE && cell.getMessageObject() != null && cell.getMessageObject().getId() == highlightMessageId); + if (searchContainer != null && searchContainer.getVisibility() == View.VISIBLE && MessagesSearchQuery.getLastSearchQuery() != null) { + cell.setHighlightedText(MessagesSearchQuery.getLastSearchQuery()); + } else { + cell.setHighlightedText(null); + } } else if (view instanceof ChatActionCell) { ChatActionCell cell = (ChatActionCell) view; cell.setMessageObject(cell.getMessageObject()); @@ -7784,12 +7794,9 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not avatarContainer.setVisibility(View.GONE); headerItem.setVisibility(View.GONE); attachItem.setVisibility(View.GONE); - if (searchUpItem.getVisibility() != View.VISIBLE) { - searchItem.setVisibility(View.VISIBLE); - searchUpItem.setVisibility(View.VISIBLE); - searchDownItem.setVisibility(View.VISIBLE); - } - updateSearchButtons(0); + searchItem.setVisibility(View.VISIBLE); + updateSearchButtons(0, 0, 0); + updateBottomOverlay(); openSearchKeyboard = text == null; searchItem.openSearch(openSearchKeyboard); if (text != null) { @@ -7826,32 +7833,40 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not @Override public PhotoViewer.PlaceProviderObject getPlaceForPhoto(MessageObject messageObject, TLRPC.FileLocation fileLocation, int index) { - if (messageObject == null) { - return null; - } int count = chatListView.getChildCount(); for (int a = 0; a < count; a++) { - MessageObject messageToOpen = null; ImageReceiver imageReceiver = null; View view = chatListView.getChildAt(a); if (view instanceof ChatMessageCell) { - ChatMessageCell cell = (ChatMessageCell) view; - MessageObject message = cell.getMessageObject(); - if (message != null && message.getId() == messageObject.getId()) { - messageToOpen = message; - imageReceiver = cell.getPhotoImage(); + if (messageObject != null) { + ChatMessageCell cell = (ChatMessageCell) view; + MessageObject message = cell.getMessageObject(); + if (message != null && message.getId() == messageObject.getId()) { + imageReceiver = cell.getPhotoImage(); + } } } else if (view instanceof ChatActionCell) { ChatActionCell cell = (ChatActionCell) view; MessageObject message = cell.getMessageObject(); - if (message != null && message.getId() == messageObject.getId()) { - messageToOpen = message; - imageReceiver = cell.getPhotoImage(); + if (message != null) { + if (messageObject != null) { + if (message.getId() == messageObject.getId()) { + imageReceiver = cell.getPhotoImage(); + } + } else if (fileLocation != null && message.photoThumbs != null) { + for (int b = 0; b < message.photoThumbs.size(); b++) { + TLRPC.PhotoSize photoSize = message.photoThumbs.get(b); + if (photoSize.location.volume_id == fileLocation.volume_id && photoSize.location.local_id == fileLocation.local_id) { + imageReceiver = cell.getPhotoImage(); + break; + } + } + } } } - if (messageToOpen != null) { + if (imageReceiver != null) { int coords[] = new int[2]; view.getLocationInWindow(coords); PhotoViewer.PlaceProviderObject object = new PhotoViewer.PlaceProviderObject(); @@ -7861,6 +7876,9 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not object.imageReceiver = imageReceiver; object.thumb = imageReceiver.getBitmap(); object.radius = imageReceiver.getRoundRadius(); + if (view instanceof ChatActionCell && currentChat != null) { + object.dialogId = -currentChat.id; + } if (pinnedMessageView != null && pinnedMessageView.getTag() == null || reportSpamView != null && reportSpamView.getTag() == null) { object.clipTopAddition = AndroidUtilities.dp(48); } @@ -7908,11 +7926,13 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not public void sendPhoto(MediaController.PhotoEntry photoEntry) { if (photoEntry.imagePath != null) { - SendMessagesHelper.prepareSendingPhoto(photoEntry.imagePath, null, dialog_id, replyingMessageObject, photoEntry.caption, chatActivityEnterView == null || chatActivityEnterView.asAdmin()); + SendMessagesHelper.prepareSendingPhoto(photoEntry.imagePath, null, dialog_id, replyingMessageObject, photoEntry.caption); showReplyPanel(false, null, null, null, false, true); + DraftQuery.cleanDraft(dialog_id, true); } else if (photoEntry.path != null) { - SendMessagesHelper.prepareSendingPhoto(photoEntry.path, null, dialog_id, replyingMessageObject, photoEntry.caption, chatActivityEnterView == null || chatActivityEnterView.asAdmin()); + SendMessagesHelper.prepareSendingPhoto(photoEntry.path, null, dialog_id, replyingMessageObject, photoEntry.caption); showReplyPanel(false, null, null, null, false, true); + DraftQuery.cleanDraft(dialog_id, true); } } @@ -8172,10 +8192,6 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not @Override public void didPressedReplyMessage(ChatMessageCell cell, int id) { MessageObject messageObject = cell.getMessageObject(); - if (messageObject.replyMessageObject != null && !messageObject.replyMessageObject.isImportant() && channelMessagesImportant == 2) { - channelMessagesImportant = 1; - avatarContainer.setRadioChecked(true, false); - } scrollToMessageId(id, messageObject.getId(), true, messageObject.getDialogId() == mergeDialogId ? 1 : 0); } @@ -8201,7 +8217,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not } if (message.type == 13) { showDialog(new StickersAlert(getParentActivity(), message.getInputStickerSet(), null, bottomOverlayChat.getVisibility() != View.VISIBLE ? chatActivityEnterView : null)); - } else if (message.type == 1 || message.type == 0 && !message.isWebpageDocument()) { + } else if (Build.VERSION.SDK_INT >= 16 && message.isVideo() || message.type == 1 || message.type == 0 && !message.isWebpageDocument() || message.isGif()) { PhotoViewer.getInstance().setParentActivity(getParentActivity()); PhotoViewer.getInstance().openPhoto(message, message.type != 0 ? dialog_id : 0, message.type != 0 ? mergeDialogId : 0, ChatActivity.this); } else if (message.type == 3) { @@ -8228,52 +8244,10 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not fragment.setMessageObject(message); presentFragment(fragment); } else if (message.type == 9 || message.type == 0) { - File f = null; - String fileName = message.getFileName(); - if (message.messageOwner.attachPath != null && message.messageOwner.attachPath.length() != 0) { - f = new File(message.messageOwner.attachPath); - } - if (f == null || !f.exists()) { - f = FileLoader.getPathToMessage(message.messageOwner); - } - if (f != null && f.exists()) { - String realMimeType = null; - try { - Intent intent = new Intent(Intent.ACTION_VIEW); - MimeTypeMap myMime = MimeTypeMap.getSingleton(); - int idx = fileName.lastIndexOf('.'); - if (idx != -1) { - String ext = fileName.substring(idx + 1); - realMimeType = myMime.getMimeTypeFromExtension(ext.toLowerCase()); - if (realMimeType == null) { - if (message.type == 9 || message.type == 0) { - realMimeType = message.getDocument().mime_type; - } - if (realMimeType == null || realMimeType.length() == 0) { - realMimeType = null; - } - } - if (realMimeType != null) { - intent.setDataAndType(Uri.fromFile(f), realMimeType); - } else { - intent.setDataAndType(Uri.fromFile(f), "text/plain"); - } - } else { - intent.setDataAndType(Uri.fromFile(f), "text/plain"); - } - if (realMimeType != null) { - try { - getParentActivity().startActivityForResult(intent, 500); - } catch (Exception e) { - intent.setDataAndType(Uri.fromFile(f), "text/plain"); - getParentActivity().startActivityForResult(intent, 500); - } - } else { - getParentActivity().startActivityForResult(intent, 500); - } - } catch (Exception e) { - alertUserOpenError(message); - } + try { + AndroidUtilities.openForView(message, getParentActivity()); + } catch (Exception e) { + alertUserOpenError(message); } } } @@ -8288,7 +8262,12 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not public void didClickedImage(ChatActionCell cell) { MessageObject message = cell.getMessageObject(); PhotoViewer.getInstance().setParentActivity(getParentActivity()); - PhotoViewer.getInstance().openPhoto(message, 0, 0, ChatActivity.this); + TLRPC.PhotoSize photoSize = FileLoader.getClosestPhotoSizeWithSize(message.photoThumbs, 640); + if (photoSize != null) { + PhotoViewer.getInstance().openPhoto(photoSize.location, ChatActivity.this); + } else { + PhotoViewer.getInstance().openPhoto(message, 0, 0, ChatActivity.this); + } } @Override @@ -8377,6 +8356,11 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not ((ChatMessageCell) view).downloadAudioIfNeed(); } messageCell.setHighlighted(highlightMessageId != Integer.MAX_VALUE && message.getId() == highlightMessageId); + if (searchContainer != null && searchContainer.getVisibility() == View.VISIBLE && MessagesSearchQuery.getLastSearchQuery() != null) { + messageCell.setHighlightedText(MessagesSearchQuery.getLastSearchQuery()); + } else { + messageCell.setHighlightedText(null); + } } else if (view instanceof ChatActionCell) { ChatActionCell actionCell = (ChatActionCell) view; actionCell.setMessageObject(message); @@ -8405,8 +8389,17 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not @Override public boolean onPreDraw() { messageCell.getViewTreeObserver().removeOnPreDrawListener(this); - messageCell.getLocalVisibleRect(scrollRect); - messageCell.setVisiblePart(scrollRect.top, scrollRect.bottom - scrollRect.top); + + int height = chatListView.getMeasuredHeight(); + int top = messageCell.getTop(); + int bottom = messageCell.getBottom(); + int viewTop = top >= 0 ? 0 : -top; + int viewBottom = messageCell.getMeasuredHeight(); + if (viewBottom > height) { + viewBottom = viewTop + height; + } + messageCell.setVisiblePart(viewTop, viewBottom - viewTop); + return true; } }); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/AlertsCreator.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/AlertsCreator.java index c9c17d729..93ffa940c 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/AlertsCreator.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/AlertsCreator.java @@ -74,7 +74,7 @@ public class AlertsCreator { NotificationsController.getInstance().removeNotificationsForDialog(dialog_id); MessagesStorage.getInstance().setDialogFlags(dialog_id, flags); editor.commit(); - TLRPC.Dialog dialog = MessagesController.getInstance().dialogs_dict.get(dialog_id); + TLRPC.TL_dialog dialog = MessagesController.getInstance().dialogs_dict.get(dialog_id); if (dialog != null) { dialog.notify_settings = new TLRPC.TL_peerNotifySettings(); dialog.notify_settings.mute_until = untilTime; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/AnimatedFileDrawable.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/AnimatedFileDrawable.java index 649694d7e..90a639fb4 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/AnimatedFileDrawable.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/AnimatedFileDrawable.java @@ -47,6 +47,7 @@ public class AnimatedFileDrawable extends BitmapDrawable implements Animatable { private boolean destroyWhenDone; private boolean decoderCreated; private File path; + private boolean recycleWithSecond; private BitmapShader renderingShader; private BitmapShader nextRenderingShader; @@ -68,11 +69,14 @@ public class AnimatedFileDrawable extends BitmapDrawable implements Animatable { private static ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(2, new ThreadPoolExecutor.DiscardPolicy()); private View parentView = null; + private View secondParentView = null; protected final Runnable mInvalidateTask = new Runnable() { @Override public void run() { - if (parentView != null) { + if (secondParentView != null) { + secondParentView.invalidate(); + } else if (parentView != null) { parentView.invalidate(); } } @@ -102,7 +106,9 @@ public class AnimatedFileDrawable extends BitmapDrawable implements Animatable { invalidateAfter = metaData[2] - lastTimeStamp; } lastTimeStamp = metaData[2]; - if (parentView != null) { + if (secondParentView != null) { + secondParentView.invalidate(); + } else if (parentView != null) { parentView.invalidate(); } } @@ -141,7 +147,9 @@ public class AnimatedFileDrawable extends BitmapDrawable implements Animatable { private final Runnable mStartTask = new Runnable() { @Override public void run() { - if (parentView != null) { + if (secondParentView != null) { + secondParentView.invalidate(); + } else if (parentView != null) { parentView.invalidate(); } } @@ -163,7 +171,18 @@ public class AnimatedFileDrawable extends BitmapDrawable implements Animatable { parentView = view; } + public void setSecondParentView(View view) { + secondParentView = view; + if (view == null && recycleWithSecond) { + recycle(); + } + } + public void recycle() { + if (secondParentView != null) { + recycleWithSecond = true; + return; + } isRunning = false; isRecycled = true; if (loadFrameTask == null) { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatActivityEnterView.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatActivityEnterView.java index 902b93cad..0da8100e2 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatActivityEnterView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatActivityEnterView.java @@ -9,6 +9,9 @@ package org.telegram.ui.Components; import android.Manifest; +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; import android.annotation.SuppressLint; import android.app.Activity; import android.app.AlertDialog; @@ -27,7 +30,6 @@ import android.os.SystemClock; import android.text.Editable; import android.text.InputFilter; import android.text.Layout; -import android.text.Spannable; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.StaticLayout; @@ -64,15 +66,13 @@ import org.telegram.messenger.SendMessagesHelper; import org.telegram.messenger.FileLog; import org.telegram.messenger.NotificationCenter; import org.telegram.messenger.R; -import org.telegram.messenger.Utilities; +import org.telegram.messenger.query.DraftQuery; +import org.telegram.messenger.query.MessagesQuery; import org.telegram.tgnet.ConnectionsManager; import org.telegram.tgnet.TLRPC; import org.telegram.messenger.UserConfig; import org.telegram.ui.ActionBar.ActionBar; -import org.telegram.messenger.AnimationCompat.AnimatorListenerAdapterProxy; -import org.telegram.messenger.AnimationCompat.AnimatorSetProxy; -import org.telegram.messenger.AnimationCompat.ObjectAnimatorProxy; -import org.telegram.messenger.AnimationCompat.ViewProxy; +import org.telegram.messenger.AnimatorListenerAdapterProxy; import org.telegram.messenger.ApplicationLoader; import org.telegram.ui.ActionBar.Theme; import org.telegram.ui.ChatActivity; @@ -84,7 +84,7 @@ import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Locale; -public class ChatActivityEnterView extends FrameLayoutFixed implements NotificationCenter.NotificationCenterDelegate, SizeNotifierFrameLayout.SizeNotifierFrameLayoutDelegate, StickersAlert.StickersAlertDelegate { +public class ChatActivityEnterView extends FrameLayout implements NotificationCenter.NotificationCenterDelegate, SizeNotifierFrameLayout.SizeNotifierFrameLayoutDelegate, StickersAlert.StickersAlertDelegate { public interface ChatActivityEnterViewDelegate { void onMessageSend(CharSequence message); @@ -200,7 +200,14 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat @SuppressLint("DrawAllocation") @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); + try { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } catch (Exception e) { + setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), AndroidUtilities.dp(51)); + FileLog.e("tmessages", e); + } + + captionLayout = null; if (caption != null && caption.length() > 0) { @@ -269,7 +276,12 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat showPopup(AndroidUtilities.usingHardwareInput ? 0 : 2, 0); openKeyboardInternal(); } - return super.onTouchEvent(event); + try { + return super.onTouchEvent(event); + } catch (Exception e) { + FileLog.e("tmessages", e); + } + return false; } } @@ -295,10 +307,10 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat private View topView; private PopupWindow botKeyboardPopup; private BotKeyboardView botKeyboardView; - private ImageView asAdminButton; private ImageView notifyButton; private RecordCircle recordCircle; private CloseProgressDrawable2 progressDrawable; + private Drawable backgroundDrawable; private MessageObject editingMessageObject; private int editingMessageReqId; @@ -309,10 +321,7 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat private boolean silent; private boolean canWriteToChannel; - private boolean isAsAdmin; - private boolean adminModeAvailable; - - private boolean isPaused; + private boolean isPaused = true; private boolean showKeyboardOnResume; private MessageObject botButtonsMessageObject; @@ -321,9 +330,9 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat private boolean hasBotCommands; private PowerManager.WakeLock mWakeLock; - private AnimatorSetProxy runningAnimation; - private AnimatorSetProxy runningAnimation2; - private AnimatorSetProxy runningAnimationAudio; + private AnimatorSet runningAnimation; + private AnimatorSet runningAnimation2; + private AnimatorSet runningAnimationAudio; private int runningAnimationType; private int audioInterfaceState; @@ -359,11 +368,10 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat private String audioToSendPath; private MessageObject audioToSendMessageObject; - private float topViewAnimation; private boolean topViewShowed; private boolean needShowTopView; private boolean allowShowTopView; - private AnimatorSetProxy currentTopViewAnimation; + private AnimatorSet currentTopViewAnimation; private MessageObject pendingMessageObject; private TLRPC.KeyboardButton pendingLocationButton; @@ -501,9 +509,10 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat public ChatActivityEnterView(Activity context, SizeNotifierFrameLayout parent, ChatActivity fragment, boolean isChat) { super(context); - setBackgroundResource(R.drawable.compose_panel); + backgroundDrawable = context.getResources().getDrawable(R.drawable.compose_panel); setFocusable(true); setFocusableInTouchMode(true); + setWillNotDraw(false); NotificationCenter.getInstance().addObserver(this, NotificationCenter.recordStarted); NotificationCenter.getInstance().addObserver(this, NotificationCenter.recordStartError); @@ -523,11 +532,11 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat sendByEnter = preferences.getBoolean("send_by_enter", false); textFieldContainer = new LinearLayout(context); - textFieldContainer.setBackgroundColor(0xffffffff); + //textFieldContainer.setBackgroundColor(0xffffffff); textFieldContainer.setOrientation(LinearLayout.HORIZONTAL); addView(textFieldContainer, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, Gravity.LEFT | Gravity.TOP, 0, 2, 0, 0)); - FrameLayoutFixed frameLayout = new FrameLayoutFixed(context); + FrameLayout frameLayout = new FrameLayout(context); textFieldContainer.addView(frameLayout, LayoutHelper.createLinear(0, LayoutHelper.WRAP_CONTENT, 1.0f)); emojiButton = new ImageView(context); @@ -624,7 +633,7 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat return; } checkSendButton(true); - CharSequence message = getTrimmedString(charSequence.toString()); + CharSequence message = AndroidUtilities.getTrimmedString(charSequence.toString()); if (delegate != null) { if (count > 2 || charSequence == null || charSequence.length() == 0) { messageWebPageSearch = true; @@ -636,7 +645,7 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat if (innerTextChange != 2 && before != count && (count - before) > 1) { processChange = true; } - if (editingMessageObject == null && !isAsAdmin && message.length() != 0 && lastTypingTimeSend < System.currentTimeMillis() - 5000 && !ignoreTextChange) { + if (editingMessageObject == null && !canWriteToChannel && message.length() != 0 && lastTypingTimeSend < System.currentTimeMillis() - 5000 && !ignoreTextChange) { int currentTime = ConnectionsManager.getInstance().getCurrentTime(); TLRPC.User currentUser = null; if ((int) dialog_id > 0) { @@ -682,7 +691,7 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat attachButton = new LinearLayout(context); attachButton.setOrientation(LinearLayout.HORIZONTAL); attachButton.setEnabled(false); - ViewProxy.setPivotX(attachButton, AndroidUtilities.dp(48)); + attachButton.setPivotX(AndroidUtilities.dp(48)); frameLayout.addView(attachButton, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, 48, Gravity.BOTTOM | Gravity.RIGHT)); botButton = new ImageView(context); @@ -715,25 +724,6 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat } }); - asAdminButton = new ImageView(context); - asAdminButton.setImageResource(isAsAdmin ? R.drawable.publish_active : R.drawable.publish); - asAdminButton.setScaleType(ImageView.ScaleType.CENTER); - asAdminButton.setVisibility(adminModeAvailable ? VISIBLE : GONE); - if (Build.VERSION.SDK_INT >= 21) { - asAdminButton.setBackgroundDrawable(Theme.createBarSelectorDrawable(Theme.INPUT_FIELD_SELECTOR_COLOR)); - } - attachButton.addView(asAdminButton, LayoutHelper.createLinear(48, 48)); - asAdminButton.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - isAsAdmin = !isAsAdmin; - asAdminButton.setImageResource(isAsAdmin ? R.drawable.publish_active : R.drawable.publish); - updateFieldHint(); - SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE); - preferences.edit().putBoolean("asadmin_" + dialog_id, isAsAdmin).commit(); - } - }); - notifyButton = new ImageView(context); notifyButton.setImageResource(silent ? R.drawable.notify_members_off : R.drawable.notify_members_on); notifyButton.setScaleType(ImageView.ScaleType.CENTER); @@ -759,7 +749,7 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat }); } - recordedAudioPanel = new FrameLayoutFixed(context); + recordedAudioPanel = new FrameLayout(context); recordedAudioPanel.setVisibility(audioToSend == null ? GONE : VISIBLE); recordedAudioPanel.setBackgroundColor(0xffffffff); recordedAudioPanel.setFocusable(true); @@ -819,7 +809,7 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat recordedAudioTimeTextView.setText("0:13"); recordedAudioPanel.addView(recordedAudioTimeTextView, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.RIGHT | Gravity.CENTER_VERTICAL, 0, 0, 13, 0)); - recordPanel = new FrameLayoutFixed(context); + recordPanel = new FrameLayout(context); recordPanel.setVisibility(GONE); recordPanel.setBackgroundColor(0xffffffff); frameLayout.addView(recordPanel, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 48, Gravity.BOTTOM)); @@ -890,7 +880,7 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat } } startedDraggingX = -1; - MediaController.getInstance().startRecording(dialog_id, replyingMessageObject, asAdmin()); + MediaController.getInstance().startRecording(dialog_id, replyingMessageObject); updateAudioRecordIntefrace(); audioSendButton.getParent().requestDisallowInterceptTouchEvent(true); } else if (motionEvent.getAction() == MotionEvent.ACTION_UP || motionEvent.getAction() == MotionEvent.ACTION_CANCEL) { @@ -906,11 +896,11 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat updateAudioRecordIntefrace(); } - x = x + ViewProxy.getX(audioSendButton); + x = x + audioSendButton.getX(); FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) slideText.getLayoutParams(); if (startedDraggingX != -1) { float dist = (x - startedDraggingX); - ViewProxy.setTranslationX(recordCircle, dist); + recordCircle.setTranslationX(dist); params.leftMargin = AndroidUtilities.dp(30) + (int) dist; slideText.setLayoutParams(params); float alpha = 1.0f + dist / distCanMove; @@ -919,9 +909,9 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat } else if (alpha < 0) { alpha = 0; } - ViewProxy.setAlpha(slideText, alpha); + slideText.setAlpha(alpha); } - if (x <= ViewProxy.getX(slideText) + slideText.getWidth() + AndroidUtilities.dp(30)) { + if (x <= slideText.getX() + slideText.getWidth() + AndroidUtilities.dp(30)) { if (startedDraggingX == -1) { startedDraggingX = x; distCanMove = (recordPanel.getMeasuredWidth() - slideText.getMeasuredWidth() - AndroidUtilities.dp(48)) / 2.0f; @@ -934,9 +924,9 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat } if (params.leftMargin > AndroidUtilities.dp(30)) { params.leftMargin = AndroidUtilities.dp(30); - ViewProxy.setTranslationX(recordCircle, 0); + recordCircle.setTranslationX(0); slideText.setLayoutParams(params); - ViewProxy.setAlpha(slideText, 1); + slideText.setAlpha(1); startedDraggingX = -1; } } @@ -955,10 +945,9 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat //cancelBotButton.setImageResource(R.drawable.delete_reply); cancelBotButton.setImageDrawable(progressDrawable = new CloseProgressDrawable2()); cancelBotButton.setSoundEffectsEnabled(false); - ViewProxy.setScaleX(cancelBotButton, 0.1f); - ViewProxy.setScaleY(cancelBotButton, 0.1f); - ViewProxy.setAlpha(cancelBotButton, 0.0f); - cancelBotButton.clearAnimation(); + cancelBotButton.setScaleX(0.1f); + cancelBotButton.setScaleY(0.1f); + cancelBotButton.setAlpha(0.0f); sendButtonContainer.addView(cancelBotButton, LayoutHelper.createFrame(48, 48)); cancelBotButton.setOnClickListener(new OnClickListener() { @Override @@ -978,10 +967,9 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat sendButton.setScaleType(ImageView.ScaleType.CENTER_INSIDE); sendButton.setImageResource(R.drawable.ic_send); sendButton.setSoundEffectsEnabled(false); - ViewProxy.setScaleX(sendButton, 0.1f); - ViewProxy.setScaleY(sendButton, 0.1f); - ViewProxy.setAlpha(sendButton, 0.0f); - sendButton.clearAnimation(); + sendButton.setScaleX(0.1f); + sendButton.setScaleY(0.1f); + sendButton.setAlpha(0.0f); sendButtonContainer.addView(sendButton, LayoutHelper.createFrame(48, 48)); sendButton.setOnClickListener(new OnClickListener() { @Override @@ -997,6 +985,31 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat checkSendButton(false); } + @Override + protected boolean drawChild(Canvas canvas, View child, long drawingTime) { + if (child == topView) { + canvas.save(); + canvas.clipRect(0, 0, getMeasuredWidth(), child.getLayoutParams().height + AndroidUtilities.dp(2)); + } + boolean result = super.drawChild(canvas, child, drawingTime); + if (child == topView) { + canvas.restore(); + } + return result; + } + + @Override + protected void onDraw(Canvas canvas) { + int top = topView != null && topView.getVisibility() == VISIBLE ? (int) topView.getTranslationY() : 0; + backgroundDrawable.setBounds(0, top, getMeasuredWidth(), getMeasuredHeight()); + backgroundDrawable.draw(canvas); + } + + @Override + public boolean hasOverlappingRendering() { + return false; + } + public void showContextProgress(boolean show) { if (progressDrawable == null) { return; @@ -1021,21 +1034,11 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat } topView = view; topView.setVisibility(GONE); + topView.setTranslationY(height); addView(topView, 0, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, height, Gravity.TOP | Gravity.LEFT, 0, 2, 0, 0)); needShowTopView = false; } - public void setTopViewAnimation(float progress) { - topViewAnimation = progress; - LayoutParams layoutParams2 = (LayoutParams) textFieldContainer.getLayoutParams(); - layoutParams2.topMargin = AndroidUtilities.dp(2) + (int) (topView.getLayoutParams().height * progress); - textFieldContainer.setLayoutParams(layoutParams2); - } - - public float getTopViewAnimation() { - return topViewAnimation; - } - public void setForceShowSendButton(boolean value, boolean animated) { forceShowSendButton = value; checkSendButton(animated); @@ -1059,10 +1062,6 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat emojiView.switchToGifRecent(); } - public boolean asAdmin() { - return isAsAdmin; - } - public void showTopView(boolean animated, final boolean openKeyboard) { if (topView == null || topViewShowed || getVisibility() != VISIBLE) { return; @@ -1075,17 +1074,15 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat currentTopViewAnimation.cancel(); currentTopViewAnimation = null; } + resizeForTopView(true); if (animated) { if (keyboardVisible || isPopupShowing()) { - currentTopViewAnimation = new AnimatorSetProxy(); - currentTopViewAnimation.playTogether( - ObjectAnimatorProxy.ofFloat(ChatActivityEnterView.this, "topViewAnimation", 1.0f) - ); + currentTopViewAnimation = new AnimatorSet(); + currentTopViewAnimation.playTogether(ObjectAnimator.ofFloat(topView, "translationY", 0)); currentTopViewAnimation.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (currentTopViewAnimation != null && currentTopViewAnimation.equals(animation)) { - setTopViewAnimation(1.0f); if (recordedAudioPanel.getVisibility() != VISIBLE && (!forceShowSendButton || openKeyboard)) { openKeyboard(); } @@ -1094,7 +1091,7 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat } @Override - public void onAnimationCancel(Object animation) { + public void onAnimationCancel(Animator animation) { if (currentTopViewAnimation != null && currentTopViewAnimation.equals(animation)) { currentTopViewAnimation = null; } @@ -1103,13 +1100,13 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat currentTopViewAnimation.setDuration(200); currentTopViewAnimation.start(); } else { - setTopViewAnimation(1.0f); + topView.setTranslationY(0); if (recordedAudioPanel.getVisibility() != VISIBLE && (!forceShowSendButton || openKeyboard)) { openKeyboard(); } } } else { - setTopViewAnimation(1.0f); + topView.setTranslationY(0); } } } @@ -1122,29 +1119,25 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat topViewShowed = false; needShowTopView = false; if (allowShowTopView) { - float resumeValue = 1.0f; if (currentTopViewAnimation != null) { - resumeValue = topViewAnimation; currentTopViewAnimation.cancel(); currentTopViewAnimation = null; } if (animated) { - currentTopViewAnimation = new AnimatorSetProxy(); - currentTopViewAnimation.playTogether( - ObjectAnimatorProxy.ofFloat(ChatActivityEnterView.this, "topViewAnimation", resumeValue, 0.0f) - ); + currentTopViewAnimation = new AnimatorSet(); + currentTopViewAnimation.playTogether(ObjectAnimator.ofFloat(topView, "translationY", topView.getLayoutParams().height)); currentTopViewAnimation.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (currentTopViewAnimation != null && currentTopViewAnimation.equals(animation)) { topView.setVisibility(GONE); - setTopViewAnimation(0.0f); + resizeForTopView(false); currentTopViewAnimation = null; } } @Override - public void onAnimationCancel(Object animation) { + public void onAnimationCancel(Animator animation) { if (currentTopViewAnimation != null && currentTopViewAnimation.equals(animation)) { currentTopViewAnimation = null; } @@ -1154,7 +1147,7 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat currentTopViewAnimation.start(); } else { topView.setVisibility(GONE); - setTopViewAnimation(0.0f); + topView.setTranslationY(topView.getLayoutParams().height); } } } @@ -1177,7 +1170,8 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat allowShowTopView = false; if (needShowTopView) { topView.setVisibility(GONE); - setTopViewAnimation(0.0f); + resizeForTopView(false); + topView.setTranslationY(topView.getLayoutParams().height); } } } else { @@ -1185,13 +1179,20 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat allowShowTopView = true; if (needShowTopView) { topView.setVisibility(VISIBLE); - setTopViewAnimation(1.0f); + resizeForTopView(true); + topView.setTranslationY(0); } } } } } + private void resizeForTopView(boolean show) { + LayoutParams layoutParams = (LayoutParams) textFieldContainer.getLayoutParams(); + layoutParams.topMargin = AndroidUtilities.dp(2) + (show ? topView.getLayoutParams().height : 0); + textFieldContainer.setLayoutParams(layoutParams); + } + public void onDestroy() { NotificationCenter.getInstance().removeObserver(this, NotificationCenter.recordStarted); NotificationCenter.getInstance().removeObserver(this, NotificationCenter.recordStartError); @@ -1243,22 +1244,11 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat if ((int) dialog_id < 0) { TLRPC.Chat currentChat = MessagesController.getInstance().getChat(-(int) dialog_id); silent = ApplicationLoader.applicationContext.getSharedPreferences("Notifications", Activity.MODE_PRIVATE).getBoolean("silent_" + dialog_id, false); - isAsAdmin = ChatObject.isChannel(currentChat) && (currentChat.creator || currentChat.editor) && !currentChat.megagroup; - adminModeAvailable = isAsAdmin && !currentChat.broadcast; - canWriteToChannel = isAsAdmin; - if (adminModeAvailable) { - SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE); - isAsAdmin = preferences.getBoolean("asadmin_" + dialog_id, true); - } - if (asAdminButton != null) { - asAdminButton.setVisibility(adminModeAvailable ? VISIBLE : GONE); - asAdminButton.setImageResource(isAsAdmin ? R.drawable.publish_active : R.drawable.publish); - updateFieldHint(); - } + canWriteToChannel = ChatObject.isChannel(currentChat) && (currentChat.creator || currentChat.editor) && !currentChat.megagroup; if (notifyButton != null) { notifyButton.setVisibility(canWriteToChannel ? VISIBLE : GONE); notifyButton.setImageResource(silent ? R.drawable.notify_members_off : R.drawable.notify_members_on); - ViewProxy.setPivotX(attachButton, AndroidUtilities.dp((botButton == null || botButton.getVisibility() == GONE) && (notifyButton == null || notifyButton.getVisibility() == GONE) ? 48 : 96)); + attachButton.setPivotX(AndroidUtilities.dp((botButton == null || botButton.getVisibility() == GONE) && (notifyButton == null || notifyButton.getVisibility() == GONE) ? 48 : 96)); } if (attachButton != null) { updateFieldRight(attachButton.getVisibility() == VISIBLE ? 1 : 0); @@ -1276,14 +1266,10 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat if (editingMessageObject != null) { messageEditText.setHint(editingCaption ? LocaleController.getString("Caption", R.string.Caption) : LocaleController.getString("TypeMessage", R.string.TypeMessage)); } else { - if (isAsAdmin) { - if (silent) { - messageEditText.setHint(LocaleController.getString("ChannelSilentBroadcast", R.string.ChannelSilentBroadcast)); - } else { - messageEditText.setHint(LocaleController.getString("ChannelBroadcast", R.string.ChannelBroadcast)); - } + if (silent) { + messageEditText.setHint(LocaleController.getString("ChannelSilentBroadcast", R.string.ChannelSilentBroadcast)); } else { - messageEditText.setHint(LocaleController.getString("ChannelComment", R.string.ChannelComment)); + messageEditText.setHint(LocaleController.getString("ChannelBroadcast", R.string.ChannelBroadcast)); } } } else { @@ -1320,20 +1306,19 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat audioToSendPath = null; audioToSend = null; audioToSendMessageObject = null; - AnimatorSetProxy animatorSetProxy = new AnimatorSetProxy(); - animatorSetProxy.playTogether( - ObjectAnimatorProxy.ofFloat(recordedAudioPanel, "alpha", 0.0f) + AnimatorSet AnimatorSet = new AnimatorSet(); + AnimatorSet.playTogether( + ObjectAnimator.ofFloat(recordedAudioPanel, "alpha", 0.0f) ); - animatorSetProxy.setDuration(200); - animatorSetProxy.addListener(new AnimatorListenerAdapterProxy() { + AnimatorSet.setDuration(200); + AnimatorSet.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { - recordedAudioPanel.clearAnimation(); + public void onAnimationEnd(Animator animation) { recordedAudioPanel.setVisibility(GONE); } }); - animatorSetProxy.start(); + AnimatorSet.start(); } private void sendMessage() { @@ -1359,7 +1344,7 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat if (playing != null && playing == audioToSendMessageObject) { MediaController.getInstance().cleanupPlayer(true, true); } - SendMessagesHelper.getInstance().sendMessage(audioToSend, null, audioToSendPath, dialog_id, replyingMessageObject, isAsAdmin, null, null); + SendMessagesHelper.getInstance().sendMessage(audioToSend, null, audioToSendPath, dialog_id, replyingMessageObject, null, null); if (delegate != null) { delegate.onMessageSend(null); } @@ -1381,34 +1366,10 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat } } - private ArrayList getEntities(CharSequence message) { - ArrayList entities = null; - if (message instanceof Spannable) { - Spannable spannable = (Spannable) message; - URLSpanUserMention spans[] = spannable.getSpans(0, message.length(), URLSpanUserMention.class); - if (spans != null && spans.length > 0) { - entities = new ArrayList<>(); - for (int b = 0; b < spans.length; b++) { - TLRPC.TL_inputMessageEntityMentionName entity = new TLRPC.TL_inputMessageEntityMentionName(); - entity.user_id = MessagesController.getInputUser(Utilities.parseInt(spans[b].getURL())); - if (entity.user_id != null) { - entity.offset = spannable.getSpanStart(spans[b]); - entity.length = Math.min(spannable.getSpanEnd(spans[b]), message.length()) - entity.offset; - if (message.charAt(entity.offset + entity.length - 1) == ' ') { - entity.length--; - } - entities.add(entity); - } - } - } - } - return entities; - } - public void doneEditingMessage() { if (editingMessageObject != null) { delegate.onMessageEditEnd(true); - editingMessageReqId = SendMessagesHelper.getInstance().editMessage(editingMessageObject, messageEditText.getText().toString(), messageWebPageSearch, parentFragment, getEntities(messageEditText.getText()), new Runnable() { + editingMessageReqId = SendMessagesHelper.getInstance().editMessage(editingMessageObject, messageEditText.getText().toString(), messageWebPageSearch, parentFragment, MessagesQuery.getEntities(messageEditText.getText()), new Runnable() { @Override public void run() { editingMessageReqId = 0; @@ -1419,36 +1380,26 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat } public boolean processSendingText(CharSequence text) { - text = getTrimmedString(text); + text = AndroidUtilities.getTrimmedString(text); if (text.length() != 0) { int count = (int) Math.ceil(text.length() / 4096.0f); for (int a = 0; a < count; a++) { CharSequence mess = text.subSequence(a * 4096, Math.min((a + 1) * 4096, text.length())); - SendMessagesHelper.getInstance().sendMessage(mess.toString(), dialog_id, replyingMessageObject, messageWebPage, messageWebPageSearch, asAdmin(), getEntities(mess), null, null); + SendMessagesHelper.getInstance().sendMessage(mess.toString(), dialog_id, replyingMessageObject, messageWebPage, messageWebPageSearch, MessagesQuery.getEntities(mess), null, null); } return true; } return false; } - private CharSequence getTrimmedString(CharSequence src) { - if (src.length() == 0) { - return src; - } - while (src.length() > 0 && (src.charAt(0) == '\n' || src.charAt(0) == ' ')) { - src = src.subSequence(1, src.length()); - } - while (src.length() > 0 && (src.charAt(src.length() - 1) == '\n' || src.charAt(src.length() - 1) == ' ')) { - src = src.subSequence(0, src.length() - 1); - } - return src; - } - - private void checkSendButton(final boolean animated) { + private void checkSendButton(boolean animated) { if (editingMessageObject != null) { return; } - CharSequence message = getTrimmedString(messageEditText.getText()); + if (isPaused) { + animated = false; + } + CharSequence message = AndroidUtilities.getTrimmedString(messageEditText.getText()); if (message.length() > 0 || forceShowSendButton || audioToSend != null) { boolean showBotButton = messageEditText.caption != null && sendButton.getVisibility() == VISIBLE; boolean showSendButton = messageEditText.caption == null && cancelBotButton.getVisibility() == VISIBLE; @@ -1467,23 +1418,22 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat } if (attachButton != null) { - runningAnimation2 = new AnimatorSetProxy(); + runningAnimation2 = new AnimatorSet(); runningAnimation2.playTogether( - ObjectAnimatorProxy.ofFloat(attachButton, "alpha", 0.0f), - ObjectAnimatorProxy.ofFloat(attachButton, "scaleX", 0.0f) + ObjectAnimator.ofFloat(attachButton, "alpha", 0.0f), + ObjectAnimator.ofFloat(attachButton, "scaleX", 0.0f) ); runningAnimation2.setDuration(100); runningAnimation2.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (runningAnimation2 != null && runningAnimation2.equals(animation)) { attachButton.setVisibility(GONE); - attachButton.clearAnimation(); } } @Override - public void onAnimationCancel(Object animation) { + public void onAnimationCancel(Animator animation) { if (runningAnimation2 != null && runningAnimation2.equals(animation)) { runningAnimation2 = null; } @@ -1496,34 +1446,34 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat } } - runningAnimation = new AnimatorSetProxy(); + runningAnimation = new AnimatorSet(); - ArrayList animators = new ArrayList<>(); + ArrayList animators = new ArrayList<>(); if (audioSendButton.getVisibility() == VISIBLE) { - animators.add(ObjectAnimatorProxy.ofFloat(audioSendButton, "scaleX", 0.1f)); - animators.add(ObjectAnimatorProxy.ofFloat(audioSendButton, "scaleY", 0.1f)); - animators.add(ObjectAnimatorProxy.ofFloat(audioSendButton, "alpha", 0.0f)); + animators.add(ObjectAnimator.ofFloat(audioSendButton, "scaleX", 0.1f)); + animators.add(ObjectAnimator.ofFloat(audioSendButton, "scaleY", 0.1f)); + animators.add(ObjectAnimator.ofFloat(audioSendButton, "alpha", 0.0f)); } if (showBotButton) { - animators.add(ObjectAnimatorProxy.ofFloat(sendButton, "scaleX", 0.1f)); - animators.add(ObjectAnimatorProxy.ofFloat(sendButton, "scaleY", 0.1f)); - animators.add(ObjectAnimatorProxy.ofFloat(sendButton, "alpha", 0.0f)); + animators.add(ObjectAnimator.ofFloat(sendButton, "scaleX", 0.1f)); + animators.add(ObjectAnimator.ofFloat(sendButton, "scaleY", 0.1f)); + animators.add(ObjectAnimator.ofFloat(sendButton, "alpha", 0.0f)); } else if (showSendButton) { - animators.add(ObjectAnimatorProxy.ofFloat(cancelBotButton, "scaleX", 0.1f)); - animators.add(ObjectAnimatorProxy.ofFloat(cancelBotButton, "scaleY", 0.1f)); - animators.add(ObjectAnimatorProxy.ofFloat(cancelBotButton, "alpha", 0.0f)); + animators.add(ObjectAnimator.ofFloat(cancelBotButton, "scaleX", 0.1f)); + animators.add(ObjectAnimator.ofFloat(cancelBotButton, "scaleY", 0.1f)); + animators.add(ObjectAnimator.ofFloat(cancelBotButton, "alpha", 0.0f)); } if (messageEditText.caption != null) { runningAnimationType = 3; - animators.add(ObjectAnimatorProxy.ofFloat(cancelBotButton, "scaleX", 1.0f)); - animators.add(ObjectAnimatorProxy.ofFloat(cancelBotButton, "scaleY", 1.0f)); - animators.add(ObjectAnimatorProxy.ofFloat(cancelBotButton, "alpha", 1.0f)); + animators.add(ObjectAnimator.ofFloat(cancelBotButton, "scaleX", 1.0f)); + animators.add(ObjectAnimator.ofFloat(cancelBotButton, "scaleY", 1.0f)); + animators.add(ObjectAnimator.ofFloat(cancelBotButton, "alpha", 1.0f)); cancelBotButton.setVisibility(VISIBLE); } else { runningAnimationType = 1; - animators.add(ObjectAnimatorProxy.ofFloat(sendButton, "scaleX", 1.0f)); - animators.add(ObjectAnimatorProxy.ofFloat(sendButton, "scaleY", 1.0f)); - animators.add(ObjectAnimatorProxy.ofFloat(sendButton, "alpha", 1.0f)); + animators.add(ObjectAnimator.ofFloat(sendButton, "scaleX", 1.0f)); + animators.add(ObjectAnimator.ofFloat(sendButton, "scaleY", 1.0f)); + animators.add(ObjectAnimator.ofFloat(sendButton, "alpha", 1.0f)); sendButton.setVisibility(VISIBLE); } @@ -1531,26 +1481,23 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat runningAnimation.setDuration(150); runningAnimation.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (runningAnimation != null && runningAnimation.equals(animation)) { if (messageEditText.caption != null) { cancelBotButton.setVisibility(VISIBLE); sendButton.setVisibility(GONE); - sendButton.clearAnimation(); } else { sendButton.setVisibility(VISIBLE); cancelBotButton.setVisibility(GONE); - cancelBotButton.clearAnimation(); } audioSendButton.setVisibility(GONE); - audioSendButton.clearAnimation(); runningAnimation = null; runningAnimationType = 0; } } @Override - public void onAnimationCancel(Object animation) { + public void onAnimationCancel(Animator animation) { if (runningAnimation != null && runningAnimation.equals(animation)) { runningAnimation = null; } @@ -1558,35 +1505,31 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat }); runningAnimation.start(); } else { - ViewProxy.setScaleX(audioSendButton, 0.1f); - ViewProxy.setScaleY(audioSendButton, 0.1f); - ViewProxy.setAlpha(audioSendButton, 0.0f); + audioSendButton.setScaleX(0.1f); + audioSendButton.setScaleY(0.1f); + audioSendButton.setAlpha(0.0f); if (messageEditText.caption != null) { - ViewProxy.setScaleX(sendButton, 0.1f); - ViewProxy.setScaleY(sendButton, 0.1f); - ViewProxy.setAlpha(sendButton, 0.0f); - ViewProxy.setScaleX(cancelBotButton, 1.0f); - ViewProxy.setScaleY(cancelBotButton, 1.0f); - ViewProxy.setAlpha(cancelBotButton, 1.0f); + sendButton.setScaleX(0.1f); + sendButton.setScaleY(0.1f); + sendButton.setAlpha(0.0f); + cancelBotButton.setScaleX(1.0f); + cancelBotButton.setScaleY(1.0f); + cancelBotButton.setAlpha(1.0f); cancelBotButton.setVisibility(VISIBLE); sendButton.setVisibility(GONE); - sendButton.clearAnimation(); } else { - ViewProxy.setScaleX(cancelBotButton, 0.1f); - ViewProxy.setScaleY(cancelBotButton, 0.1f); - ViewProxy.setAlpha(cancelBotButton, 0.0f); - ViewProxy.setScaleX(sendButton, 1.0f); - ViewProxy.setScaleY(sendButton, 1.0f); - ViewProxy.setAlpha(sendButton, 1.0f); + cancelBotButton.setScaleX(0.1f); + cancelBotButton.setScaleY(0.1f); + cancelBotButton.setAlpha(0.0f); + sendButton.setScaleX(1.0f); + sendButton.setScaleY(1.0f); + sendButton.setAlpha(1.0f); sendButton.setVisibility(VISIBLE); cancelBotButton.setVisibility(GONE); - cancelBotButton.clearAnimation(); } audioSendButton.setVisibility(GONE); - audioSendButton.clearAnimation(); if (attachButton != null) { attachButton.setVisibility(GONE); - attachButton.clearAnimation(); if (delegate != null) { delegate.onAttachButtonHidden(); } @@ -1611,10 +1554,10 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat if (attachButton != null) { attachButton.setVisibility(VISIBLE); - runningAnimation2 = new AnimatorSetProxy(); + runningAnimation2 = new AnimatorSet(); runningAnimation2.playTogether( - ObjectAnimatorProxy.ofFloat(attachButton, "alpha", 1.0f), - ObjectAnimatorProxy.ofFloat(attachButton, "scaleX", 1.0f) + ObjectAnimator.ofFloat(attachButton, "alpha", 1.0f), + ObjectAnimator.ofFloat(attachButton, "scaleX", 1.0f) ); runningAnimation2.setDuration(100); runningAnimation2.start(); @@ -1623,33 +1566,31 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat } audioSendButton.setVisibility(VISIBLE); - runningAnimation = new AnimatorSetProxy(); + runningAnimation = new AnimatorSet(); runningAnimationType = 2; - ArrayList animators = new ArrayList<>(); - animators.add(ObjectAnimatorProxy.ofFloat(audioSendButton, "scaleX", 1.0f)); - animators.add(ObjectAnimatorProxy.ofFloat(audioSendButton, "scaleY", 1.0f)); - animators.add(ObjectAnimatorProxy.ofFloat(audioSendButton, "alpha", 1.0f)); + ArrayList animators = new ArrayList<>(); + animators.add(ObjectAnimator.ofFloat(audioSendButton, "scaleX", 1.0f)); + animators.add(ObjectAnimator.ofFloat(audioSendButton, "scaleY", 1.0f)); + animators.add(ObjectAnimator.ofFloat(audioSendButton, "alpha", 1.0f)); if (cancelBotButton.getVisibility() == VISIBLE) { - animators.add(ObjectAnimatorProxy.ofFloat(cancelBotButton, "scaleX", 0.1f)); - animators.add(ObjectAnimatorProxy.ofFloat(cancelBotButton, "scaleY", 0.1f)); - animators.add(ObjectAnimatorProxy.ofFloat(cancelBotButton, "alpha", 0.0f)); + animators.add(ObjectAnimator.ofFloat(cancelBotButton, "scaleX", 0.1f)); + animators.add(ObjectAnimator.ofFloat(cancelBotButton, "scaleY", 0.1f)); + animators.add(ObjectAnimator.ofFloat(cancelBotButton, "alpha", 0.0f)); } else { - animators.add(ObjectAnimatorProxy.ofFloat(sendButton, "scaleX", 0.1f)); - animators.add(ObjectAnimatorProxy.ofFloat(sendButton, "scaleY", 0.1f)); - animators.add(ObjectAnimatorProxy.ofFloat(sendButton, "alpha", 0.0f)); + animators.add(ObjectAnimator.ofFloat(sendButton, "scaleX", 0.1f)); + animators.add(ObjectAnimator.ofFloat(sendButton, "scaleY", 0.1f)); + animators.add(ObjectAnimator.ofFloat(sendButton, "alpha", 0.0f)); } runningAnimation.playTogether(animators); runningAnimation.setDuration(150); runningAnimation.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (runningAnimation != null && runningAnimation.equals(animation)) { sendButton.setVisibility(GONE); - sendButton.clearAnimation(); cancelBotButton.setVisibility(GONE); - cancelBotButton.clearAnimation(); audioSendButton.setVisibility(VISIBLE); runningAnimation = null; runningAnimationType = 0; @@ -1657,7 +1598,7 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat } @Override - public void onAnimationCancel(Object animation) { + public void onAnimationCancel(Animator animation) { if (runningAnimation != null && runningAnimation.equals(animation)) { runningAnimation = null; } @@ -1665,19 +1606,17 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat }); runningAnimation.start(); } else { - ViewProxy.setScaleX(sendButton, 0.1f); - ViewProxy.setScaleY(sendButton, 0.1f); - ViewProxy.setAlpha(sendButton, 0.0f); - ViewProxy.setScaleX(cancelBotButton, 0.1f); - ViewProxy.setScaleY(cancelBotButton, 0.1f); - ViewProxy.setAlpha(cancelBotButton, 0.0f); - ViewProxy.setScaleX(audioSendButton, 1.0f); - ViewProxy.setScaleY(audioSendButton, 1.0f); - ViewProxy.setAlpha(audioSendButton, 1.0f); + sendButton.setScaleX(0.1f); + sendButton.setScaleY(0.1f); + sendButton.setAlpha(0.0f); + cancelBotButton.setScaleX(0.1f); + cancelBotButton.setScaleY(0.1f); + cancelBotButton.setAlpha(0.0f); + audioSendButton.setScaleX(1.0f); + audioSendButton.setScaleY(1.0f); + audioSendButton.setAlpha(1.0f); cancelBotButton.setVisibility(GONE); - cancelBotButton.clearAnimation(); sendButton.setVisibility(GONE); - sendButton.clearAnimation(); audioSendButton.setVisibility(VISIBLE); if (attachButton != null) { delegate.onAttachButtonShow(); @@ -1740,22 +1679,22 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) slideText.getLayoutParams(); params.leftMargin = AndroidUtilities.dp(30); slideText.setLayoutParams(params); - ViewProxy.setAlpha(slideText, 1); - ViewProxy.setX(recordPanel, AndroidUtilities.displaySize.x); - ViewProxy.setTranslationX(recordCircle, 0); + slideText.setAlpha(1); + recordPanel.setX(AndroidUtilities.displaySize.x); + recordCircle.setTranslationX(0); if (runningAnimationAudio != null) { runningAnimationAudio.cancel(); } - runningAnimationAudio = new AnimatorSetProxy(); - runningAnimationAudio.playTogether(ObjectAnimatorProxy.ofFloat(recordPanel, "translationX", 0), - ObjectAnimatorProxy.ofFloat(recordCircle, "scale", 1), - ObjectAnimatorProxy.ofFloat(audioSendButton, "alpha", 0)); + runningAnimationAudio = new AnimatorSet(); + runningAnimationAudio.playTogether(ObjectAnimator.ofFloat(recordPanel, "translationX", 0), + ObjectAnimator.ofFloat(recordCircle, "scale", 1), + ObjectAnimator.ofFloat(audioSendButton, "alpha", 0)); runningAnimationAudio.setDuration(300); runningAnimationAudio.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animator) { + public void onAnimationEnd(Animator animator) { if (runningAnimationAudio != null && runningAnimationAudio.equals(animator)) { - ViewProxy.setX(recordPanel, 0); + recordPanel.setX(0); runningAnimationAudio = null; } } @@ -1780,19 +1719,19 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat if (runningAnimationAudio != null) { runningAnimationAudio.cancel(); } - runningAnimationAudio = new AnimatorSetProxy(); - runningAnimationAudio.playTogether(ObjectAnimatorProxy.ofFloat(recordPanel, "translationX", AndroidUtilities.displaySize.x), - ObjectAnimatorProxy.ofFloat(recordCircle, "scale", 0.0f), - ObjectAnimatorProxy.ofFloat(audioSendButton, "alpha", 1.0f)); + runningAnimationAudio = new AnimatorSet(); + runningAnimationAudio.playTogether(ObjectAnimator.ofFloat(recordPanel, "translationX", AndroidUtilities.displaySize.x), + ObjectAnimator.ofFloat(recordCircle, "scale", 0.0f), + ObjectAnimator.ofFloat(audioSendButton, "alpha", 1.0f)); runningAnimationAudio.setDuration(300); runningAnimationAudio.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animator) { + public void onAnimationEnd(Animator animator) { if (runningAnimationAudio != null && runningAnimationAudio.equals(animator)) { FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) slideText.getLayoutParams(); params.leftMargin = AndroidUtilities.dp(30); slideText.setLayoutParams(params); - ViewProxy.setAlpha(slideText, 1); + slideText.setAlpha(1); recordPanel.setVisibility(GONE); recordCircle.setVisibility(GONE); runningAnimationAudio = null; @@ -1833,9 +1772,9 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat } else { TLRPC.User user = messageObject != null && (int) dialog_id < 0 ? MessagesController.getInstance().getUser(messageObject.messageOwner.from_id) : null; if ((botCount != 1 || username) && user != null && user.bot && !command.contains("@")) { - SendMessagesHelper.getInstance().sendMessage(String.format(Locale.US, "%s@%s", command, user.username), dialog_id, null, null, false, asAdmin(), null, null, null); + SendMessagesHelper.getInstance().sendMessage(String.format(Locale.US, "%s@%s", command, user.username), dialog_id, null, null, false, null, null, null); } else { - SendMessagesHelper.getInstance().sendMessage(command, dialog_id, null, null, false, asAdmin(), null, null, null); + SendMessagesHelper.getInstance().sendMessage(command, dialog_id, null, null, false, null, null, null); } } } @@ -1863,7 +1802,7 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat inputFilters[0] = new InputFilter.LengthFilter(4096); if (editingMessageObject.messageText != null) { SpannableStringBuilder stringBuilder = new SpannableStringBuilder(editingMessageObject.messageText.toString()); - ArrayList entities = getEntities(editingMessageObject.messageText); + ArrayList entities = MessagesQuery.getEntities(editingMessageObject.messageText); if (entities != null) { for (int a = 0; a < entities.size(); a++) { TLRPC.TL_inputMessageEntityMentionName entity = (TLRPC.TL_inputMessageEntityMentionName) entities.get(a); @@ -1883,11 +1822,6 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) messageEditText.getLayoutParams(); layoutParams.rightMargin = AndroidUtilities.dp(4); messageEditText.setLayoutParams(layoutParams); - sendButton.clearAnimation(); - cancelBotButton.clearAnimation(); - audioSendButton.clearAnimation(); - attachButton.clearAnimation(); - sendButtonContainer.clearAnimation(); sendButton.setVisibility(GONE); cancelBotButton.setVisibility(GONE); audioSendButton.setVisibility(GONE); @@ -1899,21 +1833,19 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat audioSendButton.setVisibility(VISIBLE); attachButton.setVisibility(VISIBLE); sendButtonContainer.setVisibility(VISIBLE); - ViewProxy.setScaleX(attachButton, 1.0f); - ViewProxy.setAlpha(attachButton, 1.0f); - ViewProxy.setScaleX(sendButton, 0.1f); - ViewProxy.setScaleY(sendButton, 0.1f); - ViewProxy.setAlpha(sendButton, 0.0f); - ViewProxy.setScaleX(cancelBotButton, 0.1f); - ViewProxy.setScaleY(cancelBotButton, 0.1f); - ViewProxy.setAlpha(cancelBotButton, 0.0f); - ViewProxy.setScaleX(audioSendButton, 1.0f); - ViewProxy.setScaleY(audioSendButton, 1.0f); - ViewProxy.setAlpha(audioSendButton, 1.0f); + attachButton.setScaleX(1.0f); + attachButton.setAlpha(1.0f); + sendButton.setScaleX(0.1f); + sendButton.setScaleY(0.1f); + sendButton.setAlpha(0.0f); + cancelBotButton.setScaleX(0.1f); + cancelBotButton.setScaleY(0.1f); + cancelBotButton.setAlpha(0.0f); + audioSendButton.setScaleX(1.0f); + audioSendButton.setScaleY(1.0f); + audioSendButton.setAlpha(1.0f); sendButton.setVisibility(GONE); - sendButton.clearAnimation(); cancelBotButton.setVisibility(GONE); - cancelBotButton.clearAnimation(); messageEditText.setText(""); delegate.onAttachButtonShow(); updateFieldRight(1); @@ -1959,6 +1891,16 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat } } + public void setFieldFocused() { + if (messageEditText != null) { + try { + messageEditText.requestFocus(); + } catch (Exception e) { + FileLog.e("tmessages", e); + } + } + } + public void setFieldFocused(boolean focus) { if (messageEditText == null) { return; @@ -1989,9 +1931,9 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat return messageEditText != null && messageEditText.length() > 0; } - public String getFieldText() { + public CharSequence getFieldText() { if (messageEditText != null && messageEditText.length() > 0) { - return messageEditText.getText().toString(); + return messageEditText.getText(); } return null; } @@ -2031,8 +1973,7 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat botButton.setVisibility(GONE); } updateFieldRight(2); - ViewProxy.setPivotX(attachButton, AndroidUtilities.dp((botButton == null || botButton.getVisibility() == GONE) && (notifyButton == null || notifyButton.getVisibility() == GONE) ? 48 : 96)); - attachButton.clearAnimation(); + attachButton.setPivotX(AndroidUtilities.dp((botButton == null || botButton.getVisibility() == GONE) && (notifyButton == null || notifyButton.getVisibility() == GONE) ? 48 : 96)); } public void setBotsCount(int count, boolean hasCommands) { @@ -2111,7 +2052,7 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat return; } if (button instanceof TLRPC.TL_keyboardButton) { - SendMessagesHelper.getInstance().sendMessage(button.text, dialog_id, replyMessageObject, null, false, asAdmin(), null, null, null); + SendMessagesHelper.getInstance().sendMessage(button.text, dialog_id, replyMessageObject, null, false, null, null, null); } else if (button instanceof TLRPC.TL_keyboardButtonUrl) { parentFragment.showOpenUrlAlert(button.url); } else if (button instanceof TLRPC.TL_keyboardButtonRequestPhone) { @@ -2156,10 +2097,7 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat fragment.finishFragment(); return; } - SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE); - SharedPreferences.Editor editor = preferences.edit(); - editor.putString("dialog_" + did, "@" + user.username + " " + button.query); - editor.commit(); + DraftQuery.saveDraft(did, "@" + user.username + " " + button.query, null, null, true); if (did != dialog_id) { int lower_part = (int) did; if (lower_part != 0) { @@ -2246,7 +2184,7 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat @Override public void onGifSelected(TLRPC.Document gif) { - SendMessagesHelper.getInstance().sendSticker(gif, dialog_id, replyingMessageObject, asAdmin()); + SendMessagesHelper.getInstance().sendSticker(gif, dialog_id, replyingMessageObject); if ((int) dialog_id == 0) { MessagesController.getInstance().saveGif(gif); } @@ -2298,7 +2236,7 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat @Override public void onStickerSelected(TLRPC.Document sticker) { - SendMessagesHelper.getInstance().sendSticker(sticker, dialog_id, replyingMessageObject, asAdmin()); + SendMessagesHelper.getInstance().sendSticker(sticker, dialog_id, replyingMessageObject); if (delegate != null) { delegate.onMessageSend(null); } @@ -2594,8 +2532,7 @@ public class ChatActivityEnterView extends FrameLayoutFixed implements Notificat message.flags |= TLRPC.MESSAGE_FLAG_HAS_MEDIA | TLRPC.MESSAGE_FLAG_HAS_FROM_ID; audioToSendMessageObject = new MessageObject(message, null, false); - ViewProxy.setAlpha(recordedAudioPanel, 1.0f); - recordedAudioPanel.clearAnimation(); + recordedAudioPanel.setAlpha(1.0f); recordedAudioPanel.setVisibility(VISIBLE); int duration = 0; for (int a = 0; a < audioToSend.attributes.size(); a++) { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAttachAlert.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAttachAlert.java index 9b22e8bd7..4c52ff340 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAttachAlert.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAttachAlert.java @@ -40,10 +40,7 @@ import android.widget.ImageView; import android.widget.TextView; import org.telegram.messenger.AndroidUtilities; -import org.telegram.messenger.AnimationCompat.AnimatorListenerAdapterProxy; -import org.telegram.messenger.AnimationCompat.AnimatorSetProxy; -import org.telegram.messenger.AnimationCompat.ObjectAnimatorProxy; -import org.telegram.messenger.AnimationCompat.ViewProxy; +import org.telegram.messenger.AnimatorListenerAdapterProxy; import org.telegram.messenger.ApplicationLoader; import org.telegram.messenger.MessagesController; import org.telegram.messenger.ContactsController; @@ -99,7 +96,7 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N private TextView hintTextView; private ArrayList innerAnimators = new ArrayList<>(); - private AnimatorSetProxy currentHintAnimation; + private AnimatorSet currentHintAnimation; private boolean hintShowed; private Runnable hideHintRunnable; @@ -351,58 +348,48 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N height -= AndroidUtilities.statusBarHeight; } int contentSize = backgroundPaddingTop + AndroidUtilities.dp(294) + (SearchQuery.inlineBots.isEmpty() ? 0 : ((int) Math.ceil(SearchQuery.inlineBots.size() / 4.0f) * AndroidUtilities.dp(100) + AndroidUtilities.dp(12))); - if (Build.VERSION.SDK_INT < 11) { - super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(294) + backgroundPaddingTop, MeasureSpec.EXACTLY)); - } else { - int padding = contentSize == AndroidUtilities.dp(294) ? 0 : (height - AndroidUtilities.dp(294)); - if (padding != 0 && contentSize < height) { - padding -= (height - contentSize); - } - if (padding == 0) { - padding = backgroundPaddingTop; - } - if (getPaddingTop() != padding) { - ignoreLayout = true; - setPadding(backgroundPaddingLeft, padding, backgroundPaddingLeft, 0); - ignoreLayout = false; - } - super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(Math.min(contentSize, height), MeasureSpec.EXACTLY)); + int padding = contentSize == AndroidUtilities.dp(294) ? 0 : (height - AndroidUtilities.dp(294)); + if (padding != 0 && contentSize < height) { + padding -= (height - contentSize); } + if (padding == 0) { + padding = backgroundPaddingTop; + } + if (getPaddingTop() != padding) { + ignoreLayout = true; + setPadding(backgroundPaddingLeft, padding, backgroundPaddingLeft, 0); + ignoreLayout = false; + } + super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(Math.min(contentSize, height), MeasureSpec.EXACTLY)); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); - if (Build.VERSION.SDK_INT >= 11) { - updateLayout(); - } + updateLayout(); } @Override public void onDraw(Canvas canvas) { - if (Build.VERSION.SDK_INT >= 11) { - if (useRevealAnimation && Build.VERSION.SDK_INT <= 19) { - canvas.save(); - canvas.clipRect(backgroundPaddingLeft, scrollOffsetY, getMeasuredWidth() - backgroundPaddingLeft, getMeasuredHeight()); - if (revealAnimationInProgress) { - canvas.drawCircle(revealX, revealY, revealRadius, ciclePaint); - } else { - canvas.drawRect(backgroundPaddingLeft, scrollOffsetY, getMeasuredWidth() - backgroundPaddingLeft, getMeasuredHeight(), ciclePaint); - } - canvas.restore(); + if (useRevealAnimation && Build.VERSION.SDK_INT <= 19) { + canvas.save(); + canvas.clipRect(backgroundPaddingLeft, scrollOffsetY, getMeasuredWidth() - backgroundPaddingLeft, getMeasuredHeight()); + if (revealAnimationInProgress) { + canvas.drawCircle(revealX, revealY, revealRadius, ciclePaint); } else { - shadowDrawable.setBounds(0, scrollOffsetY - backgroundPaddingTop, getMeasuredWidth(), getMeasuredHeight()); - shadowDrawable.draw(canvas); + canvas.drawRect(backgroundPaddingLeft, scrollOffsetY, getMeasuredWidth() - backgroundPaddingLeft, getMeasuredHeight(), ciclePaint); } + canvas.restore(); + } else { + shadowDrawable.setBounds(0, scrollOffsetY - backgroundPaddingTop, getMeasuredWidth(), getMeasuredHeight()); + shadowDrawable.draw(canvas); } } }; - if (Build.VERSION.SDK_INT < 11) { - containerView.setBackgroundDrawable(shadowDrawable); - } else { - containerView.setWillNotDraw(false); - listView.setClipToPadding(false); - } + + listView.setTag(10); + containerView.setWillNotDraw(false); + listView.setClipToPadding(false); listView.setLayoutManager(layoutManager = new LinearLayoutManager(getContext())); layoutManager.setOrientation(LinearLayoutManager.VERTICAL); listView.setAdapter(adapter = new ListAdapter(context)); @@ -432,12 +419,10 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE).edit().putBoolean("bothint", true).commit(); } } - if (Build.VERSION.SDK_INT >= 11) { - updateLayout(); - } + updateLayout(); } }); - containerView.setPadding(backgroundPaddingLeft, Build.VERSION.SDK_INT < 11 ? backgroundPaddingTop : 0, backgroundPaddingLeft, 0); + containerView.setPadding(backgroundPaddingLeft, 0, backgroundPaddingLeft, 0); attachView = new FrameLayout(context) { @Override @@ -471,9 +456,7 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N attachPhotoRecyclerView.setPadding(AndroidUtilities.dp(8), 0, AndroidUtilities.dp(8), 0); attachPhotoRecyclerView.setItemAnimator(null); attachPhotoRecyclerView.setLayoutAnimation(null); - if (Build.VERSION.SDK_INT >= 9) { - attachPhotoRecyclerView.setOverScrollMode(RecyclerListView.OVER_SCROLL_NEVER); - } + attachPhotoRecyclerView.setOverScrollMode(RecyclerListView.OVER_SCROLL_NEVER); attachView.addView(attachPhotoRecyclerView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 80)); attachPhotoLayoutManager = new LinearLayoutManager(context) { @Override @@ -625,26 +608,25 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N if (hintTextView == null) { return; } - currentHintAnimation = new AnimatorSetProxy(); + currentHintAnimation = new AnimatorSet(); currentHintAnimation.playTogether( - ObjectAnimatorProxy.ofFloat(hintTextView, "alpha", 0.0f) + ObjectAnimator.ofFloat(hintTextView, "alpha", 0.0f) ); currentHintAnimation.setInterpolator(decelerateInterpolator); currentHintAnimation.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (currentHintAnimation == null || !currentHintAnimation.equals(animation)) { return; } currentHintAnimation = null; if (hintTextView != null) { - hintTextView.clearAnimation(); hintTextView.setVisibility(View.INVISIBLE); } } @Override - public void onAnimationCancel(Object animation) { + public void onAnimationCancel(Animator animation) { if (currentHintAnimation != null && currentHintAnimation.equals(animation)) { currentHintAnimation = null; } @@ -665,14 +647,14 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N hintShowed = true; hintTextView.setVisibility(View.VISIBLE); - currentHintAnimation = new AnimatorSetProxy(); + currentHintAnimation = new AnimatorSet(); currentHintAnimation.playTogether( - ObjectAnimatorProxy.ofFloat(hintTextView, "alpha", 0.0f, 1.0f) + ObjectAnimator.ofFloat(hintTextView, "alpha", 0.0f, 1.0f) ); currentHintAnimation.setInterpolator(decelerateInterpolator); currentHintAnimation.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (currentHintAnimation == null || !currentHintAnimation.equals(animation)) { return; } @@ -690,7 +672,7 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N } @Override - public void onAnimationCancel(Object animation) { + public void onAnimationCancel(Animator animation) { if (currentHintAnimation != null && currentHintAnimation.equals(animation)) { currentHintAnimation = null; } @@ -786,9 +768,8 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N currentHintAnimation.cancel(); currentHintAnimation = null; } - ViewProxy.setAlpha(hintTextView, 0.0f); + hintTextView.setAlpha(0.0f); hintTextView.setVisibility(View.INVISIBLE); - hintTextView.clearAnimation(); attachPhotoLayoutManager.scrollToPositionWithOffset(0, 1000000); photoAttachAdapter.clearSelectedPhotos(); baseFragment = parentFragment; @@ -807,6 +788,9 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N } private PhotoAttachPhotoCell getCellForIndex(int index) { + if (MediaController.allPhotosAlbumEntry == null) { + return null; + } int count = attachPhotoRecyclerView.getChildCount(); for (int a = 0; a < count; a++) { View view = attachPhotoRecyclerView.getChildAt(a); @@ -836,7 +820,7 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N object.parentView = attachPhotoRecyclerView; object.imageReceiver = cell.getImageView().getImageReceiver(); object.thumb = object.imageReceiver.getBitmap(); - object.scale = ViewProxy.getScaleX(cell.getImageView()); + object.scale = cell.getImageView().getScaleX(); object.clipBottomAddition = (Build.VERSION.SDK_INT >= 21 ? 0 : -AndroidUtilities.statusBarHeight); cell.getCheckBox().setVisibility(View.GONE); return object; @@ -1185,7 +1169,7 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N @SuppressLint("NewApi") private void startRevealAnimation(final boolean open) { - ViewProxy.setTranslationY(containerView, 0); + containerView.setTranslationY(0); final AnimatorSet animatorSet = new AnimatorSet(); @@ -1258,9 +1242,7 @@ public class ChatAttachAlert extends BottomSheet implements NotificationCenter.N currentSheetAnimation = null; onRevealAnimationEnd(open); containerView.invalidate(); - if (Build.VERSION.SDK_INT >= 11) { - containerView.setLayerType(View.LAYER_TYPE_NONE, null); - } + containerView.setLayerType(View.LAYER_TYPE_NONE, null); if (!open) { containerView.setVisibility(View.INVISIBLE); try { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAvatarContainer.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAvatarContainer.java index c851dbf9c..afad12c08 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAvatarContainer.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAvatarContainer.java @@ -37,7 +37,6 @@ public class ChatAvatarContainer extends FrameLayout { private BackupImageView avatarImageView; private SimpleTextView titleTextView; private SimpleTextView subtitleTextView; - private RadioButton radioButton; private ImageView timeItem; private TimerDrawable timerDrawable; private ChatActivity parentFragment; @@ -45,15 +44,10 @@ public class ChatAvatarContainer extends FrameLayout { private RecordStatusDrawable recordStatusDrawable; private SendingFileExDrawable sendingFileDrawable; private AvatarDrawable avatarDrawable = new AvatarDrawable(); - private ChatAvatarContainerDelegate delegate; private int onlineCount = -1; - public interface ChatAvatarContainerDelegate { - void didPressedRadioButton(); - } - - public ChatAvatarContainer(Context context, ChatActivity chatActivity, boolean needRadio, boolean needTime) { + public ChatAvatarContainer(Context context, ChatActivity chatActivity, boolean needTime) { super(context); parentFragment = chatActivity; @@ -90,37 +84,27 @@ public class ChatAvatarContainer extends FrameLayout { }); } - if (needRadio) { - radioButton = new RadioButton(context); - radioButton.setVisibility(View.GONE); - addView(radioButton); - } - setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - if (radioButton == null || radioButton.getVisibility() != View.VISIBLE) { - TLRPC.User user = parentFragment.getCurrentUser(); - TLRPC.Chat chat = parentFragment.getCurrentChat(); - if (user != null) { - Bundle args = new Bundle(); - args.putInt("user_id", user.id); - if (timeItem != null) { - args.putLong("dialog_id", parentFragment.getDialogId()); - } - ProfileActivity fragment = new ProfileActivity(args); - fragment.setPlayProfileAnimation(true); - parentFragment.presentFragment(fragment); - } else if (chat != null) { - Bundle args = new Bundle(); - args.putInt("chat_id", chat.id); - ProfileActivity fragment = new ProfileActivity(args); - fragment.setChatInfo(parentFragment.getCurrentChatInfo()); - fragment.setPlayProfileAnimation(true); - parentFragment.presentFragment(fragment); + TLRPC.User user = parentFragment.getCurrentUser(); + TLRPC.Chat chat = parentFragment.getCurrentChat(); + if (user != null) { + Bundle args = new Bundle(); + args.putInt("user_id", user.id); + if (timeItem != null) { + args.putLong("dialog_id", parentFragment.getDialogId()); } - } else { - delegate.didPressedRadioButton(); + ProfileActivity fragment = new ProfileActivity(args); + fragment.setPlayProfileAnimation(true); + parentFragment.presentFragment(fragment); + } else if (chat != null) { + Bundle args = new Bundle(); + args.putInt("chat_id", chat.id); + ProfileActivity fragment = new ProfileActivity(args); + fragment.setChatInfo(parentFragment.getCurrentChatInfo()); + fragment.setPlayProfileAnimation(true); + parentFragment.presentFragment(fragment); } } }); @@ -140,10 +124,6 @@ public class ChatAvatarContainer extends FrameLayout { int availableWidth = width - AndroidUtilities.dp(54 + 16); avatarImageView.measure(MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(42), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(42), MeasureSpec.EXACTLY)); titleTextView.measure(MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.AT_MOST), MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(24), MeasureSpec.AT_MOST)); - if (radioButton != null && radioButton.getVisibility() == VISIBLE) { - radioButton.measure(MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(24), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(24), MeasureSpec.EXACTLY)); - availableWidth -= AndroidUtilities.dp(20); - } subtitleTextView.measure(MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.AT_MOST), MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(20), MeasureSpec.AT_MOST)); if (timeItem != null) { timeItem.measure(MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(34), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(34), MeasureSpec.EXACTLY)); @@ -160,24 +140,7 @@ public class ChatAvatarContainer extends FrameLayout { if (timeItem != null) { timeItem.layout(AndroidUtilities.dp(8 + 16), viewTop + AndroidUtilities.dp(15), AndroidUtilities.dp(8 + 16 + 34), viewTop + AndroidUtilities.dp(15 + 34)); } - if (radioButton != null && radioButton.getVisibility() == VISIBLE) { - subtitleTextView.layout(AndroidUtilities.dp(8 + 54 + 20), viewTop + AndroidUtilities.dp(24), AndroidUtilities.dp(8 + 54 + 20) + subtitleTextView.getMeasuredWidth(), viewTop + subtitleTextView.getTextHeight() + AndroidUtilities.dp(24)); - viewTop = viewTop + subtitleTextView.getTextHeight() / 2 + AndroidUtilities.dp(12); - radioButton.layout(AndroidUtilities.dp(8 + 50), viewTop, AndroidUtilities.dp(8 + 50 + 24), viewTop + AndroidUtilities.dp(24)); - } else { - subtitleTextView.layout(AndroidUtilities.dp(8 + 54), viewTop + AndroidUtilities.dp(24), AndroidUtilities.dp(8 + 54) + subtitleTextView.getMeasuredWidth(), viewTop + subtitleTextView.getTextHeight() + AndroidUtilities.dp(24)); - } - } - - public void setRadioChecked(boolean value, boolean animated) { - if (radioButton == null) { - return; - } - radioButton.setChecked(value, animated); - } - - public boolean isRadioChecked() { - return radioButton.isChecked(); + subtitleTextView.layout(AndroidUtilities.dp(8 + 54), viewTop + AndroidUtilities.dp(24), AndroidUtilities.dp(8 + 54) + subtitleTextView.getMeasuredWidth(), viewTop + subtitleTextView.getTextHeight() + AndroidUtilities.dp(24)); } public void showTimeItem() { @@ -210,10 +173,6 @@ public class ChatAvatarContainer extends FrameLayout { titleTextView.setText(value); } - public void setDelegate(ChatAvatarContainerDelegate chatAvatarContainerDelegate) { - delegate = chatAvatarContainerDelegate; - } - private void setTypingAnimation(boolean start) { if (start) { try { @@ -257,38 +216,28 @@ public class ChatAvatarContainer extends FrameLayout { if (chat != null) { TLRPC.ChatFull info = parentFragment.getCurrentChatInfo(); if (ChatObject.isChannel(chat)) { - if (!chat.broadcast && !chat.megagroup && !(chat instanceof TLRPC.TL_channelForbidden)) { - subtitleTextView.setText(LocaleController.getString("ShowDiscussion", R.string.ShowDiscussion)); - if (radioButton != null && radioButton.getVisibility() != VISIBLE) { - radioButton.setVisibility(View.VISIBLE); - } - } else { - if (info != null && info.participants_count != 0) { - if (chat.megagroup && info.participants_count <= 200) { - if (onlineCount > 1 && info.participants_count != 0) { - subtitleTextView.setText(String.format("%s, %s", LocaleController.formatPluralString("Members", info.participants_count), LocaleController.formatPluralString("Online", onlineCount))); - } else { - subtitleTextView.setText(LocaleController.formatPluralString("Members", info.participants_count)); - } + if (info != null && info.participants_count != 0) { + if (chat.megagroup && info.participants_count <= 200) { + if (onlineCount > 1 && info.participants_count != 0) { + subtitleTextView.setText(String.format("%s, %s", LocaleController.formatPluralString("Members", info.participants_count), LocaleController.formatPluralString("Online", onlineCount))); } else { - int result[] = new int[1]; - String shortNumber = LocaleController.formatShortNumber(info.participants_count, result); - String text = LocaleController.formatPluralString("Members", result[0]).replace(String.format("%d", result[0]), shortNumber); - subtitleTextView.setText(text); + subtitleTextView.setText(LocaleController.formatPluralString("Members", info.participants_count)); } } else { - if (chat.megagroup) { - subtitleTextView.setText(LocaleController.getString("Loading", R.string.Loading).toLowerCase()); - } else { - if ((chat.flags & TLRPC.CHAT_FLAG_IS_PUBLIC) != 0) { - subtitleTextView.setText(LocaleController.getString("ChannelPublic", R.string.ChannelPublic).toLowerCase()); - } else { - subtitleTextView.setText(LocaleController.getString("ChannelPrivate", R.string.ChannelPrivate).toLowerCase()); - } - } + int result[] = new int[1]; + String shortNumber = LocaleController.formatShortNumber(info.participants_count, result); + String text = LocaleController.formatPluralString("Members", result[0]).replace(String.format("%d", result[0]), shortNumber); + subtitleTextView.setText(text); } - if (radioButton != null && radioButton.getVisibility() != GONE) { - radioButton.setVisibility(View.GONE); + } else { + if (chat.megagroup) { + subtitleTextView.setText(LocaleController.getString("Loading", R.string.Loading).toLowerCase()); + } else { + if ((chat.flags & TLRPC.CHAT_FLAG_IS_PUBLIC) != 0) { + subtitleTextView.setText(LocaleController.getString("ChannelPublic", R.string.ChannelPublic).toLowerCase()); + } else { + subtitleTextView.setText(LocaleController.getString("ChannelPrivate", R.string.ChannelPrivate).toLowerCase()); + } } } } else { @@ -298,7 +247,7 @@ public class ChatAvatarContainer extends FrameLayout { subtitleTextView.setText(LocaleController.getString("YouLeft", R.string.YouLeft)); } else { int count = chat.participants_count; - if (info != null) { + if (info != null && info.participants != null) { count = info.participants.participants.size(); } if (onlineCount > 1 && count != 0) { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/CheckBox.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/CheckBox.java index 5a075500f..7b68bc2e1 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/CheckBox.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/CheckBox.java @@ -8,6 +8,7 @@ package org.telegram.ui.Components; +import android.animation.ObjectAnimator; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; @@ -18,7 +19,6 @@ import android.graphics.drawable.Drawable; import android.view.View; import org.telegram.messenger.AndroidUtilities; -import org.telegram.messenger.AnimationCompat.ObjectAnimatorProxy; public class CheckBox extends View { @@ -37,7 +37,7 @@ public class CheckBox extends View { private boolean drawBackground; private float progress; - private ObjectAnimatorProxy checkAnimator; + private ObjectAnimator checkAnimator; private boolean isCheckAnimation = true; private boolean attachedToWindow; @@ -117,7 +117,7 @@ public class CheckBox extends View { private void animateToCheckedState(boolean newCheckedState) { isCheckAnimation = newCheckedState; - checkAnimator = ObjectAnimatorProxy.ofFloatProxy(this, "progress", newCheckedState ? 1 : 0); + checkAnimator = ObjectAnimator.ofFloat(this, "progress", newCheckedState ? 1 : 0); checkAnimator.setDuration(300); checkAnimator.start(); } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/CheckBoxSquare.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/CheckBoxSquare.java index eea7e1615..ab89f7ab0 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/CheckBoxSquare.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/CheckBoxSquare.java @@ -8,6 +8,7 @@ package org.telegram.ui.Components; +import android.animation.ObjectAnimator; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; @@ -19,7 +20,6 @@ import android.graphics.RectF; import android.view.View; import org.telegram.messenger.AndroidUtilities; -import org.telegram.messenger.AnimationCompat.ObjectAnimatorProxy; public class CheckBoxSquare extends View { @@ -32,7 +32,7 @@ public class CheckBoxSquare extends View { private Canvas drawCanvas; private float progress; - private ObjectAnimatorProxy checkAnimator; + private ObjectAnimator checkAnimator; private boolean attachedToWindow; private boolean isChecked; @@ -91,7 +91,7 @@ public class CheckBoxSquare extends View { } private void animateToCheckedState(boolean newCheckedState) { - checkAnimator = ObjectAnimatorProxy.ofFloatProxy(this, "progress", newCheckedState ? 1 : 0); + checkAnimator = ObjectAnimator.ofFloat(this, "progress", newCheckedState ? 1 : 0); checkAnimator.setDuration(300); checkAnimator.start(); } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/ClippingImageView.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/ClippingImageView.java index 64790946f..6988823b4 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/ClippingImageView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/ClippingImageView.java @@ -19,7 +19,6 @@ import android.graphics.Shader; import android.view.View; import org.telegram.messenger.FileLog; -import org.telegram.messenger.AnimationCompat.ViewProxy; public class ClippingImageView extends View { @@ -67,10 +66,10 @@ public class ClippingImageView extends View { public void setAnimationProgress(float progress) { animationProgress = progress; - ViewProxy.setScaleX(this, animationValues[0][0] + (animationValues[1][0] - animationValues[0][0]) * animationProgress); - ViewProxy.setScaleY(this, animationValues[0][1] + (animationValues[1][1] - animationValues[0][1]) * animationProgress); - ViewProxy.setTranslationX(this, animationValues[0][2] + (animationValues[1][2] - animationValues[0][2]) * animationProgress); - ViewProxy.setTranslationY(this, animationValues[0][3] + (animationValues[1][3] - animationValues[0][3]) * animationProgress); + setScaleX(animationValues[0][0] + (animationValues[1][0] - animationValues[0][0]) * animationProgress); + setScaleY(animationValues[0][1] + (animationValues[1][1] - animationValues[0][1]) * animationProgress); + setTranslationX(animationValues[0][2] + (animationValues[1][2] - animationValues[0][2]) * animationProgress); + setTranslationY(animationValues[0][3] + (animationValues[1][3] - animationValues[0][3]) * animationProgress); setClipHorizontal((int) (animationValues[0][4] + (animationValues[1][4] - animationValues[0][4]) * animationProgress)); setClipTop((int) (animationValues[0][5] + (animationValues[1][5] - animationValues[0][5]) * animationProgress)); setClipBottom((int) (animationValues[0][6] + (animationValues[1][6] - animationValues[0][6]) * animationProgress)); @@ -108,7 +107,7 @@ public class ClippingImageView extends View { return; } if (bmp != null) { - float scaleY = ViewProxy.getScaleY(this); + float scaleY = getScaleY(); canvas.save(); if (needRadius) { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/EmojiView.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/EmojiView.java index 3dbd1a357..8b7c9a237 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/EmojiView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/EmojiView.java @@ -18,7 +18,6 @@ import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.RectF; import android.graphics.drawable.Drawable; -import android.os.Build; import android.support.v4.view.PagerAdapter; import android.support.v4.view.ViewPager; import android.util.TypedValue; @@ -41,7 +40,6 @@ import android.widget.PopupWindow; import android.widget.TextView; import org.telegram.messenger.AndroidUtilities; -import org.telegram.messenger.AnimationCompat.ViewProxy; import org.telegram.messenger.Emoji; import org.telegram.messenger.EmojiData; import org.telegram.messenger.LocaleController; @@ -627,6 +625,7 @@ public class EmojiView extends FrameLayout implements NotificationCenter.Notific if (needGif) { gifsGridView = new RecyclerListView(context); + gifsGridView.setTag(11); gifsGridView.setLayoutManager(flowLayoutManager = new ExtendedGridLayoutManager(context, 100) { private Size size = new Size(); @@ -666,9 +665,7 @@ public class EmojiView extends FrameLayout implements NotificationCenter.Notific outRect.right = flowLayoutManager.isLastInRow(position) ? 0 : AndroidUtilities.dp(2); } }); - if (Build.VERSION.SDK_INT >= 9) { - gifsGridView.setOverScrollMode(RecyclerListView.OVER_SCROLL_NEVER); - } + gifsGridView.setOverScrollMode(RecyclerListView.OVER_SCROLL_NEVER); gifsGridView.setAdapter(gifsAdapter = new GifsAdapter(context)); gifsGridView.setOnItemClickListener(new RecyclerListView.OnItemClickListener() { @Override @@ -747,51 +744,48 @@ public class EmojiView extends FrameLayout implements NotificationCenter.Notific @Override public boolean onTouchEvent(MotionEvent ev) { - if (Build.VERSION.SDK_INT >= 11) { - if (first) { - first = false; - lastX = ev.getX(); - } - float newTranslationX = ViewProxy.getTranslationX(scrollSlidingTabStrip); - if (scrollSlidingTabStrip.getScrollX() == 0 && newTranslationX == 0) { - if (!startedScroll && lastX - ev.getX() < 0) { - if (pager.beginFakeDrag()) { - startedScroll = true; - lastTranslateX = ViewProxy.getTranslationX(scrollSlidingTabStrip); - } - } else if (startedScroll && lastX - ev.getX() > 0) { - if (pager.isFakeDragging()) { - pager.endFakeDrag(); - startedScroll = false; - } - } - } - if (startedScroll) { - int dx = (int) (ev.getX() - lastX + newTranslationX - lastTranslateX); - try { - pager.fakeDragBy(dx); - lastTranslateX = newTranslationX; - } catch (Exception e) { - try { - pager.endFakeDrag(); - } catch (Exception e2) { - //don't promt - } - startedScroll = false; - FileLog.e("tmessages", e); - } - } + if (first) { + first = false; lastX = ev.getX(); - if (ev.getAction() == MotionEvent.ACTION_CANCEL || ev.getAction() == MotionEvent.ACTION_UP) { - first = true; - if (startedScroll) { + } + float newTranslationX = scrollSlidingTabStrip.getTranslationX(); + if (scrollSlidingTabStrip.getScrollX() == 0 && newTranslationX == 0) { + if (!startedScroll && lastX - ev.getX() < 0) { + if (pager.beginFakeDrag()) { + startedScroll = true; + lastTranslateX = scrollSlidingTabStrip.getTranslationX(); + } + } else if (startedScroll && lastX - ev.getX() > 0) { + if (pager.isFakeDragging()) { pager.endFakeDrag(); startedScroll = false; } } - return startedScroll || super.onTouchEvent(ev); } - return super.onTouchEvent(ev); + if (startedScroll) { + int dx = (int) (ev.getX() - lastX + newTranslationX - lastTranslateX); + try { + pager.fakeDragBy(dx); + lastTranslateX = newTranslationX; + } catch (Exception e) { + try { + pager.endFakeDrag(); + } catch (Exception e2) { + //don't promt + } + startedScroll = false; + FileLog.e("tmessages", e); + } + } + lastX = ev.getX(); + if (ev.getAction() == MotionEvent.ACTION_CANCEL || ev.getAction() == MotionEvent.ACTION_UP) { + first = true; + if (startedScroll) { + pager.endFakeDrag(); + startedScroll = false; + } + } + return startedScroll || super.onTouchEvent(ev); } }; scrollSlidingTabStrip.setUnderlineHeight(AndroidUtilities.dp(1)); @@ -799,7 +793,7 @@ public class EmojiView extends FrameLayout implements NotificationCenter.Notific scrollSlidingTabStrip.setUnderlineColor(0xffe2e5e7); scrollSlidingTabStrip.setVisibility(INVISIBLE); addView(scrollSlidingTabStrip, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 48, Gravity.LEFT | Gravity.TOP)); - ViewProxy.setTranslationX(scrollSlidingTabStrip, AndroidUtilities.displaySize.x); + scrollSlidingTabStrip.setTranslationX(AndroidUtilities.displaySize.x); updateStickerTabs(); scrollSlidingTabStrip.setDelegate(new ScrollSlidingTabStrip.ScrollSlidingTabStripDelegate() { @Override @@ -1038,21 +1032,10 @@ public class EmojiView extends FrameLayout implements NotificationCenter.Notific } } - if (ViewProxy.getTranslationX(pagerSlidingTabStripContainer) != margin) { - ViewProxy.setTranslationX(pagerSlidingTabStripContainer, margin); - ViewProxy.setTranslationX(scrollSlidingTabStrip, width + margin); + if (pagerSlidingTabStripContainer.getTranslationX() != margin) { + pagerSlidingTabStripContainer.setTranslationX(margin); + scrollSlidingTabStrip.setTranslationX(width + margin); scrollSlidingTabStrip.setVisibility(margin < 0 ? VISIBLE : INVISIBLE); - if (Build.VERSION.SDK_INT < 11) { - if (margin <= -width) { - pagerSlidingTabStripContainer.clearAnimation(); - pagerSlidingTabStripContainer.setVisibility(GONE); - } else { - pagerSlidingTabStripContainer.setVisibility(VISIBLE); - } - } - } else if (Build.VERSION.SDK_INT < 11 && pagerSlidingTabStripContainer.getVisibility() == GONE) { - pagerSlidingTabStripContainer.clearAnimation(); - pagerSlidingTabStripContainer.setVisibility(GONE); } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/ExtendedGridLayoutManager.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/ExtendedGridLayoutManager.java index 624a714a3..f5cb849a9 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/ExtendedGridLayoutManager.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/ExtendedGridLayoutManager.java @@ -38,8 +38,9 @@ public class ExtendedGridLayoutManager extends GridLayoutManager { int preferredRowSize = AndroidUtilities.dp(100); float totalItemSize = 0; - int[] weights = new int[getFlowItemCount()]; - for (int a = 0; a < getFlowItemCount(); a++) { + int itemsCount = getFlowItemCount(); + int[] weights = new int[itemsCount]; + for (int a = 0; a < itemsCount; a++) { Size size = sizeForItem(a); totalItemSize += (size.width / size.height) * preferredRowSize; weights[a] = Math.round(size.width / size.height * 100); @@ -75,7 +76,7 @@ public class ExtendedGridLayoutManager extends GridLayoutManager { Size preferredSize = sizeForItem(j); int width = Math.round(rowSize / summedRatios * (preferredSize.width / preferredSize.height)); int itemSpan; - if (j != n - 1) { + if (itemsCount < 3 || j != n - 1) { itemSpan = (int) (width / viewPortAvailableSize * getSpanCount()); spanLeft -= itemSpan; } else { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/FrameLayoutFixed.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/FrameLayoutFixed.java deleted file mode 100644 index f558d9d2d..000000000 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/FrameLayoutFixed.java +++ /dev/null @@ -1,163 +0,0 @@ -/* - * This is the source code of Telegram for Android v. 3.x.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-2016. - */ - -package org.telegram.ui.Components; - -import android.content.Context; -import android.graphics.drawable.Drawable; -import android.util.AttributeSet; -import android.view.View; -import android.widget.FrameLayout; - -import org.telegram.messenger.AndroidUtilities; -import org.telegram.messenger.FileLog; - -import java.util.ArrayList; - -public class FrameLayoutFixed extends FrameLayout { - - private final ArrayList mMatchParentChildren = new ArrayList<>(1); - - public FrameLayoutFixed(Context context) { - super(context); - } - - public FrameLayoutFixed(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public FrameLayoutFixed(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - } - - public final int getMeasuredStateFixed(View view) { - return (view.getMeasuredWidth()&0xff000000) - | ((view.getMeasuredHeight()>>16) - & (0xff000000>>16)); - } - - public static int resolveSizeAndStateFixed(int size, int measureSpec, int childMeasuredState) { - int result = size; - int specMode = MeasureSpec.getMode(measureSpec); - int specSize = MeasureSpec.getSize(measureSpec); - switch (specMode) { - case MeasureSpec.UNSPECIFIED: - result = size; - break; - case MeasureSpec.AT_MOST: - if (specSize < size) { - result = specSize | 0x01000000; - } else { - result = size; - } - break; - case MeasureSpec.EXACTLY: - result = specSize; - break; - } - return result | (childMeasuredState&0xff000000); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - try { - int count = getChildCount(); - - final boolean measureMatchParentChildren = - MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY || - MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY; - mMatchParentChildren.clear(); - - int maxHeight = 0; - int maxWidth = 0; - int childState = 0; - - for (int i = 0; i < count; i++) { - final View child = getChildAt(i); - if (child.getVisibility() != GONE) { - measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0); - final LayoutParams lp = (LayoutParams) child.getLayoutParams(); - maxWidth = Math.max(maxWidth, - child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin); - maxHeight = Math.max(maxHeight, - child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin); - childState |= getMeasuredStateFixed(child); - if (measureMatchParentChildren) { - if (lp.width == LayoutHelper.MATCH_PARENT || - lp.height == LayoutHelper.MATCH_PARENT) { - mMatchParentChildren.add(child); - } - } - } - } - - // Account for padding too - maxWidth += getPaddingLeft() + getPaddingRight(); - maxHeight += getPaddingTop() + getPaddingBottom(); - - // Check against our minimum height and width - maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight()); - maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth()); - - // Check against our foreground's minimum height and width - final Drawable drawable = getForeground(); - if (drawable != null) { - maxHeight = Math.max(maxHeight, drawable.getMinimumHeight()); - maxWidth = Math.max(maxWidth, drawable.getMinimumWidth()); - } - - setMeasuredDimension(resolveSizeAndStateFixed(maxWidth, widthMeasureSpec, childState), - resolveSizeAndStateFixed(maxHeight, heightMeasureSpec, childState << 16)); - - count = mMatchParentChildren.size(); - if (count > 1) { - for (int i = 0; i < count; i++) { - final View child = mMatchParentChildren.get(i); - - final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); - int childWidthMeasureSpec; - int childHeightMeasureSpec; - - if (lp.width == LayoutHelper.MATCH_PARENT) { - childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth() - - getPaddingLeft() - getPaddingRight() - - lp.leftMargin - lp.rightMargin, - MeasureSpec.EXACTLY); - } else { - childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, - getPaddingLeft() + getPaddingRight() + - lp.leftMargin + lp.rightMargin, - lp.width); - } - - if (lp.height == LayoutHelper.MATCH_PARENT) { - childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight() - - getPaddingTop() - getPaddingBottom() - - lp.topMargin - lp.bottomMargin, - MeasureSpec.EXACTLY); - } else { - childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, - getPaddingTop() + getPaddingBottom() + - lp.topMargin + lp.bottomMargin, - lp.height); - } - - child.measure(childWidthMeasureSpec, childHeightMeasureSpec); - } - } - } catch (Exception e) { - FileLog.e("tmessages", e); - try { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - } catch (Exception e2) { - FileLog.e("tmessages", e2); - setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(10), MeasureSpec.EXACTLY)); - } - } - } -} diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/NumberTextView.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/NumberTextView.java index dbc1f0f5b..991b3448c 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/NumberTextView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/NumberTextView.java @@ -8,6 +8,8 @@ package org.telegram.ui.Components; +import android.animation.Animator; +import android.animation.ObjectAnimator; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; @@ -18,8 +20,7 @@ import android.text.TextPaint; import android.view.View; import org.telegram.messenger.AndroidUtilities; -import org.telegram.messenger.AnimationCompat.AnimatorListenerAdapterProxy; -import org.telegram.messenger.AnimationCompat.ObjectAnimatorProxy; +import org.telegram.messenger.AnimatorListenerAdapterProxy; import java.util.ArrayList; import java.util.Locale; @@ -29,7 +30,7 @@ public class NumberTextView extends View { private ArrayList letters = new ArrayList<>(); private ArrayList oldLetters = new ArrayList<>(); private TextPaint textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); - private ObjectAnimatorProxy animator; + private ObjectAnimator animator; private float progress = 0.0f; private int currentNumber = 1; @@ -77,11 +78,11 @@ public class NumberTextView extends View { } } if (animated && !oldLetters.isEmpty()) { - animator = ObjectAnimatorProxy.ofFloatProxy(this, "progress", forwardAnimation ? -1 : 1, 0); + animator = ObjectAnimator.ofFloat(this, "progress", forwardAnimation ? -1 : 1, 0); animator.setDuration(150); animator.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { animator = null; oldLetters.clear(); } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/PasscodeView.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/PasscodeView.java index cf49b2769..3b68e7110 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/PasscodeView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/PasscodeView.java @@ -8,6 +8,9 @@ package org.telegram.ui.Components; +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; import android.app.Activity; import android.app.AlertDialog; import android.content.Context; @@ -30,7 +33,6 @@ import android.text.TextWatcher; import android.text.method.PasswordTransformationMethod; import android.util.TypedValue; import android.view.ActionMode; -import android.view.ContextMenu; import android.view.Gravity; import android.view.HapticFeedbackConstants; import android.view.KeyEvent; @@ -52,10 +54,7 @@ import org.telegram.messenger.ApplicationLoader; import org.telegram.messenger.FileLog; import org.telegram.messenger.R; import org.telegram.messenger.UserConfig; -import org.telegram.messenger.AnimationCompat.AnimatorListenerAdapterProxy; -import org.telegram.messenger.AnimationCompat.AnimatorSetProxy; -import org.telegram.messenger.AnimationCompat.ObjectAnimatorProxy; -import org.telegram.messenger.AnimationCompat.ViewProxy; +import org.telegram.messenger.AnimatorListenerAdapterProxy; import java.util.ArrayList; import java.util.Locale; @@ -72,7 +71,7 @@ public class PasscodeView extends FrameLayout { private ArrayList dotTextViews; private StringBuilder stringBuilder; private String DOT = "\u2022"; - private AnimatorSetProxy currentAnimation; + private AnimatorSet currentAnimation; private Runnable dotRunnable; public AnimatingTextView(Context context) { @@ -86,9 +85,9 @@ public class PasscodeView extends FrameLayout { textView.setTextColor(0xffffffff); textView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 36); textView.setGravity(Gravity.CENTER); - ViewProxy.setAlpha(textView, 0); - ViewProxy.setPivotX(textView, AndroidUtilities.dp(25)); - ViewProxy.setPivotY(textView, AndroidUtilities.dp(25)); + textView.setAlpha(0); + textView.setPivotX(AndroidUtilities.dp(25)); + textView.setPivotY(AndroidUtilities.dp(25)); addView(textView); LayoutParams layoutParams = (LayoutParams) textView.getLayoutParams(); layoutParams.width = AndroidUtilities.dp(50); @@ -101,10 +100,10 @@ public class PasscodeView extends FrameLayout { textView.setTextColor(0xffffffff); textView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 36); textView.setGravity(Gravity.CENTER); - ViewProxy.setAlpha(textView, 0); + textView.setAlpha(0); textView.setText(DOT); - ViewProxy.setPivotX(textView, AndroidUtilities.dp(25)); - ViewProxy.setPivotY(textView, AndroidUtilities.dp(25)); + textView.setPivotX(AndroidUtilities.dp(25)); + textView.setPivotY(AndroidUtilities.dp(25)); addView(textView); layoutParams = (LayoutParams) textView.getLayoutParams(); layoutParams.width = AndroidUtilities.dp(50); @@ -130,37 +129,37 @@ public class PasscodeView extends FrameLayout { } - ArrayList animators = new ArrayList<>(); + ArrayList animators = new ArrayList<>(); final int newPos = stringBuilder.length(); stringBuilder.append(c); TextView textView = characterTextViews.get(newPos); textView.setText(c); - ViewProxy.setTranslationX(textView, getXForTextView(newPos)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "scaleX", 0, 1)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "scaleY", 0, 1)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "alpha", 0, 1)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "translationY", AndroidUtilities.dp(20), 0)); + textView.setTranslationX(getXForTextView(newPos)); + animators.add(ObjectAnimator.ofFloat(textView, "scaleX", 0, 1)); + animators.add(ObjectAnimator.ofFloat(textView, "scaleY", 0, 1)); + animators.add(ObjectAnimator.ofFloat(textView, "alpha", 0, 1)); + animators.add(ObjectAnimator.ofFloat(textView, "translationY", AndroidUtilities.dp(20), 0)); textView = dotTextViews.get(newPos); - ViewProxy.setTranslationX(textView, getXForTextView(newPos)); - ViewProxy.setAlpha(textView, 0); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "scaleX", 0, 1)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "scaleY", 0, 1)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "translationY", AndroidUtilities.dp(20), 0)); + textView.setTranslationX(getXForTextView(newPos)); + textView.setAlpha(0); + animators.add(ObjectAnimator.ofFloat(textView, "scaleX", 0, 1)); + animators.add(ObjectAnimator.ofFloat(textView, "scaleY", 0, 1)); + animators.add(ObjectAnimator.ofFloat(textView, "translationY", AndroidUtilities.dp(20), 0)); for (int a = newPos + 1; a < 4; a++) { textView = characterTextViews.get(a); - if (ViewProxy.getAlpha(textView) != 0) { - animators.add(ObjectAnimatorProxy.ofFloat(textView, "scaleX", 0)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "scaleY", 0)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "alpha", 0)); + if (textView.getAlpha() != 0) { + animators.add(ObjectAnimator.ofFloat(textView, "scaleX", 0)); + animators.add(ObjectAnimator.ofFloat(textView, "scaleY", 0)); + animators.add(ObjectAnimator.ofFloat(textView, "alpha", 0)); } textView = dotTextViews.get(a); - if (ViewProxy.getAlpha(textView) != 0) { - animators.add(ObjectAnimatorProxy.ofFloat(textView, "scaleX", 0)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "scaleY", 0)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "alpha", 0)); + if (textView.getAlpha() != 0) { + animators.add(ObjectAnimator.ofFloat(textView, "scaleX", 0)); + animators.add(ObjectAnimator.ofFloat(textView, "scaleY", 0)); + animators.add(ObjectAnimator.ofFloat(textView, "alpha", 0)); } } @@ -173,23 +172,23 @@ public class PasscodeView extends FrameLayout { if (dotRunnable != this) { return; } - ArrayList animators = new ArrayList<>(); + ArrayList animators = new ArrayList<>(); TextView textView = characterTextViews.get(newPos); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "scaleX", 0)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "scaleY", 0)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "alpha", 0)); + animators.add(ObjectAnimator.ofFloat(textView, "scaleX", 0)); + animators.add(ObjectAnimator.ofFloat(textView, "scaleY", 0)); + animators.add(ObjectAnimator.ofFloat(textView, "alpha", 0)); textView = dotTextViews.get(newPos); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "scaleX", 1)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "scaleY", 1)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "alpha", 1)); + animators.add(ObjectAnimator.ofFloat(textView, "scaleX", 1)); + animators.add(ObjectAnimator.ofFloat(textView, "scaleY", 1)); + animators.add(ObjectAnimator.ofFloat(textView, "alpha", 1)); - currentAnimation = new AnimatorSetProxy(); + currentAnimation = new AnimatorSet(); currentAnimation.setDuration(150); currentAnimation.playTogether(animators); currentAnimation.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (currentAnimation != null && currentAnimation.equals(animation)) { currentAnimation = null; } @@ -202,28 +201,28 @@ public class PasscodeView extends FrameLayout { for (int a = 0; a < newPos; a++) { textView = characterTextViews.get(a); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "translationX", getXForTextView(a))); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "scaleX", 0)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "scaleY", 0)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "alpha", 0)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "translationY", 0)); + animators.add(ObjectAnimator.ofFloat(textView, "translationX", getXForTextView(a))); + animators.add(ObjectAnimator.ofFloat(textView, "scaleX", 0)); + animators.add(ObjectAnimator.ofFloat(textView, "scaleY", 0)); + animators.add(ObjectAnimator.ofFloat(textView, "alpha", 0)); + animators.add(ObjectAnimator.ofFloat(textView, "translationY", 0)); textView = dotTextViews.get(a); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "translationX", getXForTextView(a))); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "scaleX", 1)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "scaleY", 1)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "alpha", 1)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "translationY", 0)); + animators.add(ObjectAnimator.ofFloat(textView, "translationX", getXForTextView(a))); + animators.add(ObjectAnimator.ofFloat(textView, "scaleX", 1)); + animators.add(ObjectAnimator.ofFloat(textView, "scaleY", 1)); + animators.add(ObjectAnimator.ofFloat(textView, "alpha", 1)); + animators.add(ObjectAnimator.ofFloat(textView, "translationY", 0)); } if (currentAnimation != null) { currentAnimation.cancel(); } - currentAnimation = new AnimatorSetProxy(); + currentAnimation = new AnimatorSet(); currentAnimation.setDuration(150); currentAnimation.playTogether(animators); currentAnimation.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (currentAnimation != null && currentAnimation.equals(animation)) { currentAnimation = null; } @@ -250,7 +249,7 @@ public class PasscodeView extends FrameLayout { FileLog.e("tmessages", e); } - ArrayList animators = new ArrayList<>(); + ArrayList animators = new ArrayList<>(); int deletingPos = stringBuilder.length() - 1; if (deletingPos != 0) { stringBuilder.deleteCharAt(deletingPos); @@ -258,21 +257,21 @@ public class PasscodeView extends FrameLayout { for (int a = deletingPos; a < 4; a++) { TextView textView = characterTextViews.get(a); - if (ViewProxy.getAlpha(textView) != 0) { - animators.add(ObjectAnimatorProxy.ofFloat(textView, "scaleX", 0)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "scaleY", 0)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "alpha", 0)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "translationY", 0)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "translationX", getXForTextView(a))); + if (textView.getAlpha() != 0) { + animators.add(ObjectAnimator.ofFloat(textView, "scaleX", 0)); + animators.add(ObjectAnimator.ofFloat(textView, "scaleY", 0)); + animators.add(ObjectAnimator.ofFloat(textView, "alpha", 0)); + animators.add(ObjectAnimator.ofFloat(textView, "translationY", 0)); + animators.add(ObjectAnimator.ofFloat(textView, "translationX", getXForTextView(a))); } textView = dotTextViews.get(a); - if (ViewProxy.getAlpha(textView) != 0) { - animators.add(ObjectAnimatorProxy.ofFloat(textView, "scaleX", 0)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "scaleY", 0)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "alpha", 0)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "translationY", 0)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "translationX", getXForTextView(a))); + if (textView.getAlpha() != 0) { + animators.add(ObjectAnimator.ofFloat(textView, "scaleX", 0)); + animators.add(ObjectAnimator.ofFloat(textView, "scaleY", 0)); + animators.add(ObjectAnimator.ofFloat(textView, "alpha", 0)); + animators.add(ObjectAnimator.ofFloat(textView, "translationY", 0)); + animators.add(ObjectAnimator.ofFloat(textView, "translationX", getXForTextView(a))); } } @@ -282,9 +281,9 @@ public class PasscodeView extends FrameLayout { for (int a = 0; a < deletingPos; a++) { TextView textView = characterTextViews.get(a); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "translationX", getXForTextView(a))); + animators.add(ObjectAnimator.ofFloat(textView, "translationX", getXForTextView(a))); textView = dotTextViews.get(a); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "translationX", getXForTextView(a))); + animators.add(ObjectAnimator.ofFloat(textView, "translationX", getXForTextView(a))); } if (dotRunnable != null) { @@ -295,12 +294,12 @@ public class PasscodeView extends FrameLayout { if (currentAnimation != null) { currentAnimation.cancel(); } - currentAnimation = new AnimatorSetProxy(); + currentAnimation = new AnimatorSet(); currentAnimation.setDuration(150); currentAnimation.playTogether(animators); currentAnimation.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (currentAnimation != null && currentAnimation.equals(animation)) { currentAnimation = null; } @@ -323,30 +322,30 @@ public class PasscodeView extends FrameLayout { } stringBuilder.delete(0, stringBuilder.length()); if (animated) { - ArrayList animators = new ArrayList<>(); + ArrayList animators = new ArrayList<>(); for (int a = 0; a < 4; a++) { TextView textView = characterTextViews.get(a); - if (ViewProxy.getAlpha(textView) != 0) { - animators.add(ObjectAnimatorProxy.ofFloat(textView, "scaleX", 0)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "scaleY", 0)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "alpha", 0)); + if (textView.getAlpha() != 0) { + animators.add(ObjectAnimator.ofFloat(textView, "scaleX", 0)); + animators.add(ObjectAnimator.ofFloat(textView, "scaleY", 0)); + animators.add(ObjectAnimator.ofFloat(textView, "alpha", 0)); } textView = dotTextViews.get(a); - if (ViewProxy.getAlpha(textView) != 0) { - animators.add(ObjectAnimatorProxy.ofFloat(textView, "scaleX", 0)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "scaleY", 0)); - animators.add(ObjectAnimatorProxy.ofFloat(textView, "alpha", 0)); + if (textView.getAlpha() != 0) { + animators.add(ObjectAnimator.ofFloat(textView, "scaleX", 0)); + animators.add(ObjectAnimator.ofFloat(textView, "scaleY", 0)); + animators.add(ObjectAnimator.ofFloat(textView, "alpha", 0)); } } - currentAnimation = new AnimatorSetProxy(); + currentAnimation = new AnimatorSet(); currentAnimation.setDuration(150); currentAnimation.playTogether(animators); currentAnimation.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (currentAnimation != null && currentAnimation.equals(animation)) { currentAnimation = null; } @@ -355,10 +354,8 @@ public class PasscodeView extends FrameLayout { currentAnimation.start(); } else { for (int a = 0; a < 4; a++) { - TextView textView = characterTextViews.get(a); - ViewProxy.setAlpha(textView, 0); - textView = dotTextViews.get(a); - ViewProxy.setAlpha(textView, 0); + characterTextViews.get(a).setAlpha(0); + dotTextViews.get(a).setAlpha(0); } } } @@ -377,23 +374,21 @@ public class PasscodeView extends FrameLayout { for (int a = 0; a < 4; a++) { if (a < stringBuilder.length()) { TextView textView = characterTextViews.get(a); - ViewProxy.setAlpha(textView, 0); - ViewProxy.setScaleX(textView, 1); - ViewProxy.setScaleY(textView, 1); - ViewProxy.setTranslationY(textView, 0); - ViewProxy.setTranslationX(textView, getXForTextView(a)); + textView.setAlpha(0); + textView.setScaleX(1); + textView.setScaleY(1); + textView.setTranslationY(0); + textView.setTranslationX(getXForTextView(a)); textView = dotTextViews.get(a); - ViewProxy.setAlpha(textView, 1); - ViewProxy.setScaleX(textView, 1); - ViewProxy.setScaleY(textView, 1); - ViewProxy.setTranslationY(textView, 0); - ViewProxy.setTranslationX(textView, getXForTextView(a)); + textView.setAlpha(1); + textView.setScaleX(1); + textView.setScaleY(1); + textView.setTranslationY(0); + textView.setTranslationX(getXForTextView(a)); } else { - TextView textView = characterTextViews.get(a); - ViewProxy.setAlpha(textView, 0); - textView = dotTextViews.get(a); - ViewProxy.setAlpha(textView, 0); + characterTextViews.get(a).setAlpha(0); + dotTextViews.get(a).setAlpha(0); } } super.onLayout(changed, left, top, right, bottom); @@ -535,30 +530,22 @@ public class PasscodeView extends FrameLayout { } } }); - if (android.os.Build.VERSION.SDK_INT < 11) { - passwordEditText.setOnCreateContextMenuListener(new View.OnCreateContextMenuListener() { - public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { - menu.clear(); - } - }); - } else { - passwordEditText.setCustomSelectionActionModeCallback(new ActionMode.Callback() { - public boolean onPrepareActionMode(ActionMode mode, Menu menu) { - return false; - } + passwordEditText.setCustomSelectionActionModeCallback(new ActionMode.Callback() { + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + return false; + } - public void onDestroyActionMode(ActionMode mode) { - } + public void onDestroyActionMode(ActionMode mode) { + } - public boolean onCreateActionMode(ActionMode mode, Menu menu) { - return false; - } + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + return false; + } - public boolean onActionItemClicked(ActionMode mode, MenuItem item) { - return false; - } - }); - } + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + return false; + } + }); checkImage = new ImageView(context); checkImage.setImageResource(R.drawable.passcode_check); @@ -764,23 +751,18 @@ public class PasscodeView extends FrameLayout { passwordEditText.clearFocus(); AndroidUtilities.hideKeyboard(passwordEditText); - if (Build.VERSION.SDK_INT >= 14) { - AnimatorSetProxy animatorSetProxy = new AnimatorSetProxy(); - animatorSetProxy.setDuration(200); - animatorSetProxy.playTogether( - ObjectAnimatorProxy.ofFloat(this, "translationY", AndroidUtilities.dp(20)), - ObjectAnimatorProxy.ofFloat(this, "alpha", AndroidUtilities.dp(0.0f))); - animatorSetProxy.addListener(new AnimatorListenerAdapterProxy() { - @Override - public void onAnimationEnd(Object animation) { - PasscodeView.this.clearAnimation(); - setVisibility(View.GONE); - } - }); - animatorSetProxy.start(); - } else { - setVisibility(View.GONE); - } + AnimatorSet AnimatorSet = new AnimatorSet(); + AnimatorSet.setDuration(200); + AnimatorSet.playTogether( + ObjectAnimator.ofFloat(this, "translationY", AndroidUtilities.dp(20)), + ObjectAnimator.ofFloat(this, "alpha", AndroidUtilities.dp(0.0f))); + AnimatorSet.addListener(new AnimatorListenerAdapterProxy() { + @Override + public void onAnimationEnd(Animator animation) { + setVisibility(View.GONE); + } + }); + AnimatorSet.start(); UserConfig.appLocked = false; UserConfig.saveConfig(false); @@ -793,19 +775,18 @@ public class PasscodeView extends FrameLayout { private void shakeTextView(final float x, final int num) { if (num == 6) { - passcodeTextView.clearAnimation(); return; } - AnimatorSetProxy animatorSetProxy = new AnimatorSetProxy(); - animatorSetProxy.playTogether(ObjectAnimatorProxy.ofFloat(passcodeTextView, "translationX", AndroidUtilities.dp(x))); - animatorSetProxy.setDuration(50); - animatorSetProxy.addListener(new AnimatorListenerAdapterProxy() { + AnimatorSet AnimatorSet = new AnimatorSet(); + AnimatorSet.playTogether(ObjectAnimator.ofFloat(passcodeTextView, "translationX", AndroidUtilities.dp(x))); + AnimatorSet.setDuration(50); + AnimatorSet.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { shakeTextView(num == 5 ? 0 : -x, num + 1); } }); - animatorSetProxy.start(); + AnimatorSet.start(); } private void onPasscodeError() { @@ -986,11 +967,8 @@ public class PasscodeView extends FrameLayout { if (getVisibility() == View.VISIBLE) { return; } - if (Build.VERSION.SDK_INT >= 14) { - ViewProxy.setAlpha(this, 1.0f); - ViewProxy.setTranslationY(this, 0); - this.clearAnimation(); - } + setAlpha(1.0f); + setTranslationY(0); SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE); int selectedBackground = preferences.getInt("selectedBackground", 1000001); if (selectedBackground == 1000001) { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/PhotoFilterView.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/PhotoFilterView.java index c5a162982..5158eeede 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/PhotoFilterView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/PhotoFilterView.java @@ -8,6 +8,9 @@ package org.telegram.ui.Components; +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Bitmap; @@ -36,10 +39,7 @@ import org.telegram.messenger.DispatchQueue; import org.telegram.messenger.FileLog; import org.telegram.messenger.R; import org.telegram.messenger.Utilities; -import org.telegram.messenger.AnimationCompat.AnimatorListenerAdapterProxy; -import org.telegram.messenger.AnimationCompat.AnimatorSetProxy; -import org.telegram.messenger.AnimationCompat.ObjectAnimatorProxy; -import org.telegram.messenger.AnimationCompat.ViewProxy; +import org.telegram.messenger.AnimatorListenerAdapterProxy; import org.telegram.ui.ActionBar.Theme; import org.telegram.ui.Cells.PhotoEditToolCell; @@ -2090,9 +2090,8 @@ public class PhotoFilterView extends FrameLayout { layoutManager.setOrientation(LinearLayoutManager.HORIZONTAL); recyclerListView.setLayoutManager(layoutManager); recyclerListView.setClipToPadding(false); - if (Build.VERSION.SDK_INT >= 9) { - recyclerListView.setOverScrollMode(RecyclerListView.OVER_SCROLL_NEVER); - } + recyclerListView.setTag(12); + recyclerListView.setOverScrollMode(RecyclerListView.OVER_SCROLL_NEVER); recyclerListView.setAdapter(toolsAdapter = new ToolsAdapter(context)); toolsView.addView(recyclerListView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 60, Gravity.LEFT | Gravity.TOP)); recyclerListView.setOnItemClickListener(new RecyclerListView.OnItemClickListener() { @@ -2595,26 +2594,24 @@ public class PhotoFilterView extends FrameLayout { curvesControl.setVisibility(INVISIBLE); } - AnimatorSetProxy animatorSet = new AnimatorSetProxy(); + AnimatorSet animatorSet = new AnimatorSet(); animatorSet.playTogether( - ObjectAnimatorProxy.ofFloat(viewFrom, "translationY", 0, AndroidUtilities.dp(126)) + ObjectAnimator.ofFloat(viewFrom, "translationY", 0, AndroidUtilities.dp(126)) ); animatorSet.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { - viewFrom.clearAnimation(); + public void onAnimationEnd(Animator animation) { viewFrom.setVisibility(GONE); viewTo.setVisibility(VISIBLE); - ViewProxy.setTranslationY(viewTo, AndroidUtilities.dp(126)); + viewTo.setTranslationY(AndroidUtilities.dp(126)); - AnimatorSetProxy animatorSet = new AnimatorSetProxy(); + AnimatorSet animatorSet = new AnimatorSet(); animatorSet.playTogether( - ObjectAnimatorProxy.ofFloat(viewTo, "translationY", 0) + ObjectAnimator.ofFloat(viewTo, "translationY", 0) ); animatorSet.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { - viewTo.clearAnimation(); + public void onAnimationEnd(Animator animation) { if (selectedTool == enhanceTool) { checkEnhance(); } @@ -2799,10 +2796,10 @@ public class PhotoFilterView extends FrameLayout { private void checkEnhance() { if (enhanceValue == 0) { - AnimatorSetProxy animatorSetProxy = new AnimatorSetProxy(); - animatorSetProxy.setDuration(200); - animatorSetProxy.playTogether(ObjectAnimatorProxy.ofInt(valueSeekBar, "progress", 50)); - animatorSetProxy.start(); + AnimatorSet AnimatorSet = new AnimatorSet(); + AnimatorSet.setDuration(200); + AnimatorSet.playTogether(ObjectAnimator.ofInt(valueSeekBar, "progress", 50)); + AnimatorSet.start(); } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/PhotoViewerCaptionEnterView.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/PhotoViewerCaptionEnterView.java index c8c4bfe29..4d5808c6b 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/PhotoViewerCaptionEnterView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/PhotoViewerCaptionEnterView.java @@ -8,6 +8,8 @@ package org.telegram.ui.Components; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; import android.content.Context; import android.os.Build; import android.text.Editable; @@ -35,13 +37,11 @@ import org.telegram.messenger.ApplicationLoader; import org.telegram.messenger.FileLog; import org.telegram.messenger.R; import org.telegram.tgnet.TLRPC; -import org.telegram.messenger.AnimationCompat.AnimatorSetProxy; -import org.telegram.messenger.AnimationCompat.ObjectAnimatorProxy; import java.lang.reflect.Field; import java.lang.reflect.Method; -public class PhotoViewerCaptionEnterView extends FrameLayoutFixed implements NotificationCenter.NotificationCenterDelegate, SizeNotifierFrameLayoutPhoto.SizeNotifierFrameLayoutPhotoDelegate { +public class PhotoViewerCaptionEnterView extends FrameLayout implements NotificationCenter.NotificationCenterDelegate, SizeNotifierFrameLayoutPhoto.SizeNotifierFrameLayoutPhotoDelegate { public interface PhotoViewerCaptionEnterViewDelegate { void onCaptionEnter(); @@ -56,9 +56,9 @@ public class PhotoViewerCaptionEnterView extends FrameLayoutFixed implements Not private ActionMode currentActionMode; - private AnimatorSetProxy runningAnimation; - private AnimatorSetProxy runningAnimation2; - private ObjectAnimatorProxy runningAnimationAudio; + private AnimatorSet runningAnimation; + private AnimatorSet runningAnimation2; + private ObjectAnimator runningAnimationAudio; private int runningAnimationType; private int audioInterfaceState; @@ -89,7 +89,7 @@ public class PhotoViewerCaptionEnterView extends FrameLayoutFixed implements Not textFieldContainer.setOrientation(LinearLayout.HORIZONTAL); addView(textFieldContainer, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, Gravity.LEFT | Gravity.TOP, 2, 0, 0, 0)); - FrameLayoutFixed frameLayout = new FrameLayoutFixed(context); + FrameLayout frameLayout = new FrameLayout(context); textFieldContainer.addView(frameLayout, LayoutHelper.createLinear(0, LayoutHelper.WRAP_CONTENT, 1.0f)); emojiButton = new ImageView(context); @@ -108,7 +108,17 @@ public class PhotoViewerCaptionEnterView extends FrameLayoutFixed implements Not } }); - messageEditText = new EditText(context); + messageEditText = new EditText(context) { + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + try { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } catch (Exception e) { + setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), AndroidUtilities.dp(51)); + FileLog.e("tmessages", e); + } + } + }; if (Build.VERSION.SDK_INT >= 23) { messageEditText.setCustomSelectionActionModeCallback(new ActionMode.Callback() { @Override diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/PlayerView.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/PlayerView.java index 18cbeaf85..c25a465aa 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/PlayerView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/PlayerView.java @@ -8,9 +8,11 @@ package org.telegram.ui.Components; +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; import android.content.Context; import android.graphics.Canvas; -import android.os.Build; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.TextUtils; @@ -23,10 +25,7 @@ import android.widget.ImageView; import android.widget.TextView; import org.telegram.messenger.AndroidUtilities; -import org.telegram.messenger.AnimationCompat.AnimatorListenerAdapterProxy; -import org.telegram.messenger.AnimationCompat.AnimatorSetProxy; -import org.telegram.messenger.AnimationCompat.ObjectAnimatorProxy; -import org.telegram.messenger.AnimationCompat.ViewProxy; +import org.telegram.messenger.AnimatorListenerAdapterProxy; import org.telegram.messenger.MediaController; import org.telegram.messenger.MessageObject; import org.telegram.messenger.NotificationCenter; @@ -40,7 +39,7 @@ public class PlayerView extends FrameLayout implements NotificationCenter.Notifi private ImageView playButton; private TextView titleTextView; private MessageObject lastMessageObject; - private AnimatorSetProxy animatorSet; + private AnimatorSet animatorSet; private float yPosition; private BaseFragment fragment; private float topPadding; @@ -165,7 +164,6 @@ public class PlayerView extends FrameLayout implements NotificationCenter.Notifi if (visible) { visible = false; if (create) { - clearAnimation(); if (getVisibility() != GONE) { setVisibility(GONE); } @@ -175,15 +173,14 @@ public class PlayerView extends FrameLayout implements NotificationCenter.Notifi animatorSet.cancel(); animatorSet = null; } - animatorSet = new AnimatorSetProxy(); - animatorSet.playTogether(ObjectAnimatorProxy.ofFloat(this, "translationY", -AndroidUtilities.dp(36)), - ObjectAnimatorProxy.ofFloat(this, "topPadding", 0)); + animatorSet = new AnimatorSet(); + animatorSet.playTogether(ObjectAnimator.ofFloat(this, "translationY", -AndroidUtilities.dp(36)), + ObjectAnimator.ofFloat(this, "topPadding", 0)); animatorSet.setDuration(200); animatorSet.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (animatorSet != null && animatorSet.equals(animation)) { - clearAnimation(); setVisibility(GONE); animatorSet = null; } @@ -195,7 +192,7 @@ public class PlayerView extends FrameLayout implements NotificationCenter.Notifi } else { if (create && topPadding == 0) { setTopPadding(AndroidUtilities.dp(36)); - ViewProxy.setTranslationY(this, 0); + setTranslationY(0); yPosition = 0; } if (!visible) { @@ -204,13 +201,13 @@ public class PlayerView extends FrameLayout implements NotificationCenter.Notifi animatorSet.cancel(); animatorSet = null; } - animatorSet = new AnimatorSetProxy(); - animatorSet.playTogether(ObjectAnimatorProxy.ofFloat(this, "translationY", -AndroidUtilities.dp(36), 0), - ObjectAnimatorProxy.ofFloat(this, "topPadding", AndroidUtilities.dp(36))); + animatorSet = new AnimatorSet(); + animatorSet.playTogether(ObjectAnimator.ofFloat(this, "translationY", -AndroidUtilities.dp(36), 0), + ObjectAnimator.ofFloat(this, "topPadding", AndroidUtilities.dp(36))); animatorSet.setDuration(200); animatorSet.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (animatorSet != null && animatorSet.equals(animation)) { animatorSet = null; } @@ -252,17 +249,12 @@ public class PlayerView extends FrameLayout implements NotificationCenter.Notifi @Override protected boolean drawChild(Canvas canvas, View child, long drawingTime) { - int restoreToCount = 0; - if (Build.VERSION.SDK_INT >= 11) { - restoreToCount = canvas.save(); - if (yPosition < 0) { - canvas.clipRect(0, (int) -yPosition, child.getMeasuredWidth(), AndroidUtilities.dp(39)); - } + int restoreToCount = canvas.save(); + if (yPosition < 0) { + canvas.clipRect(0, (int) -yPosition, child.getMeasuredWidth(), AndroidUtilities.dp(39)); } final boolean result = super.drawChild(canvas, child, drawingTime); - if (Build.VERSION.SDK_INT >= 11) { - canvas.restoreToCount(restoreToCount); - } + canvas.restoreToCount(restoreToCount); return result; } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/RadioButton.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/RadioButton.java index 93bd1efff..8299a6fd7 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/RadioButton.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/RadioButton.java @@ -8,6 +8,7 @@ package org.telegram.ui.Components; +import android.animation.ObjectAnimator; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; @@ -18,9 +19,7 @@ import android.graphics.PorterDuffXfermode; import android.view.View; import org.telegram.messenger.AndroidUtilities; -import org.telegram.messenger.AnimationCompat.ObjectAnimatorProxy; import org.telegram.messenger.FileLog; -import org.telegram.messenger.ImageLoader; import org.telegram.ui.ActionBar.Theme; public class RadioButton extends View { @@ -35,7 +34,7 @@ public class RadioButton extends View { private int color = Theme.ACTION_BAR_SUBTITLE_COLOR; private float progress; - private ObjectAnimatorProxy checkAnimator; + private ObjectAnimator checkAnimator; private boolean attachedToWindow; private boolean isChecked; @@ -55,9 +54,6 @@ public class RadioButton extends View { try { bitmap = Bitmap.createBitmap(AndroidUtilities.dp(size), AndroidUtilities.dp(size), Bitmap.Config.ARGB_4444); - if (ImageLoader.getInstance().runtimeHack != null) { - ImageLoader.getInstance().runtimeHack.trackFree(bitmap.getRowBytes() * bitmap.getHeight()); - } bitmapCanvas = new Canvas(bitmap); } catch (Throwable e) { FileLog.e("tmessages", e); @@ -96,7 +92,7 @@ public class RadioButton extends View { } private void animateToCheckedState(boolean newCheckedState) { - checkAnimator = ObjectAnimatorProxy.ofFloatProxy(this, "progress", newCheckedState ? 1 : 0); + checkAnimator = ObjectAnimator.ofFloat(this, "progress", newCheckedState ? 1 : 0); checkAnimator.setDuration(200); checkAnimator.start(); } @@ -111,11 +107,6 @@ public class RadioButton extends View { protected void onDetachedFromWindow() { super.onDetachedFromWindow(); attachedToWindow = false; - if (bitmap != null && ImageLoader.getInstance().runtimeHack != null) { - ImageLoader.getInstance().runtimeHack.trackAlloc(bitmap.getRowBytes() * bitmap.getHeight()); - bitmap.recycle(); - bitmap = null; - } } public void setChecked(boolean checked, boolean animated) { @@ -140,16 +131,10 @@ public class RadioButton extends View { protected void onDraw(Canvas canvas) { if (bitmap == null || bitmap.getWidth() != getMeasuredWidth()) { if (bitmap != null) { - if (ImageLoader.getInstance().runtimeHack != null) { - ImageLoader.getInstance().runtimeHack.trackAlloc(bitmap.getRowBytes() * bitmap.getHeight()); - } bitmap.recycle(); } try { bitmap = Bitmap.createBitmap(getMeasuredWidth(), getMeasuredHeight(), Bitmap.Config.ARGB_8888); - if (ImageLoader.getInstance().runtimeHack != null) { - ImageLoader.getInstance().runtimeHack.trackFree(bitmap.getRowBytes() * bitmap.getHeight()); - } bitmapCanvas = new Canvas(bitmap); } catch (Throwable e) { FileLog.e("tmessages", e); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/SeekBarWaveform.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/SeekBarWaveform.java index 776fbe48c..35f47941b 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/SeekBarWaveform.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/SeekBarWaveform.java @@ -103,7 +103,7 @@ public class SeekBarWaveform { } } if (startX != -1 && Math.abs(x - startX) > AndroidUtilities.getPixelsInCM(0.2f, true)) { - if (parentView != null) { + if (parentView != null && parentView.getParent() != null) { parentView.getParent().requestDisallowInterceptTouchEvent(true); } startDraging = true; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/ShareAlert.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/ShareAlert.java index 43e0ac00a..c4967829d 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/ShareAlert.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/ShareAlert.java @@ -75,7 +75,7 @@ public class ShareAlert extends BottomSheet { private MessageObject sendingMessageObject; private EmptyTextProgressView searchEmptyView; private Drawable shadowDrawable; - private HashMap selectedDialogs = new HashMap<>(); + private HashMap selectedDialogs = new HashMap<>(); private TLRPC.TL_exportedMessageLink exportedMessageLink; private boolean loadingLink; @@ -145,25 +145,19 @@ public class ShareAlert extends BottomSheet { } int size = Math.max(searchAdapter.getItemCount(), listAdapter.getItemCount()); int contentSize = AndroidUtilities.dp(48) + Math.max(3, (int) Math.ceil(size / 4.0f)) * AndroidUtilities.dp(100) + backgroundPaddingTop; - if (Build.VERSION.SDK_INT < 11) { - super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(Math.min(contentSize, AndroidUtilities.displaySize.y / 5 * 3), MeasureSpec.EXACTLY)); - } else { - int padding = contentSize < height ? 0 : height - (height / 5 * 3) + AndroidUtilities.dp(8); - if (gridView.getPaddingTop() != padding) { - ignoreLayout = true; - gridView.setPadding(0, padding, 0, AndroidUtilities.dp(8)); - ignoreLayout = false; - } - super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(Math.min(contentSize, height), MeasureSpec.EXACTLY)); + int padding = contentSize < height ? 0 : height - (height / 5 * 3) + AndroidUtilities.dp(8); + if (gridView.getPaddingTop() != padding) { + ignoreLayout = true; + gridView.setPadding(0, padding, 0, AndroidUtilities.dp(8)); + ignoreLayout = false; } + super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(Math.min(contentSize, height), MeasureSpec.EXACTLY)); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); - if (Build.VERSION.SDK_INT >= 11) { - updateLayout(); - } + updateLayout(); } @Override @@ -176,18 +170,12 @@ public class ShareAlert extends BottomSheet { @Override protected void onDraw(Canvas canvas) { - if (Build.VERSION.SDK_INT >= 11) { - shadowDrawable.setBounds(0, scrollOffsetY - backgroundPaddingTop, getMeasuredWidth(), getMeasuredHeight()); - shadowDrawable.draw(canvas); - } + shadowDrawable.setBounds(0, scrollOffsetY - backgroundPaddingTop, getMeasuredWidth(), getMeasuredHeight()); + shadowDrawable.draw(canvas); } }; - if (Build.VERSION.SDK_INT < 11) { - containerView.setBackgroundDrawable(shadowDrawable); - } else { - containerView.setWillNotDraw(false); - } - containerView.setPadding(backgroundPaddingLeft, Build.VERSION.SDK_INT < 11 ? backgroundPaddingTop : 0, backgroundPaddingLeft, 0); + containerView.setWillNotDraw(false); + containerView.setPadding(backgroundPaddingLeft, 0, backgroundPaddingLeft, 0); frameLayout = new FrameLayout(context); frameLayout.setBackgroundColor(0xffffffff); @@ -217,17 +205,8 @@ public class ShareAlert extends BottomSheet { } else { ArrayList arrayList = new ArrayList<>(); arrayList.add(sendingMessageObject); - for (HashMap.Entry entry : selectedDialogs.entrySet()) { - TLRPC.Dialog dialog = entry.getValue(); - boolean asAdmin = true; - int lower_id = (int) dialog.id; - if (lower_id < 0) { - TLRPC.Chat chat = MessagesController.getInstance().getChat(-lower_id); - if (chat.megagroup) { - asAdmin = false; - } - } - SendMessagesHelper.getInstance().sendMessage(arrayList, entry.getKey(), asAdmin); + for (HashMap.Entry entry : selectedDialogs.entrySet()) { + SendMessagesHelper.getInstance().sendMessage(arrayList, entry.getKey()); } dismiss(); } @@ -311,6 +290,7 @@ public class ShareAlert extends BottomSheet { }); gridView = new RecyclerListView(context); + gridView.setTag(13); gridView.setPadding(0, 0, 0, AndroidUtilities.dp(8)); gridView.setClipToPadding(false); gridView.setLayoutManager(layoutManager = new GridLayoutManager(getContext(), 4)); @@ -336,7 +316,7 @@ public class ShareAlert extends BottomSheet { gridView.setOnItemClickListener(new RecyclerListView.OnItemClickListener() { @Override public void onItemClick(View view, int position) { - TLRPC.Dialog dialog; + TLRPC.TL_dialog dialog; if (gridView.getAdapter() == listAdapter) { dialog = listAdapter.getItem(position); } else { @@ -356,15 +336,12 @@ public class ShareAlert extends BottomSheet { updateSelectedCount(); } }); - if (Build.VERSION.SDK_INT >= 11) { - gridView.setOnScrollListener(new RecyclerView.OnScrollListener() { - @SuppressLint("NewApi") - @Override - public void onScrolled(RecyclerView recyclerView, int dx, int dy) { - updateLayout(); - } - }); - } + gridView.setOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + updateLayout(); + } + }); searchEmptyView = new EmptyTextProgressView(context); searchEmptyView.setShowAtCenter(true); @@ -421,14 +398,9 @@ public class ShareAlert extends BottomSheet { return; } try { - if (Build.VERSION.SDK_INT < 11) { - android.text.ClipboardManager clipboard = (android.text.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); - clipboard.setText(exportedMessageLink.link); - } else { - android.content.ClipboardManager clipboard = (android.content.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); - android.content.ClipData clip = android.content.ClipData.newPlainText("label", exportedMessageLink.link); - clipboard.setPrimaryClip(clip); - } + android.content.ClipboardManager clipboard = (android.content.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); + android.content.ClipData clip = android.content.ClipData.newPlainText("label", exportedMessageLink.link); + clipboard.setPrimaryClip(clip); Toast.makeText(context, LocaleController.getString("LinkCopied", R.string.LinkCopied), Toast.LENGTH_SHORT).show(); } catch (Exception e) { FileLog.e("tmessages", e); @@ -468,12 +440,12 @@ public class ShareAlert extends BottomSheet { private Context context; private int currentCount; - private ArrayList dialogs = new ArrayList<>(); + private ArrayList dialogs = new ArrayList<>(); public ShareDialogsAdapter(Context context) { this.context = context; for (int a = 0; a < MessagesController.getInstance().dialogsServerOnly.size(); a++) { - TLRPC.Dialog dialog = MessagesController.getInstance().dialogsServerOnly.get(a); + TLRPC.TL_dialog dialog = MessagesController.getInstance().dialogsServerOnly.get(a); int lower_id = (int) dialog.id; int high_id = (int) (dialog.id >> 32); if (lower_id != 0 && high_id != 1) { @@ -494,7 +466,7 @@ public class ShareAlert extends BottomSheet { return dialogs.size(); } - public TLRPC.Dialog getItem(int i) { + public TLRPC.TL_dialog getItem(int i) { if (i < 0 || i >= dialogs.size()) { return null; } @@ -516,7 +488,7 @@ public class ShareAlert extends BottomSheet { @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { ShareDialogCell cell = (ShareDialogCell) holder.itemView; - TLRPC.Dialog dialog = getItem(position); + TLRPC.TL_dialog dialog = getItem(position); cell.setDialog((int) dialog.id, selectedDialogs.containsKey(dialog.id), null); } @@ -537,7 +509,7 @@ public class ShareAlert extends BottomSheet { private int lastSearchId = 0; private class DialogSearchResult { - public TLRPC.Dialog dialog = new TLRPC.Dialog(); + public TLRPC.TL_dialog dialog = new TLRPC.TL_dialog(); public TLObject object; public int date; public CharSequence name; @@ -818,7 +790,7 @@ public class ShareAlert extends BottomSheet { return searchResult.size(); } - public TLRPC.Dialog getItem(int i) { + public TLRPC.TL_dialog getItem(int i) { return searchResult.get(i).dialog; } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/StickersAlert.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/StickersAlert.java index 0e5993d8f..0461ec5b4 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/StickersAlert.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/StickersAlert.java @@ -8,6 +8,9 @@ package org.telegram.ui.Components; +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.*; @@ -27,10 +30,7 @@ import android.widget.TextView; import android.widget.Toast; import org.telegram.messenger.AndroidUtilities; -import org.telegram.messenger.AnimationCompat.AnimatorListenerAdapterProxy; -import org.telegram.messenger.AnimationCompat.AnimatorSetProxy; -import org.telegram.messenger.AnimationCompat.ObjectAnimatorProxy; -import org.telegram.messenger.AnimationCompat.ViewProxy; +import org.telegram.messenger.AnimatorListenerAdapterProxy; import org.telegram.messenger.Emoji; import org.telegram.messenger.FileLog; import org.telegram.messenger.LocaleController; @@ -66,7 +66,7 @@ public class StickersAlert extends BottomSheet implements NotificationCenter.Not private TextView stickerEmojiTextView; private RecyclerListView.OnItemClickListener stickersOnItemClickListener; private Drawable shadowDrawable; - private AnimatorSetProxy shadowAnimation[] = new AnimatorSetProxy[2]; + private AnimatorSet shadowAnimation[] = new AnimatorSet[2]; private View shadow[] = new View[2]; private FrameLayout emptyView; @@ -110,32 +110,26 @@ public class StickersAlert extends BottomSheet implements NotificationCenter.Not height -= AndroidUtilities.statusBarHeight; } int contentSize = AndroidUtilities.dp(48 + 48) + Math.max(3, (stickerSet != null ? (int) Math.ceil(stickerSet.documents.size() / 5.0f) : 0)) * AndroidUtilities.dp(82) + backgroundPaddingTop; - if (Build.VERSION.SDK_INT < 11) { - super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(Math.min(contentSize, AndroidUtilities.displaySize.y / 5 * 3), MeasureSpec.EXACTLY)); - } else { - int padding = contentSize < (height / 5 * 3.2) ? 0 : (height / 5 * 2); - if (padding != 0 && contentSize < height) { - padding -= (height - contentSize); - } - if (padding == 0) { - padding = backgroundPaddingTop; - } - if (gridView.getPaddingTop() != padding) { - ignoreLayout = true; - gridView.setPadding(AndroidUtilities.dp(10), padding, AndroidUtilities.dp(10), 0); - emptyView.setPadding(0, padding, 0, 0); - ignoreLayout = false; - } - super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(Math.min(contentSize, height), MeasureSpec.EXACTLY)); + int padding = contentSize < (height / 5 * 3.2) ? 0 : (height / 5 * 2); + if (padding != 0 && contentSize < height) { + padding -= (height - contentSize); } + if (padding == 0) { + padding = backgroundPaddingTop; + } + if (gridView.getPaddingTop() != padding) { + ignoreLayout = true; + gridView.setPadding(AndroidUtilities.dp(10), padding, AndroidUtilities.dp(10), 0); + emptyView.setPadding(0, padding, 0, 0); + ignoreLayout = false; + } + super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(Math.min(contentSize, height), MeasureSpec.EXACTLY)); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); - if (Build.VERSION.SDK_INT >= 11) { - updateLayout(); - } + updateLayout(); } @Override @@ -148,18 +142,12 @@ public class StickersAlert extends BottomSheet implements NotificationCenter.Not @Override protected void onDraw(Canvas canvas) { - if (Build.VERSION.SDK_INT >= 11) { - shadowDrawable.setBounds(0, scrollOffsetY - backgroundPaddingTop, getMeasuredWidth(), getMeasuredHeight()); - shadowDrawable.draw(canvas); - } + shadowDrawable.setBounds(0, scrollOffsetY - backgroundPaddingTop, getMeasuredWidth(), getMeasuredHeight()); + shadowDrawable.draw(canvas); } }; - if (Build.VERSION.SDK_INT < 11) { - containerView.setBackgroundDrawable(shadowDrawable); - } else { - containerView.setWillNotDraw(false); - } - containerView.setPadding(backgroundPaddingLeft, Build.VERSION.SDK_INT < 11 ? backgroundPaddingTop : 0, backgroundPaddingLeft, 0); + containerView.setWillNotDraw(false); + containerView.setPadding(backgroundPaddingLeft, 0, backgroundPaddingLeft, 0); titleTextView = new TextView(context); titleTextView.setLines(1); @@ -180,8 +168,7 @@ public class StickersAlert extends BottomSheet implements NotificationCenter.Not shadow[0] = new View(context); shadow[0].setBackgroundResource(R.drawable.header_shadow); - ViewProxy.setAlpha(shadow[0], 0.0f); - shadow[0].clearAnimation(); + shadow[0].setAlpha(0.0f); shadow[0].setVisibility(View.INVISIBLE); shadow[0].setTag(1); containerView.addView(shadow[0], LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 3, Gravity.TOP | Gravity.LEFT, 0, 48, 0, 0)); @@ -201,6 +188,7 @@ public class StickersAlert extends BottomSheet implements NotificationCenter.Not super.requestLayout(); } }; + gridView.setTag(14); gridView.setLayoutManager(layoutManager = new GridLayoutManager(getContext(), 5)); gridView.setAdapter(adapter = new GridAdapter(context)); gridView.setVerticalScrollBarEnabled(false); @@ -223,15 +211,12 @@ public class StickersAlert extends BottomSheet implements NotificationCenter.Not return StickerPreviewViewer.getInstance().onTouch(event, gridView, 0, stickersOnItemClickListener); } }); - if (Build.VERSION.SDK_INT >= 11) { - gridView.setOnScrollListener(new RecyclerView.OnScrollListener() { - @SuppressLint("NewApi") - @Override - public void onScrolled(RecyclerView recyclerView, int dx, int dy) { - updateLayout(); - } - }); - } + gridView.setOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + updateLayout(); + } + }); stickersOnItemClickListener = new RecyclerListView.OnItemClickListener() { @Override public void onItemClick(View view, int position) { @@ -260,8 +245,8 @@ public class StickersAlert extends BottomSheet implements NotificationCenter.Not layoutParams.topMargin = scrollOffsetY; stickerPreviewLayout.setLayoutParams(layoutParams); stickerPreviewLayout.setVisibility(View.VISIBLE); - AnimatorSetProxy animatorSet = new AnimatorSetProxy(); - animatorSet.playTogether(ObjectAnimatorProxy.ofFloat(stickerPreviewLayout, "alpha", 0.0f, 1.0f)); + AnimatorSet animatorSet = new AnimatorSet(); + animatorSet.playTogether(ObjectAnimator.ofFloat(stickerPreviewLayout, "alpha", 0.0f, 1.0f)); animatorSet.setDuration(200); animatorSet.start(); } @@ -500,12 +485,12 @@ public class StickersAlert extends BottomSheet implements NotificationCenter.Not } private void hidePreview() { - AnimatorSetProxy animatorSet = new AnimatorSetProxy(); - animatorSet.playTogether(ObjectAnimatorProxy.ofFloat(stickerPreviewLayout, "alpha", 0.0f)); + AnimatorSet animatorSet = new AnimatorSet(); + animatorSet.playTogether(ObjectAnimator.ofFloat(stickerPreviewLayout, "alpha", 0.0f)); animatorSet.setDuration(200); animatorSet.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { stickerPreviewLayout.setVisibility(View.GONE); } }); @@ -521,14 +506,13 @@ public class StickersAlert extends BottomSheet implements NotificationCenter.Not if (shadowAnimation[num] != null) { shadowAnimation[num].cancel(); } - shadowAnimation[num] = new AnimatorSetProxy(); - shadowAnimation[num].playTogether(ObjectAnimatorProxy.ofFloat(shadow[num], "alpha", show ? 1.0f : 0.0f)); + shadowAnimation[num] = new AnimatorSet(); + shadowAnimation[num].playTogether(ObjectAnimator.ofFloat(shadow[num], "alpha", show ? 1.0f : 0.0f)); shadowAnimation[num].setDuration(150); shadowAnimation[num].addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (shadowAnimation[num] != null && shadowAnimation[num].equals(animation)) { - shadow[num].clearAnimation(); if (!show) { shadow[num].setVisibility(View.INVISIBLE); } @@ -537,7 +521,7 @@ public class StickersAlert extends BottomSheet implements NotificationCenter.Not } @Override - public void onAnimationCancel(Object animation) { + public void onAnimationCancel(Animator animation) { if (shadowAnimation[num] != null && shadowAnimation[num].equals(animation)) { shadowAnimation[num] = null; } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/Switch.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/Switch.java index 217e6b368..4488dc4a8 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/Switch.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/Switch.java @@ -16,6 +16,8 @@ package org.telegram.ui.Components; +import android.animation.ObjectAnimator; +import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Canvas; import android.graphics.PorterDuff; @@ -33,7 +35,6 @@ import android.widget.CompoundButton; import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.LocaleController; import org.telegram.messenger.R; -import org.telegram.messenger.AnimationCompat.ObjectAnimatorProxy; public class Switch extends CompoundButton { @@ -89,7 +90,7 @@ public class Switch extends CompoundButton { private int mSwitchRight; private int mSwitchBottom; - private ObjectAnimatorProxy mPositionAnimator; + private ObjectAnimator mPositionAnimator; private final Rect mTempRect = new Rect(); @@ -160,10 +161,6 @@ public class Switch extends CompoundButton { requestLayout(); } - public void setTrackResource(int resId) { - setTrackDrawable(getContext().getDrawable(resId)); - } - public Drawable getTrackDrawable() { return mTrackDrawable; } @@ -179,10 +176,6 @@ public class Switch extends CompoundButton { requestLayout(); } - public void setThumbResource(int resId) { - setThumbDrawable(getContext().getDrawable(resId)); - } - public Drawable getThumbDrawable() { return mThumbDrawable; } @@ -358,7 +351,7 @@ public class Switch extends CompoundButton { private void animateThumbToCheckedState(boolean newCheckedState) { final float targetPosition = newCheckedState ? 1 : 0; - mPositionAnimator = ObjectAnimatorProxy.ofFloatProxy(this, "thumbPosition", targetPosition); + mPositionAnimator = ObjectAnimator.ofFloat(this, "thumbPosition", targetPosition); mPositionAnimator.setDuration(THUMB_ANIMATION_DURATION); mPositionAnimator.start(); } @@ -653,6 +646,7 @@ public class Switch extends CompoundButton { invalidate(); } + @SuppressLint("NewApi") @Override public void drawableHotspotChanged(float x, float y) { super.drawableHotspotChanged(x, y); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/VideoPlayer.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/VideoPlayer.java new file mode 100644 index 000000000..2ffd7e31b --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/VideoPlayer.java @@ -0,0 +1,353 @@ +/* + * This is the source code of Telegram for Android v. 3.x.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-2016. + */ + +package org.telegram.ui.Components; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.media.AudioManager; +import android.media.MediaCodec; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.view.Surface; + +import org.telegram.messenger.exoplayer.DummyTrackRenderer; +import org.telegram.messenger.exoplayer.ExoPlaybackException; +import org.telegram.messenger.exoplayer.ExoPlayer; +import org.telegram.messenger.exoplayer.MediaCodecAudioTrackRenderer; +import org.telegram.messenger.exoplayer.MediaCodecSelector; +import org.telegram.messenger.exoplayer.MediaCodecTrackRenderer; +import org.telegram.messenger.exoplayer.MediaCodecVideoTrackRenderer; +import org.telegram.messenger.exoplayer.MediaFormat; +import org.telegram.messenger.exoplayer.TrackRenderer; +import org.telegram.messenger.exoplayer.audio.AudioCapabilities; +import org.telegram.messenger.exoplayer.extractor.ExtractorSampleSource; +import org.telegram.messenger.exoplayer.upstream.Allocator; +import org.telegram.messenger.exoplayer.upstream.DataSource; +import org.telegram.messenger.exoplayer.upstream.DefaultAllocator; +import org.telegram.messenger.exoplayer.upstream.DefaultUriDataSource; +import org.telegram.messenger.exoplayer.util.PlayerControl; + +import java.util.concurrent.CopyOnWriteArrayList; + +@SuppressLint("NewApi") +public class VideoPlayer implements ExoPlayer.Listener, MediaCodecVideoTrackRenderer.EventListener { + + public interface RendererBuilder { + void buildRenderers(VideoPlayer player); + void cancel(); + } + + public interface Listener { + void onStateChanged(boolean playWhenReady, int playbackState); + void onError(Exception e); + void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio); + } + + public static class ExtractorRendererBuilder implements RendererBuilder { + + private static final int BUFFER_SEGMENT_SIZE = 256 * 1024; + private static final int BUFFER_SEGMENT_COUNT = 256; + + private final Context context; + private final String userAgent; + private final Uri uri; + + public ExtractorRendererBuilder(Context context, String userAgent, Uri uri) { + this.context = context; + this.userAgent = userAgent; + this.uri = uri; + } + + @Override + public void buildRenderers(VideoPlayer player) { + Allocator allocator = new DefaultAllocator(BUFFER_SEGMENT_SIZE); + Handler mainHandler = player.getMainHandler(); + + TrackRenderer[] renderers = new TrackRenderer[RENDERER_COUNT]; + DataSource dataSource = new DefaultUriDataSource(context, userAgent); + ExtractorSampleSource sampleSource = new ExtractorSampleSource(uri, dataSource, allocator, BUFFER_SEGMENT_COUNT * BUFFER_SEGMENT_SIZE, mainHandler, null, 0); + renderers[TYPE_VIDEO] = new MediaCodecVideoTrackRenderer(context, sampleSource, MediaCodecSelector.DEFAULT, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, mainHandler, player, 50) { + @Override + protected void doSomeWork(long positionUs, long elapsedRealtimeUs, boolean sourceIsReady) throws ExoPlaybackException { + super.doSomeWork(positionUs, elapsedRealtimeUs, sourceIsReady); + } + }; + renderers[TYPE_AUDIO] = new MediaCodecAudioTrackRenderer(sampleSource, MediaCodecSelector.DEFAULT, null, true, mainHandler, null, AudioCapabilities.getCapabilities(context), AudioManager.STREAM_MUSIC); + player.onRenderers(renderers); + } + + @Override + public void cancel() { + + } + } + + public static final int STATE_IDLE = ExoPlayer.STATE_IDLE; + public static final int STATE_PREPARING = ExoPlayer.STATE_PREPARING; + public static final int STATE_BUFFERING = ExoPlayer.STATE_BUFFERING; + public static final int STATE_READY = ExoPlayer.STATE_READY; + public static final int STATE_ENDED = ExoPlayer.STATE_ENDED; + public static final int TRACK_DISABLED = ExoPlayer.TRACK_DISABLED; + public static final int TRACK_DEFAULT = ExoPlayer.TRACK_DEFAULT; + + public static final int RENDERER_COUNT = 2; + public static final int TYPE_VIDEO = 0; + public static final int TYPE_AUDIO = 1; + + private static final int RENDERER_BUILDING_STATE_IDLE = 1; + private static final int RENDERER_BUILDING_STATE_BUILDING = 2; + private static final int RENDERER_BUILDING_STATE_BUILT = 3; + + private final RendererBuilder rendererBuilder; + private final ExoPlayer player; + private final Handler mainHandler; + private final CopyOnWriteArrayList listeners; + private final PlayerControl playerControl; + + private int rendererBuildingState; + private int lastReportedPlaybackState; + private boolean lastReportedPlayWhenReady; + + private Surface surface; + private TrackRenderer videoRenderer; + private int videoTrackToRestore; + + private boolean backgrounded; + + public VideoPlayer(RendererBuilder rendererBuilder) { + this.rendererBuilder = rendererBuilder; + player = ExoPlayer.Factory.newInstance(RENDERER_COUNT, 1000, 5000); + player.addListener(this); + playerControl = new PlayerControl(player); + mainHandler = new Handler(); + listeners = new CopyOnWriteArrayList<>(); + lastReportedPlaybackState = STATE_IDLE; + rendererBuildingState = RENDERER_BUILDING_STATE_IDLE; + } + + public PlayerControl getPlayerControl() { + return playerControl; + } + + public void addListener(Listener listener) { + listeners.add(listener); + } + + public void removeListener(Listener listener) { + listeners.remove(listener); + } + + public void setSurface(Surface surface) { + this.surface = surface; + pushSurface(false); + } + + public Surface getSurface() { + return surface; + } + + public void blockingClearSurface() { + surface = null; + pushSurface(true); + } + + public int getTrackCount(int type) { + return player.getTrackCount(type); + } + + public MediaFormat getTrackFormat(int type, int index) { + return player.getTrackFormat(type, index); + } + + public int getSelectedTrack(int type) { + return player.getSelectedTrack(type); + } + + public void setSelectedTrack(int type, int index) { + player.setSelectedTrack(type, index); + } + + public boolean getBackgrounded() { + return backgrounded; + } + + public void setBackgrounded(boolean backgrounded) { + if (this.backgrounded == backgrounded) { + return; + } + this.backgrounded = backgrounded; + if (backgrounded) { + videoTrackToRestore = getSelectedTrack(TYPE_VIDEO); + setSelectedTrack(TYPE_VIDEO, TRACK_DISABLED); + blockingClearSurface(); + } else { + setSelectedTrack(TYPE_VIDEO, videoTrackToRestore); + } + } + + public void prepare() { + if (rendererBuildingState == RENDERER_BUILDING_STATE_BUILT) { + player.stop(); + } + rendererBuilder.cancel(); + videoRenderer = null; + rendererBuildingState = RENDERER_BUILDING_STATE_BUILDING; + maybeReportPlayerState(); + rendererBuilder.buildRenderers(this); + } + + void onRenderers(TrackRenderer[] renderers) { + for (int i = 0; i < RENDERER_COUNT; i++) { + if (renderers[i] == null) { + renderers[i] = new DummyTrackRenderer(); + } + } + videoRenderer = renderers[TYPE_VIDEO]; + pushSurface(false); + player.prepare(renderers); + rendererBuildingState = RENDERER_BUILDING_STATE_BUILT; + } + + void onRenderersError(Exception e) { + for (Listener listener : listeners) { + listener.onError(e); + } + rendererBuildingState = RENDERER_BUILDING_STATE_IDLE; + maybeReportPlayerState(); + } + + public void setPlayWhenReady(boolean playWhenReady) { + player.setPlayWhenReady(playWhenReady); + } + + public void seekTo(long positionMs) { + player.seekTo(positionMs); + } + + public void release() { + rendererBuilder.cancel(); + rendererBuildingState = RENDERER_BUILDING_STATE_IDLE; + surface = null; + player.release(); + } + + public int getPlaybackState() { + if (rendererBuildingState == RENDERER_BUILDING_STATE_BUILDING) { + return STATE_PREPARING; + } + int playerState = player.getPlaybackState(); + if (rendererBuildingState == RENDERER_BUILDING_STATE_BUILT && playerState == STATE_IDLE) { + return STATE_PREPARING; + } + return playerState; + } + + public long getCurrentPosition() { + return player.getCurrentPosition(); + } + + public long getDuration() { + return player.getDuration(); + } + + public int getBufferedPercentage() { + return player.getBufferedPercentage(); + } + + public boolean getPlayWhenReady() { + return player.getPlayWhenReady(); + } + + Looper getPlaybackLooper() { + return player.getPlaybackLooper(); + } + + Handler getMainHandler() { + return mainHandler; + } + + @Override + public void onPlayerStateChanged(boolean playWhenReady, int state) { + maybeReportPlayerState(); + } + + @Override + public void onPlayerError(ExoPlaybackException exception) { + rendererBuildingState = RENDERER_BUILDING_STATE_IDLE; + for (Listener listener : listeners) { + listener.onError(exception); + } + } + + @Override + public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { + for (Listener listener : listeners) { + listener.onVideoSizeChanged(width, height, unappliedRotationDegrees, pixelWidthHeightRatio); + } + } + + @Override + public void onDroppedFrames(int count, long elapsed) { + + } + + @Override + public void onDecoderInitializationError(MediaCodecTrackRenderer.DecoderInitializationException e) { + for (Listener listener : listeners) { + listener.onError(e); + } + } + + @Override + public void onDecoderInitialized(String decoderName, long elapsedRealtimeMs, long initializationDurationMs) { + + } + + @Override + public void onPlayWhenReadyCommitted() { + // Do nothing. + } + + @Override + public void onDrawnToSurface(Surface surface) { + // Do nothing. + } + + @Override + public void onCryptoError(MediaCodec.CryptoException e) { + for (Listener listener : listeners) { + listener.onError(e); + } + } + + private void maybeReportPlayerState() { + boolean playWhenReady = player.getPlayWhenReady(); + int playbackState = getPlaybackState(); + if (lastReportedPlayWhenReady != playWhenReady || lastReportedPlaybackState != playbackState) { + for (Listener listener : listeners) { + listener.onStateChanged(playWhenReady, playbackState); + } + lastReportedPlayWhenReady = playWhenReady; + lastReportedPlaybackState = playbackState; + } + } + + private void pushSurface(boolean blockForSurfacePush) { + if (videoRenderer == null) { + return; + } + + if (blockForSurfacePush) { + player.blockingSendMessage(videoRenderer, MediaCodecVideoTrackRenderer.MSG_SET_SURFACE, surface); + } else { + player.sendMessage(videoRenderer, MediaCodecVideoTrackRenderer.MSG_SET_SURFACE, surface); + } + } +} diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/VideoTimelineView.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/VideoTimelineView.java index 539952d42..3a485c185 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/VideoTimelineView.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/VideoTimelineView.java @@ -223,12 +223,7 @@ public class VideoTimelineView extends View { } } }; - - if (android.os.Build.VERSION.SDK_INT >= 11) { - currentTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, frameNum, null, null); - } else { - currentTask.execute(frameNum, null, null); - } + currentTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, frameNum, null, null); } public void destroy() { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/WebFrameLayout.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/WebFrameLayout.java index f43bc62fa..9bc32fc42 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/WebFrameLayout.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/WebFrameLayout.java @@ -277,14 +277,9 @@ public class WebFrameLayout extends FrameLayout { @Override public void onClick(View v) { try { - if (Build.VERSION.SDK_INT < 11) { - android.text.ClipboardManager clipboard = (android.text.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); - clipboard.setText(openUrl); - } else { - android.content.ClipboardManager clipboard = (android.content.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); - android.content.ClipData clip = android.content.ClipData.newPlainText("label", openUrl); - clipboard.setPrimaryClip(clip); - } + android.content.ClipboardManager clipboard = (android.content.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); + android.content.ClipData clip = android.content.ClipData.newPlainText("label", openUrl); + clipboard.setPrimaryClip(clip); } catch (Exception e) { FileLog.e("tmessages", e); } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ContactsActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/ContactsActivity.java index 6785b376d..e5c2b090c 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ContactsActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ContactsActivity.java @@ -15,7 +15,6 @@ import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.net.Uri; -import android.os.Build; import android.os.Bundle; import android.text.Editable; import android.text.InputType; @@ -176,9 +175,7 @@ public class ContactsActivity extends BaseFragment implements NotificationCenter searchWas = false; listView.setAdapter(listViewAdapter); listViewAdapter.notifyDataSetChanged(); - if (android.os.Build.VERSION.SDK_INT >= 11) { - listView.setFastScrollAlwaysVisible(true); - } + listView.setFastScrollAlwaysVisible(true); listView.setFastScrollEnabled(true); listView.setVerticalScrollBarEnabled(false); emptyTextView.setText(LocaleController.getString("NoContacts", R.string.NoContacts)); @@ -195,9 +192,7 @@ public class ContactsActivity extends BaseFragment implements NotificationCenter if (listView != null) { listView.setAdapter(searchListViewAdapter); searchListViewAdapter.notifyDataSetChanged(); - if (android.os.Build.VERSION.SDK_INT >= 11) { - listView.setFastScrollAlwaysVisible(false); - } + listView.setFastScrollAlwaysVisible(false); listView.setFastScrollEnabled(false); listView.setVerticalScrollBarEnabled(true); } @@ -259,10 +254,8 @@ public class ContactsActivity extends BaseFragment implements NotificationCenter listView.setFastScrollEnabled(true); listView.setScrollBarStyle(View.SCROLLBARS_OUTSIDE_OVERLAY); listView.setAdapter(listViewAdapter); - if (Build.VERSION.SDK_INT >= 11) { - listView.setFastScrollAlwaysVisible(true); - listView.setVerticalScrollbarPosition(LocaleController.isRTL ? ListView.SCROLLBAR_POSITION_LEFT : ListView.SCROLLBAR_POSITION_RIGHT); - } + listView.setFastScrollAlwaysVisible(true); + listView.setVerticalScrollbarPosition(LocaleController.isRTL ? ListView.SCROLLBAR_POSITION_LEFT : ListView.SCROLLBAR_POSITION_RIGHT); ((FrameLayout) fragmentView).addView(listView); layoutParams = (FrameLayout.LayoutParams) listView.getLayoutParams(); layoutParams.width = LayoutHelper.MATCH_PARENT; @@ -447,9 +440,6 @@ public class ContactsActivity extends BaseFragment implements NotificationCenter if (!user.bot && needForwardCount) { message = String.format("%s\n\n%s", message, LocaleController.getString("AddToTheGroupForwardCount", R.string.AddToTheGroupForwardCount)); editText = new EditText(getParentActivity()); - if (android.os.Build.VERSION.SDK_INT < 11) { - editText.setBackgroundResource(android.R.drawable.editbox_background_normal); - } editText.setTextSize(18); editText.setText("50"); editText.setGravity(Gravity.CENTER); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/CountrySelectActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/CountrySelectActivity.java index c7e81ac11..da75104f5 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/CountrySelectActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/CountrySelectActivity.java @@ -9,7 +9,6 @@ package org.telegram.ui; import android.content.Context; -import android.os.Build; import android.view.Gravity; import android.view.MotionEvent; import android.view.View; @@ -88,9 +87,7 @@ public class CountrySelectActivity extends BaseFragment { searching = false; searchWas = false; listView.setAdapter(listViewAdapter); - if (android.os.Build.VERSION.SDK_INT >= 11) { - listView.setFastScrollAlwaysVisible(true); - } + listView.setFastScrollAlwaysVisible(true); listView.setFastScrollEnabled(true); listView.setVerticalScrollBarEnabled(false); @@ -105,9 +102,7 @@ public class CountrySelectActivity extends BaseFragment { searchWas = true; if (listView != null) { listView.setAdapter(searchListViewAdapter); - if (android.os.Build.VERSION.SDK_INT >= 11) { - listView.setFastScrollAlwaysVisible(false); - } + listView.setFastScrollAlwaysVisible(false); listView.setFastScrollEnabled(false); listView.setVerticalScrollBarEnabled(true); } @@ -171,10 +166,8 @@ public class CountrySelectActivity extends BaseFragment { listView.setFastScrollEnabled(true); listView.setScrollBarStyle(View.SCROLLBARS_OUTSIDE_OVERLAY); listView.setAdapter(listViewAdapter); - if (Build.VERSION.SDK_INT >= 11) { - listView.setFastScrollAlwaysVisible(true); - listView.setVerticalScrollbarPosition(LocaleController.isRTL ? ListView.SCROLLBAR_POSITION_LEFT : ListView.SCROLLBAR_POSITION_RIGHT); - } + listView.setFastScrollAlwaysVisible(true); + listView.setVerticalScrollbarPosition(LocaleController.isRTL ? ListView.SCROLLBAR_POSITION_LEFT : ListView.SCROLLBAR_POSITION_RIGHT); ((FrameLayout) fragmentView).addView(listView); layoutParams = (FrameLayout.LayoutParams) listView.getLayoutParams(); layoutParams.width = LayoutHelper.MATCH_PARENT; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/DialogsActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/DialogsActivity.java index 70e3223d6..8561d288d 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/DialogsActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/DialogsActivity.java @@ -41,6 +41,7 @@ import android.widget.TextView; import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.BuildVars; import org.telegram.messenger.ChatObject; +import org.telegram.messenger.DialogObject; import org.telegram.messenger.ImageLoader; import org.telegram.messenger.LocaleController; import org.telegram.messenger.MessageObject; @@ -59,8 +60,6 @@ import org.telegram.messenger.UserConfig; import org.telegram.ui.ActionBar.BottomSheet; import org.telegram.ui.Adapters.DialogsAdapter; import org.telegram.ui.Adapters.DialogsSearchAdapter; -import org.telegram.messenger.AnimationCompat.ObjectAnimatorProxy; -import org.telegram.messenger.AnimationCompat.ViewProxy; import org.telegram.ui.Cells.HintDialogCell; import org.telegram.ui.Cells.ProfileSearchCell; import org.telegram.ui.Cells.UserCell; @@ -244,7 +243,7 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter. if (!onlySelect) { floatingButton.setVisibility(View.VISIBLE); floatingHidden = true; - ViewProxy.setTranslationY(floatingButton, AndroidUtilities.dp(100)); + floatingButton.setTranslationY(AndroidUtilities.dp(100)); hideFloatingButton(false); } if (listView.getAdapter() != dialogsAdapter) { @@ -323,6 +322,7 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter. listView.setItemAnimator(null); listView.setInstantClick(true); listView.setLayoutAnimation(null); + listView.setTag(4); layoutManager = new LinearLayoutManager(context) { @Override public boolean supportsPredictiveItemAnimations() { @@ -331,9 +331,7 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter. }; layoutManager.setOrientation(LinearLayoutManager.VERTICAL); listView.setLayoutManager(layoutManager); - if (Build.VERSION.SDK_INT >= 11) { - listView.setVerticalScrollbarPosition(LocaleController.isRTL ? ListView.SCROLLBAR_POSITION_LEFT : ListView.SCROLLBAR_POSITION_RIGHT); - } + listView.setVerticalScrollbarPosition(LocaleController.isRTL ? ListView.SCROLLBAR_POSITION_LEFT : ListView.SCROLLBAR_POSITION_RIGHT); frameLayout.addView(listView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); listView.setOnItemClickListener(new RecyclerListView.OnItemClickListener() { @Override @@ -345,7 +343,7 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter. int message_id = 0; RecyclerView.Adapter adapter = listView.getAdapter(); if (adapter == dialogsAdapter) { - TLRPC.Dialog dialog = dialogsAdapter.getItem(position); + TLRPC.TL_dialog dialog = dialogsAdapter.getItem(position); if (dialog == null) { return; } @@ -482,8 +480,8 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter. } return false; } - TLRPC.Dialog dialog; - ArrayList dialogs = getDialogsArray(); + TLRPC.TL_dialog dialog; + ArrayList dialogs = getDialogsArray(); if (position < 0 || position >= dialogs.size()) { return false; } @@ -494,7 +492,7 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter. int lower_id = (int) selectedDialog; int high_id = (int) (selectedDialog >> 32); - if (dialog instanceof TLRPC.TL_dialogChannel) { + if (DialogObject.isChannel(dialog)) { final TLRPC.Chat chat = MessagesController.getInstance().getChat(-lower_id); CharSequence items[]; if (chat != null && chat.megagroup) { @@ -898,7 +896,7 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter. floatingButton.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { - ViewProxy.setTranslationY(floatingButton, floatingHidden ? AndroidUtilities.dp(100) : 0); + floatingButton.setTranslationY(floatingHidden ? AndroidUtilities.dp(100) : 0); floatingButton.setClickable(!floatingHidden); if (floatingButton != null) { if (Build.VERSION.SDK_INT < 16) { @@ -1010,7 +1008,7 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter. } } - private ArrayList getDialogsArray() { + private ArrayList getDialogsArray() { if (dialogsType == 0) { return MessagesController.getInstance().dialogs; } else if (dialogsType == 1) { @@ -1042,7 +1040,7 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter. return; } floatingHidden = hide; - ObjectAnimatorProxy animator = ObjectAnimatorProxy.ofFloatProxy(floatingButton, "translationY", floatingHidden ? AndroidUtilities.dp(100) : 0).setDuration(300); + ObjectAnimator animator = ObjectAnimator.ofFloat(floatingButton, "translationY", floatingHidden ? AndroidUtilities.dp(100) : 0).setDuration(300); animator.setInterpolator(floatingInterpolator); floatingButton.setClickable(!hide); animator.start(); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/DocumentSelectActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/DocumentSelectActivity.java index 1e93dd0a3..7a36f9e21 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/DocumentSelectActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/DocumentSelectActivity.java @@ -8,6 +8,9 @@ package org.telegram.ui; +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; import android.annotation.SuppressLint; import android.app.AlertDialog; import android.content.BroadcastReceiver; @@ -15,7 +18,6 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.res.Configuration; -import android.os.Build; import android.os.Environment; import android.os.StatFs; import android.view.MotionEvent; @@ -37,8 +39,6 @@ import org.telegram.ui.ActionBar.Theme; import org.telegram.ui.Adapters.BaseFragmentAdapter; import org.telegram.ui.ActionBar.ActionBar; import org.telegram.ui.ActionBar.ActionBarMenu; -import org.telegram.messenger.AnimationCompat.AnimatorSetProxy; -import org.telegram.messenger.AnimationCompat.ObjectAnimatorProxy; import org.telegram.ui.ActionBar.BaseFragment; import org.telegram.ui.Cells.SharedDocumentCell; import org.telegram.ui.Components.LayoutHelper; @@ -238,18 +238,16 @@ public class DocumentSelectActivity extends BaseFragment { } selectedFiles.put(file.toString(), item); selectedMessagesCountTextView.setNumber(1, false); - if (Build.VERSION.SDK_INT >= 11) { - AnimatorSetProxy animatorSet = new AnimatorSetProxy(); - ArrayList animators = new ArrayList<>(); - for (int a = 0; a < actionModeViews.size(); a++) { - View view2 = actionModeViews.get(a); - AndroidUtilities.clearDrawableAnimation(view2); - animators.add(ObjectAnimatorProxy.ofFloat(view2, "scaleY", 0.1f, 1.0f)); - } - animatorSet.playTogether(animators); - animatorSet.setDuration(250); - animatorSet.start(); + AnimatorSet animatorSet = new AnimatorSet(); + ArrayList animators = new ArrayList<>(); + for (int a = 0; a < actionModeViews.size(); a++) { + View view2 = actionModeViews.get(a); + AndroidUtilities.clearDrawableAnimation(view2); + animators.add(ObjectAnimator.ofFloat(view2, "scaleY", 0.1f, 1.0f)); } + animatorSet.playTogether(animators); + animatorSet.setDuration(250); + animatorSet.start(); scrolling = false; if (view instanceof SharedDocumentCell) { ((SharedDocumentCell) view).setChecked(true, true); @@ -512,7 +510,7 @@ public class DocumentSelectActivity extends BaseFragment { HashSet paths = new HashSet<>(); String defaultPath = Environment.getExternalStorageDirectory().getPath(); - boolean isDefaultPathRemovable = Build.VERSION.SDK_INT >= 9 && Environment.isExternalStorageRemovable(); + boolean isDefaultPathRemovable = Environment.isExternalStorageRemovable(); String defaultPathState = Environment.getExternalStorageState(); if (defaultPathState.equals(Environment.MEDIA_MOUNTED) || defaultPathState.equals(Environment.MEDIA_MOUNTED_READ_ONLY)) { ListItem ext = new ListItem(); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/GroupCreateActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/GroupCreateActivity.java index 469f469ff..5d0506f43 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/GroupCreateActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/GroupCreateActivity.java @@ -14,7 +14,6 @@ import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.drawable.BitmapDrawable; -import android.os.Build; import android.os.Bundle; import android.text.Editable; import android.text.InputType; @@ -216,9 +215,7 @@ public class GroupCreateActivity extends BaseFragment implements NotificationCen } else { userSelectEditText.setHint(LocaleController.getString("SendMessageTo", R.string.SendMessageTo)); } - if (Build.VERSION.SDK_INT >= 11) { - userSelectEditText.setTextIsSelectable(false); - } + userSelectEditText.setTextIsSelectable(false); userSelectEditText.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) { @@ -275,9 +272,7 @@ public class GroupCreateActivity extends BaseFragment implements NotificationCen if (listView != null) { listView.setAdapter(searchListViewAdapter); searchListViewAdapter.notifyDataSetChanged(); - if (android.os.Build.VERSION.SDK_INT >= 11) { - listView.setFastScrollAlwaysVisible(false); - } + listView.setFastScrollAlwaysVisible(false); listView.setFastScrollEnabled(false); listView.setVerticalScrollBarEnabled(true); } @@ -291,9 +286,7 @@ public class GroupCreateActivity extends BaseFragment implements NotificationCen searchWas = false; listView.setAdapter(listViewAdapter); listViewAdapter.notifyDataSetChanged(); - if (android.os.Build.VERSION.SDK_INT >= 11) { - listView.setFastScrollAlwaysVisible(true); - } + listView.setFastScrollAlwaysVisible(true); listView.setFastScrollEnabled(true); listView.setVerticalScrollBarEnabled(false); emptyTextView.setText(LocaleController.getString("NoContacts", R.string.NoContacts)); @@ -332,10 +325,8 @@ public class GroupCreateActivity extends BaseFragment implements NotificationCen listView.setFastScrollEnabled(true); listView.setScrollBarStyle(View.SCROLLBARS_OUTSIDE_OVERLAY); listView.setAdapter(listViewAdapter); - if (Build.VERSION.SDK_INT >= 11) { - listView.setFastScrollAlwaysVisible(true); - listView.setVerticalScrollbarPosition(LocaleController.isRTL ? ListView.SCROLLBAR_POSITION_LEFT : ListView.SCROLLBAR_POSITION_RIGHT); - } + listView.setFastScrollAlwaysVisible(true); + listView.setVerticalScrollbarPosition(LocaleController.isRTL ? ListView.SCROLLBAR_POSITION_LEFT : ListView.SCROLLBAR_POSITION_RIGHT); linearLayout.addView(listView, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override @@ -407,9 +398,7 @@ public class GroupCreateActivity extends BaseFragment implements NotificationCen searchWas = false; listView.setAdapter(listViewAdapter); listViewAdapter.notifyDataSetChanged(); - if (android.os.Build.VERSION.SDK_INT >= 11) { - listView.setFastScrollAlwaysVisible(true); - } + listView.setFastScrollAlwaysVisible(true); listView.setFastScrollEnabled(true); listView.setVerticalScrollBarEnabled(false); emptyTextView.setText(LocaleController.getString("NoContacts", R.string.NoContacts)); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/GroupCreateFinalActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/GroupCreateFinalActivity.java index 4cde07fd7..3c3cef490 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/GroupCreateFinalActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/GroupCreateFinalActivity.java @@ -47,7 +47,6 @@ import org.telegram.ui.Components.AvatarDrawable; import org.telegram.ui.Components.AvatarUpdater; import org.telegram.ui.Components.BackupImageView; import org.telegram.ui.ActionBar.BaseFragment; -import org.telegram.ui.Components.FrameLayoutFixed; import org.telegram.ui.Components.LayoutHelper; import java.util.ArrayList; @@ -202,7 +201,7 @@ public class GroupCreateFinalActivity extends BaseFragment implements Notificati LinearLayout linearLayout = (LinearLayout) fragmentView; linearLayout.setOrientation(LinearLayout.VERTICAL); - FrameLayout frameLayout = new FrameLayoutFixed(context); + FrameLayout frameLayout = new FrameLayout(context); linearLayout.addView(frameLayout); LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) frameLayout.getLayoutParams(); layoutParams.width = LayoutHelper.MATCH_PARENT; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/GroupInviteActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/GroupInviteActivity.java index 09fea806e..a3ec2c9ad 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/GroupInviteActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/GroupInviteActivity.java @@ -12,7 +12,6 @@ import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; -import android.os.Build; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; @@ -132,14 +131,9 @@ public class GroupInviteActivity extends BaseFragment implements NotificationCen return; } try { - if (Build.VERSION.SDK_INT < 11) { - android.text.ClipboardManager clipboard = (android.text.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); - clipboard.setText(invite.link); - } else { - android.content.ClipboardManager clipboard = (android.content.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); - android.content.ClipData clip = android.content.ClipData.newPlainText("label", invite.link); - clipboard.setPrimaryClip(clip); - } + android.content.ClipboardManager clipboard = (android.content.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); + android.content.ClipData clip = android.content.ClipData.newPlainText("label", invite.link); + clipboard.setPrimaryClip(clip); Toast.makeText(getParentActivity(), LocaleController.getString("LinkCopied", R.string.LinkCopied), Toast.LENGTH_SHORT).show(); } catch (Exception e) { FileLog.e("tmessages", e); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/IntroActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/IntroActivity.java index 05755ec14..06c8d4f69 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/IntroActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/IntroActivity.java @@ -167,7 +167,6 @@ public class IntroActivity extends Activity { fadeinImage.clearAnimation(); fadeoutImage.clearAnimation(); - Animation outAnimation = AnimationUtils.loadAnimation(IntroActivity.this, R.anim.icon_anim_fade_out); outAnimation.setAnimationListener(new Animation.AnimationListener() { @Override diff --git a/TMessagesProj/src/main/java/org/telegram/ui/LaunchActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/LaunchActivity.java index 021b157ec..3a8a413dc 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/LaunchActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/LaunchActivity.java @@ -26,6 +26,7 @@ import android.os.Bundle; import android.os.Parcelable; import android.provider.ContactsContract; import android.support.annotation.NonNull; +import android.text.TextUtils; import android.view.ActionMode; import android.view.KeyEvent; import android.view.MotionEvent; @@ -60,6 +61,7 @@ import org.telegram.messenger.LocaleController; import org.telegram.messenger.NotificationCenter; import org.telegram.messenger.R; import org.telegram.messenger.browser.Browser; +import org.telegram.messenger.query.DraftQuery; import org.telegram.tgnet.ConnectionsManager; import org.telegram.tgnet.RequestDelegate; import org.telegram.tgnet.TLObject; @@ -282,7 +284,7 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa drawerLayoutContainer.setDrawerLayout(listView); FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) listView.getLayoutParams(); Point screenSize = AndroidUtilities.getRealScreenSize(); - layoutParams.width = AndroidUtilities.isTablet() ? AndroidUtilities.dp(320) : Math.min(screenSize.x, screenSize.y) - AndroidUtilities.dp(56); + layoutParams.width = AndroidUtilities.isTablet() ? AndroidUtilities.dp(320) : Math.min(AndroidUtilities.dp(320), Math.min(screenSize.x, screenSize.y) - AndroidUtilities.dp(56)); layoutParams.height = LayoutHelper.MATCH_PARENT; listView.setLayoutParams(layoutParams); @@ -363,9 +365,6 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa NotificationCenter.getInstance().addObserver(this, NotificationCenter.didUpdatedConnectionState); NotificationCenter.getInstance().addObserver(this, NotificationCenter.needShowAlert); NotificationCenter.getInstance().addObserver(this, NotificationCenter.wasUnableToFindCurrentLocation); - if (Build.VERSION.SDK_INT < 14) { - NotificationCenter.getInstance().addObserver(this, NotificationCenter.screenStateChanged); - } if (actionBarLayout.fragmentsStack.isEmpty()) { if (!UserConfig.isClientActivated()) { @@ -499,6 +498,11 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa }); } + private class VcardData { + String name; + ArrayList phones = new ArrayList<>(); + } + private boolean handleIntent(Intent intent, boolean isNew, boolean restore, boolean fromPassword) { int flags = intent.getFlags(); if (!fromPassword && (AndroidUtilities.needShowPasscode(true) || UserConfig.isWaitingForPasscodeEnter)) { @@ -538,19 +542,28 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa if (uri != null) { ContentResolver cr = getContentResolver(); InputStream stream = cr.openInputStream(uri); + ArrayList vcardDatas = new ArrayList<>(); + VcardData currentData = null; - String name = null; - String nameEncoding = null; - String nameCharset = null; - ArrayList phones = new ArrayList<>(); BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(stream, "UTF-8")); String line; while ((line = bufferedReader.readLine()) != null) { + FileLog.e("tmessages", line); String[] args = line.split(":"); if (args.length != 2) { continue; } - if (args[0].startsWith("FN")) { + if (args[0].equals("BEGIN") && args[1].equals("VCARD")) { + vcardDatas.add(currentData = new VcardData()); + } else if (args[0].equals("END") && args[1].equals("VCARD")) { + currentData = null; + } + if (currentData == null) { + continue; + } + if (args[0].startsWith("FN") || args[0].startsWith("ORG") && TextUtils.isEmpty(currentData.name)) { + String nameEncoding = null; + String nameCharset = null; String[] params = args[0].split(";"); for (String param : params) { String[] args2 = param.split("="); @@ -563,28 +576,28 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa nameEncoding = args2[1]; } } - name = args[1]; + currentData.name = args[1]; if (nameEncoding != null && nameEncoding.equalsIgnoreCase("QUOTED-PRINTABLE")) { - while (name.endsWith("=") && nameEncoding != null) { - name = name.substring(0, name.length() - 1); + while (currentData.name.endsWith("=") && nameEncoding != null) { + currentData.name = currentData.name.substring(0, currentData.name.length() - 1); line = bufferedReader.readLine(); if (line == null) { break; } - name += line; + currentData.name += line; } - byte[] bytes = AndroidUtilities.decodeQuotedPrintable(name.getBytes()); + byte[] bytes = AndroidUtilities.decodeQuotedPrintable(currentData.name.getBytes()); if (bytes != null && bytes.length != 0) { String decodedName = new String(bytes, nameCharset); if (decodedName != null) { - name = decodedName; + currentData.name = decodedName; } } } } else if (args[0].startsWith("TEL")) { String phone = PhoneFormat.stripExceptNumbers(args[1], true); if (phone.length() > 0) { - phones.add(phone); + currentData.phones.add(phone); } } } @@ -594,15 +607,22 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa } catch (Exception e) { FileLog.e("tmessages", e); } - if (name != null && !phones.isEmpty()) { - contactsToSend = new ArrayList<>(); - for (String phone : phones) { - TLRPC.User user = new TLRPC.TL_userContact_old2(); - user.phone = phone; - user.first_name = name; - user.last_name = ""; - user.id = 0; - contactsToSend.add(user); + for (int a = 0; a < vcardDatas.size(); a++) { + VcardData vcardData = vcardDatas.get(a); + if (vcardData.name != null && !vcardData.phones.isEmpty()) { + if (contactsToSend == null) { + contactsToSend = new ArrayList<>(); + } + + for (int b = 0; b < vcardData.phones.size(); b++) { + String phone = vcardData.phones.get(b); + TLRPC.User user = new TLRPC.TL_userContact_old2(); + user.phone = phone; + user.first_name = vcardData.name; + user.last_name = ""; + user.id = 0; + contactsToSend.add(user); + } } } } else { @@ -810,6 +830,10 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa username = data.getQueryParameter("domain"); botUser = data.getQueryParameter("start"); botChat = data.getQueryParameter("startgroup"); + messageId = Utilities.parseInt(data.getQueryParameter("post")); + if (messageId == 0) { + messageId = null; + } } else if (url.startsWith("tg:join") || url.startsWith("tg://join")) { url = url.replace("tg:join", "tg://telegram.org").replace("tg://join", "tg://telegram.org"); data = Uri.parse(url); @@ -1278,10 +1302,7 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa } if (MessagesController.checkCanOpenChat(args, fragment)) { NotificationCenter.getInstance().postNotificationName(NotificationCenter.closeChats); - SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE); - SharedPreferences.Editor editor = preferences.edit(); - editor.putString("dialog_" + did, message); - editor.commit(); + DraftQuery.saveDraft(did, message, null, null, true); actionBarLayout.presentFragment(new ChatActivity(args), true, false, true); } } @@ -1381,7 +1402,7 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa } } else { actionBarLayout.presentFragment(fragment, dialogsFragment != null, dialogsFragment == null, true); - SendMessagesHelper.prepareSendingVideo(videoPath, 0, 0, 0, 0, null, dialog_id, null, true); + SendMessagesHelper.prepareSendingVideo(videoPath, 0, 0, 0, 0, null, dialog_id, null); } } else { actionBarLayout.presentFragment(fragment, dialogsFragment != null, dialogsFragment == null, true); @@ -1393,19 +1414,19 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa captions.add(sendingText); sendingText = null; } - SendMessagesHelper.prepareSendingPhotos(null, photoPathsArray, dialog_id, null, captions, true); + SendMessagesHelper.prepareSendingPhotos(null, photoPathsArray, dialog_id, null, captions); } if (sendingText != null) { - SendMessagesHelper.prepareSendingText(sendingText, dialog_id, true); + SendMessagesHelper.prepareSendingText(sendingText, dialog_id); } if (documentsPathsArray != null || documentsUrisArray != null) { - SendMessagesHelper.prepareSendingDocuments(documentsPathsArray, documentsOriginalPathsArray, documentsUrisArray, documentsMimeType, dialog_id, null, true); + SendMessagesHelper.prepareSendingDocuments(documentsPathsArray, documentsOriginalPathsArray, documentsUrisArray, documentsMimeType, dialog_id, null); } if (contactsToSend != null && !contactsToSend.isEmpty()) { for (TLRPC.User user : contactsToSend) { - SendMessagesHelper.getInstance().sendMessage(user, dialog_id, null, true, null, null); + SendMessagesHelper.getInstance().sendMessage(user, dialog_id, null, null, null); } } } @@ -1434,9 +1455,6 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa NotificationCenter.getInstance().removeObserver(this, NotificationCenter.didUpdatedConnectionState); NotificationCenter.getInstance().removeObserver(this, NotificationCenter.needShowAlert); NotificationCenter.getInstance().removeObserver(this, NotificationCenter.wasUnableToFindCurrentLocation); - if (Build.VERSION.SDK_INT < 14) { - NotificationCenter.getInstance().removeObserver(this, NotificationCenter.screenStateChanged); - } } public void presentFragment(BaseFragment fragment) { @@ -1761,14 +1779,6 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa } } else if (id == NotificationCenter.mainUserInfoChanged) { drawerLayoutAdapter.notifyDataSetChanged(); - } else if (id == NotificationCenter.screenStateChanged) { - if (!ApplicationLoader.mainInterfacePaused) { - if (!ApplicationLoader.isScreenOn) { - onPasscodePause(); - } else { - onPasscodeResume(); - } - } } else if (id == NotificationCenter.needShowAlert) { final Integer reason = (Integer) args[0]; AlertDialog.Builder builder = new AlertDialog.Builder(this); @@ -1815,7 +1825,7 @@ public class LaunchActivity extends Activity implements ActionBarLayout.ActionBa public void didSelectLocation(TLRPC.MessageMedia location) { for (HashMap.Entry entry : waitingForLocation.entrySet()) { MessageObject messageObject = entry.getValue(); - SendMessagesHelper.getInstance().sendMessage(location, messageObject.getDialogId(), messageObject, false, null, null); + SendMessagesHelper.getInstance().sendMessage(location, messageObject.getDialogId(), messageObject, null, null); } } }); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/LocationActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/LocationActivity.java index 6150ba85d..9e03f4ff7 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/LocationActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/LocationActivity.java @@ -12,6 +12,7 @@ import android.Manifest; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.StateListAnimator; +import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.app.Activity; import android.app.AlertDialog; @@ -114,7 +115,6 @@ public class LocationActivity extends BaseFragment implements NotificationCenter private LocationActivityDelegate delegate; private int overScrollHeight = AndroidUtilities.displaySize.x - ActionBar.getCurrentActionBarHeight() - AndroidUtilities.dp(66); - private int halfHeight; private final static int share = 1; private final static int map_list_menu_map = 2; @@ -276,6 +276,7 @@ public class LocationActivity extends BaseFragment implements NotificationCenter animator.addState(new int[]{}, ObjectAnimator.ofFloat(locationButton, "translationZ", AndroidUtilities.dp(4), AndroidUtilities.dp(2)).setDuration(200)); locationButton.setStateListAnimator(animator); locationButton.setOutlineProvider(new ViewOutlineProvider() { + @SuppressLint("NewApi") @Override public void getOutline(View view, Outline outline) { outline.setOval(0, 0, AndroidUtilities.dp(56), AndroidUtilities.dp(56)); @@ -357,6 +358,7 @@ public class LocationActivity extends BaseFragment implements NotificationCenter animator.addState(new int[]{}, ObjectAnimator.ofFloat(routeButton, "translationZ", AndroidUtilities.dp(4), AndroidUtilities.dp(2)).setDuration(200)); routeButton.setStateListAnimator(animator); routeButton.setOutlineProvider(new ViewOutlineProvider() { + @SuppressLint("NewApi") @Override public void getOutline(View view, Outline outline) { outline.setOval(0, 0, AndroidUtilities.dp(56), AndroidUtilities.dp(56)); @@ -473,39 +475,33 @@ public class LocationActivity extends BaseFragment implements NotificationCenter mapView = new MapView(context) { @Override public boolean onInterceptTouchEvent(MotionEvent ev) { - if (Build.VERSION.SDK_INT >= 11) { - if (ev.getAction() == MotionEvent.ACTION_DOWN) { - if (animatorSet != null) { - animatorSet.cancel(); - } - animatorSet = new AnimatorSet(); - animatorSet.setDuration(200); - animatorSet.playTogether( - ObjectAnimator.ofFloat(markerImageView, "translationY", markerTop + -AndroidUtilities.dp(10)), - ObjectAnimator.ofFloat(markerXImageView, "alpha", 1.0f)); - animatorSet.start(); - } else if (ev.getAction() == MotionEvent.ACTION_UP) { - if (animatorSet != null) { - animatorSet.cancel(); - } - animatorSet = new AnimatorSet(); - animatorSet.setDuration(200); - animatorSet.playTogether( - ObjectAnimator.ofFloat(markerImageView, "translationY", markerTop), - ObjectAnimator.ofFloat(markerXImageView, "alpha", 0.0f)); - animatorSet.start(); + if (ev.getAction() == MotionEvent.ACTION_DOWN) { + if (animatorSet != null) { + animatorSet.cancel(); } + animatorSet = new AnimatorSet(); + animatorSet.setDuration(200); + animatorSet.playTogether( + ObjectAnimator.ofFloat(markerImageView, "translationY", markerTop + -AndroidUtilities.dp(10)), + ObjectAnimator.ofFloat(markerXImageView, "alpha", 1.0f)); + animatorSet.start(); + } else if (ev.getAction() == MotionEvent.ACTION_UP) { + if (animatorSet != null) { + animatorSet.cancel(); + } + animatorSet = new AnimatorSet(); + animatorSet.setDuration(200); + animatorSet.playTogether( + ObjectAnimator.ofFloat(markerImageView, "translationY", markerTop), + ObjectAnimator.ofFloat(markerXImageView, "alpha", 0.0f)); + animatorSet.start(); } if (ev.getAction() == MotionEvent.ACTION_MOVE) { if (!userLocationMoved) { - if (Build.VERSION.SDK_INT >= 11) { - AnimatorSet animatorSet = new AnimatorSet(); - animatorSet.setDuration(200); - animatorSet.play(ObjectAnimator.ofFloat(locationButton, "alpha", 1.0f)); - animatorSet.start(); - } else { - locationButton.setVisibility(VISIBLE); - } + AnimatorSet animatorSet = new AnimatorSet(); + animatorSet.setDuration(200); + animatorSet.play(ObjectAnimator.ofFloat(locationButton, "alpha", 1.0f)); + animatorSet.start(); userLocationMoved = true; } if (googleMap != null && userLocation != null) { @@ -537,12 +533,10 @@ public class LocationActivity extends BaseFragment implements NotificationCenter markerImageView.setImageResource(R.drawable.map_pin); mapViewClip.addView(markerImageView, LayoutHelper.createFrame(24, 42, Gravity.TOP | Gravity.CENTER_HORIZONTAL)); - if (Build.VERSION.SDK_INT >= 11) { - markerXImageView = new ImageView(context); - markerXImageView.setAlpha(0.0f); - markerXImageView.setImageResource(R.drawable.place_x); - mapViewClip.addView(markerXImageView, LayoutHelper.createFrame(14, 14, Gravity.TOP | Gravity.CENTER_HORIZONTAL)); - } + markerXImageView = new ImageView(context); + markerXImageView.setAlpha(0.0f); + markerXImageView.setImageResource(R.drawable.place_x); + mapViewClip.addView(markerXImageView, LayoutHelper.createFrame(14, 14, Gravity.TOP | Gravity.CENTER_HORIZONTAL)); mapViewClip.addView(locationButton, LayoutHelper.createFrame(Build.VERSION.SDK_INT >= 21 ? 56 : 60, Build.VERSION.SDK_INT >= 21 ? 56 : 60, (LocaleController.isRTL ? Gravity.LEFT : Gravity.RIGHT) | Gravity.BOTTOM, LocaleController.isRTL ? 14 : 0, 0, LocaleController.isRTL ? 0 : 14, 14)); locationButton.setOnClickListener(new View.OnClickListener() { @@ -558,25 +552,17 @@ public class LocationActivity extends BaseFragment implements NotificationCenter } } if (myLocation != null && googleMap != null) { - if (Build.VERSION.SDK_INT >= 11) { - AnimatorSet animatorSet = new AnimatorSet(); - animatorSet.setDuration(200); - animatorSet.play(ObjectAnimator.ofFloat(locationButton, "alpha", 0.0f)); - animatorSet.start(); - } else { - locationButton.setVisibility(View.INVISIBLE); - } + AnimatorSet animatorSet = new AnimatorSet(); + animatorSet.setDuration(200); + animatorSet.play(ObjectAnimator.ofFloat(locationButton, "alpha", 0.0f)); + animatorSet.start(); adapter.setCustomLocation(null); userLocationMoved = false; googleMap.animateCamera(CameraUpdateFactory.newLatLng(new LatLng(myLocation.getLatitude(), myLocation.getLongitude()))); } } }); - if (Build.VERSION.SDK_INT >= 11) { - locationButton.setAlpha(0.0f); - } else { - locationButton.setVisibility(View.INVISIBLE); - } + locationButton.setAlpha(0.0f); emptyTextLayout = new LinearLayout(context); emptyTextLayout.setVisibility(View.GONE); @@ -718,7 +704,6 @@ public class LocationActivity extends BaseFragment implements NotificationCenter if (firstVisibleItem == 0) { top = child.getTop(); height = overScrollHeight + (top < 0 ? top : 0); - halfHeight = (top < 0 ? top : 0) / 2; } FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) mapViewClip.getLayoutParams(); if (layoutParams != null) { @@ -733,37 +718,18 @@ public class LocationActivity extends BaseFragment implements NotificationCenter mapViewClip.setVisibility(View.VISIBLE); } } - if (Build.VERSION.SDK_INT >= 11) { - mapViewClip.setTranslationY(Math.min(0, top)); - mapView.setTranslationY(Math.max(0, -top / 2)); - markerImageView.setTranslationY(markerTop = -top - AndroidUtilities.dp(42) + height / 2); - markerXImageView.setTranslationY(-top - AndroidUtilities.dp(7) + height / 2); - if (googleMap != null) { - layoutParams = (FrameLayout.LayoutParams) mapView.getLayoutParams(); - if (layoutParams != null && layoutParams.height != overScrollHeight + AndroidUtilities.dp(10)) { - layoutParams.height = overScrollHeight + AndroidUtilities.dp(10); - googleMap.setPadding(0, 0, 0, AndroidUtilities.dp(10)); - mapView.setLayoutParams(layoutParams); - } - } - } else { - markerTop = 0; - layoutParams.height = height; - mapViewClip.setLayoutParams(layoutParams); + mapViewClip.setTranslationY(Math.min(0, top)); + mapView.setTranslationY(Math.max(0, -top / 2)); + markerImageView.setTranslationY(markerTop = -top - AndroidUtilities.dp(42) + height / 2); + markerXImageView.setTranslationY(-top - AndroidUtilities.dp(7) + height / 2); - layoutParams = (FrameLayout.LayoutParams) markerImageView.getLayoutParams(); - layoutParams.topMargin = height / 2 - AndroidUtilities.dp(42); - markerImageView.setLayoutParams(layoutParams); - - if (googleMap != null) { - layoutParams = (FrameLayout.LayoutParams) mapView.getLayoutParams(); - if (layoutParams != null) { - layoutParams.topMargin = halfHeight; - layoutParams.height = overScrollHeight + AndroidUtilities.dp(10); - googleMap.setPadding(0, 0, 0, AndroidUtilities.dp(10)); - mapView.setLayoutParams(layoutParams); - } + if (googleMap != null) { + layoutParams = (FrameLayout.LayoutParams) mapView.getLayoutParams(); + if (layoutParams != null && layoutParams.height != overScrollHeight + AndroidUtilities.dp(10)) { + layoutParams.height = overScrollHeight + AndroidUtilities.dp(10); + googleMap.setPadding(0, 0, 0, AndroidUtilities.dp(10)); + mapView.setLayoutParams(layoutParams); } } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/LoginActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/LoginActivity.java index 0ddae7e2f..dce7b5f89 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/LoginActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/LoginActivity.java @@ -315,7 +315,7 @@ public class LoginActivity extends BaseFragment { return false; } - public void needShowAlert(String title, String text) { + private void needShowAlert(String title, String text) { if (text == null || getParentActivity() == null) { return; } @@ -326,7 +326,36 @@ public class LoginActivity extends BaseFragment { showDialog(builder.create()); } - public void needShowProgress() { + private void needShowInvalidAlert(final String phoneNumber) { + if (getParentActivity() == null) { + return; + } + AlertDialog.Builder builder = new AlertDialog.Builder(getParentActivity()); + builder.setTitle(LocaleController.getString("AppName", R.string.AppName)); + builder.setMessage(LocaleController.getString("InvalidPhoneNumber", R.string.InvalidPhoneNumber)); + builder.setNeutralButton(LocaleController.getString("BotHelp", R.string.BotHelp), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + try { + PackageInfo pInfo = ApplicationLoader.applicationContext.getPackageManager().getPackageInfo(ApplicationLoader.applicationContext.getPackageName(), 0); + String version = String.format(Locale.US, "%s (%d)", pInfo.versionName, pInfo.versionCode); + + Intent mailer = new Intent(Intent.ACTION_SEND); + mailer.setType("message/rfc822"); + mailer.putExtra(Intent.EXTRA_EMAIL, new String[]{"login@stel.com"}); + mailer.putExtra(Intent.EXTRA_SUBJECT, "Invalid phone number: " + phoneNumber); + mailer.putExtra(Intent.EXTRA_TEXT, "I'm trying to use my mobile phone number: " + phoneNumber + "\nBut Telegram says it's invalid. Please help.\n\nApp version: " + version + "\nOS version: SDK " + Build.VERSION.SDK_INT + "\nDevice Name: " + Build.MANUFACTURER + Build.MODEL + "\nLocale: " + Locale.getDefault()); + getParentActivity().startActivity(Intent.createChooser(mailer, "Send email...")); + } catch (Exception e) { + needShowAlert(LocaleController.getString("AppName", R.string.AppName), LocaleController.getString("NoMailInstalled", R.string.NoMailInstalled)); + } + } + }); + builder.setPositiveButton(LocaleController.getString("OK", R.string.OK), null); + showDialog(builder.create()); + } + + private void needShowProgress() { if (getParentActivity() == null || getParentActivity().isFinishing() || progressDialog != null) { return; } @@ -358,7 +387,7 @@ public class LoginActivity extends BaseFragment { } doneButton.setVisibility(View.VISIBLE); } - if (android.os.Build.VERSION.SDK_INT > 13 && animated) { + if (animated) { final SlideView outView = views[currentViewNum]; final SlideView newView = views[page]; currentViewNum = page; @@ -899,17 +928,13 @@ public class LoginActivity extends BaseFragment { return; } - ConnectionsManager.getInstance().cleanUp(); - TLRPC.TL_auth_sendCode req = new TLRPC.TL_auth_sendCode(); + ConnectionsManager.getInstance().cleanup(); + final TLRPC.TL_auth_sendCode req = new TLRPC.TL_auth_sendCode(); String phone = PhoneFormat.stripExceptNumbers("" + codeField.getText() + phoneField.getText()); ConnectionsManager.getInstance().applyCountryPortNumber(phone); req.api_hash = BuildVars.APP_HASH; req.api_id = BuildVars.APP_ID; req.phone_number = phone; - req.lang_code = LocaleController.getLocaleStringIso639(); - if (req.lang_code.length() == 0) { - req.lang_code = "en"; - } req.allow_flashcall = simcardAvailable && allowCall; if (req.allow_flashcall) { String number = tm.getLine1Number(); @@ -939,7 +964,7 @@ public class LoginActivity extends BaseFragment { } else { if (error.text != null) { if (error.text.contains("PHONE_NUMBER_INVALID")) { - needShowAlert(LocaleController.getString("AppName", R.string.AppName), LocaleController.getString("InvalidPhoneNumber", R.string.InvalidPhoneNumber)); + needShowInvalidAlert(req.phone_number); } else if (error.text.contains("PHONE_CODE_EMPTY") || error.text.contains("PHONE_CODE_INVALID")) { needShowAlert(LocaleController.getString("AppName", R.string.AppName), LocaleController.getString("InvalidCode", R.string.InvalidCode)); } else if (error.text.contains("PHONE_CODE_EXPIRED")) { @@ -1513,10 +1538,10 @@ public class LoginActivity extends BaseFragment { destroyTimer(); destroyCodeTimer(); UserConfig.clearConfig(); - MessagesController.getInstance().cleanUp(); + MessagesController.getInstance().cleanup(); UserConfig.setCurrentUser(res.user); UserConfig.saveConfig(true); - MessagesStorage.getInstance().cleanUp(true); + MessagesStorage.getInstance().cleanup(true); ArrayList users = new ArrayList<>(); users.add(res.user); MessagesStorage.getInstance().putUsersAndChats(users, null, true, true); @@ -1975,10 +2000,10 @@ public class LoginActivity extends BaseFragment { TLRPC.TL_auth_authorization res = (TLRPC.TL_auth_authorization) response; ConnectionsManager.getInstance().setUserId(res.user.id); UserConfig.clearConfig(); - MessagesController.getInstance().cleanUp(); + MessagesController.getInstance().cleanup(); UserConfig.setCurrentUser(res.user); UserConfig.saveConfig(true); - MessagesStorage.getInstance().cleanUp(true); + MessagesStorage.getInstance().cleanup(true); ArrayList users = new ArrayList<>(); users.add(res.user); MessagesStorage.getInstance().putUsersAndChats(users, null, true, true); @@ -2198,10 +2223,10 @@ public class LoginActivity extends BaseFragment { TLRPC.TL_auth_authorization res = (TLRPC.TL_auth_authorization) response; ConnectionsManager.getInstance().setUserId(res.user.id); UserConfig.clearConfig(); - MessagesController.getInstance().cleanUp(); + MessagesController.getInstance().cleanup(); UserConfig.setCurrentUser(res.user); UserConfig.saveConfig(true); - MessagesStorage.getInstance().cleanUp(true); + MessagesStorage.getInstance().cleanup(true); ArrayList users = new ArrayList<>(); users.add(res.user); MessagesStorage.getInstance().putUsersAndChats(users, null, true, true); @@ -2411,10 +2436,10 @@ public class LoginActivity extends BaseFragment { final TLRPC.TL_auth_authorization res = (TLRPC.TL_auth_authorization) response; ConnectionsManager.getInstance().setUserId(res.user.id); UserConfig.clearConfig(); - MessagesController.getInstance().cleanUp(); + MessagesController.getInstance().cleanup(); UserConfig.setCurrentUser(res.user); UserConfig.saveConfig(true); - MessagesStorage.getInstance().cleanUp(true); + MessagesStorage.getInstance().cleanup(true); ArrayList users = new ArrayList<>(); users.add(res.user); MessagesStorage.getInstance().putUsersAndChats(users, null, true, true); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/MediaActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/MediaActivity.java index 716dea31e..06e75bc50 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/MediaActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/MediaActivity.java @@ -8,6 +8,9 @@ package org.telegram.ui; +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; import android.app.Activity; import android.app.AlertDialog; import android.content.Context; @@ -64,8 +67,6 @@ import org.telegram.ui.ActionBar.BottomSheet; import org.telegram.ui.ActionBar.Theme; import org.telegram.ui.Adapters.BaseFragmentAdapter; import org.telegram.ui.Adapters.BaseSectionsAdapter; -import org.telegram.messenger.AnimationCompat.AnimatorSetProxy; -import org.telegram.messenger.AnimationCompat.ObjectAnimatorProxy; import org.telegram.ui.Cells.GreySectionCell; import org.telegram.ui.Cells.LoadingCell; import org.telegram.ui.Cells.SharedDocumentCell; @@ -256,14 +257,6 @@ public class MediaActivity extends BaseFragment implements NotificationCenter.No actionBar.hideActionMode(); listView.invalidateViews(); } else { - if (Build.VERSION.SDK_INT < 11 && listView != null) { - listView.setAdapter(null); - listView = null; - photoVideoAdapter = null; - documentsAdapter = null; - audioAdapter = null; - linksAdapter = null; - } finishFragment(); } } else if (id == shared_media_item) { @@ -997,18 +990,16 @@ public class MediaActivity extends BaseFragment implements NotificationCenter.No } actionBar.createActionMode().getItem(delete).setVisibility(cantDeleteMessagesCount == 0 ? View.VISIBLE : View.GONE); selectedMessagesCountTextView.setNumber(1, false); - if (Build.VERSION.SDK_INT >= 11) { - AnimatorSetProxy animatorSet = new AnimatorSetProxy(); - ArrayList animators = new ArrayList<>(); - for (int i = 0; i < actionModeViews.size(); i++) { - View view2 = actionModeViews.get(i); - AndroidUtilities.clearDrawableAnimation(view2); - animators.add(ObjectAnimatorProxy.ofFloat(view2, "scaleY", 0.1f, 1.0f)); - } - animatorSet.playTogether(animators); - animatorSet.setDuration(250); - animatorSet.start(); + AnimatorSet animatorSet = new AnimatorSet(); + ArrayList animators = new ArrayList<>(); + for (int i = 0; i < actionModeViews.size(); i++) { + View view2 = actionModeViews.get(i); + AndroidUtilities.clearDrawableAnimation(view2); + animators.add(ObjectAnimator.ofFloat(view2, "scaleY", 0.1f, 1.0f)); } + animatorSet.playTogether(animators); + animatorSet.setDuration(250); + animatorSet.start(); scrolling = false; if (view instanceof SharedDocumentCell) { ((SharedDocumentCell) view).setChecked(true, true); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/PasscodeActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/PasscodeActivity.java index 64f5deb6c..fa4904cf1 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/PasscodeActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/PasscodeActivity.java @@ -25,7 +25,6 @@ import android.text.method.DigitsKeyListener; import android.text.method.PasswordTransformationMethod; import android.util.TypedValue; import android.view.ActionMode; -import android.view.ContextMenu; import android.view.Gravity; import android.view.KeyEvent; import android.view.Menu; @@ -53,7 +52,6 @@ import org.telegram.ui.ActionBar.ActionBar; import org.telegram.ui.ActionBar.ActionBarMenu; import org.telegram.ui.ActionBar.ActionBarMenuItem; import org.telegram.ui.ActionBar.BaseFragment; -import org.telegram.ui.ActionBar.Theme; import org.telegram.ui.Adapters.BaseFragmentAdapter; import org.telegram.ui.Cells.TextCheckCell; import org.telegram.ui.Cells.TextInfoPrivacyCell; @@ -230,30 +228,23 @@ public class PasscodeActivity extends BaseFragment implements NotificationCenter } } }); - if (android.os.Build.VERSION.SDK_INT < 11) { - passwordEditText.setOnCreateContextMenuListener(new View.OnCreateContextMenuListener() { - public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { - menu.clear(); - } - }); - } else { - passwordEditText.setCustomSelectionActionModeCallback(new ActionMode.Callback() { - public boolean onPrepareActionMode(ActionMode mode, Menu menu) { - return false; - } - public void onDestroyActionMode(ActionMode mode) { - } + passwordEditText.setCustomSelectionActionModeCallback(new ActionMode.Callback() { + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + return false; + } - public boolean onCreateActionMode(ActionMode mode, Menu menu) { - return false; - } + public void onDestroyActionMode(ActionMode mode) { + } - public boolean onActionItemClicked(ActionMode mode, MenuItem item) { - return false; - } - }); - } + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + return false; + } + + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + return false; + } + }); if (type == 1) { dropDownContainer = new ActionBarMenuItem(context, menu, 0); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/PhotoAlbumPickerActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/PhotoAlbumPickerActivity.java index c4b50be27..1305c1caa 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/PhotoAlbumPickerActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/PhotoAlbumPickerActivity.java @@ -119,11 +119,6 @@ public class PhotoAlbumPickerActivity extends BaseFragment implements Notificati @Override public void onItemClick(int id) { if (id == -1) { - if (Build.VERSION.SDK_INT < 11) { - listView.setAdapter(null); - listView = null; - listAdapter = null; - } finishFragment(); } else if (id == 1) { if (delegate != null) { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/PhotoPickerActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/PhotoPickerActivity.java index c7d9915b8..95dde05b8 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/PhotoPickerActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/PhotoPickerActivity.java @@ -13,7 +13,6 @@ import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.graphics.Bitmap; -import android.os.Build; import android.util.Base64; import android.view.Gravity; import android.view.LayoutInflater; @@ -34,7 +33,6 @@ import android.widget.TextView; import org.json.JSONArray; import org.json.JSONObject; import org.telegram.messenger.AndroidUtilities; -import org.telegram.messenger.AnimationCompat.ViewProxy; import org.telegram.messenger.FileLoader; import org.telegram.messenger.LocaleController; import org.telegram.messenger.MediaController; @@ -176,11 +174,6 @@ public class PhotoPickerActivity extends BaseFragment implements NotificationCen @Override public void onItemClick(int id) { if (id == -1) { - if (Build.VERSION.SDK_INT < 11) { - listView.setAdapter(null); - listView = null; - listAdapter = null; - } finishFragment(); } } @@ -513,18 +506,10 @@ public class PhotoPickerActivity extends BaseFragment implements NotificationCen PhotoViewer.PlaceProviderObject object = new PhotoViewer.PlaceProviderObject(); object.viewX = coords[0]; object.viewY = coords[1] - AndroidUtilities.statusBarHeight; - if (Build.VERSION.SDK_INT < 11) { - float scale = ViewProxy.getScaleX(cell.photoImage); - if (scale != 1) { - int width = cell.photoImage.getMeasuredWidth(); - object.viewX += (width - width * scale) / 2; - object.viewY += (width - width * scale) / 2; - } - } object.parentView = listView; object.imageReceiver = cell.photoImage.getImageReceiver(); object.thumb = object.imageReceiver.getBitmap(); - object.scale = ViewProxy.getScaleX(cell.photoImage); + object.scale = cell.photoImage.getScaleX(); cell.checkBox.setVisibility(View.GONE); return object; } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/PhotoViewer.java b/TMessagesProj/src/main/java/org/telegram/ui/PhotoViewer.java index b4015385d..b4d701d92 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/PhotoViewer.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/PhotoViewer.java @@ -9,6 +9,9 @@ package org.telegram.ui; import android.Manifest; +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; import android.annotation.SuppressLint; import android.app.Activity; import android.app.AlertDialog; @@ -22,6 +25,7 @@ import android.graphics.Paint; import android.graphics.PixelFormat; import android.graphics.Rect; import android.graphics.RectF; +import android.graphics.SurfaceTexture; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; @@ -38,6 +42,7 @@ import android.view.Gravity; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.Surface; +import android.view.TextureView; import android.view.VelocityTracker; import android.view.View; import android.view.ViewGroup; @@ -54,6 +59,9 @@ import org.telegram.messenger.Emoji; import org.telegram.messenger.ImageLoader; import org.telegram.messenger.MessagesStorage; import org.telegram.messenger.UserObject; +import org.telegram.messenger.exoplayer.AspectRatioFrameLayout; +import org.telegram.messenger.exoplayer.ExoPlayer; +import org.telegram.messenger.exoplayer.util.PlayerControl; import org.telegram.messenger.query.SharedMediaQuery; import org.telegram.messenger.ApplicationLoader; import org.telegram.messenger.FileLoader; @@ -71,13 +79,11 @@ import org.telegram.messenger.MessageObject; import org.telegram.messenger.Utilities; import org.telegram.ui.ActionBar.Theme; import org.telegram.ui.Adapters.MentionsAdapter; -import org.telegram.messenger.AnimationCompat.AnimatorListenerAdapterProxy; -import org.telegram.messenger.AnimationCompat.AnimatorSetProxy; -import org.telegram.messenger.AnimationCompat.ObjectAnimatorProxy; -import org.telegram.messenger.AnimationCompat.ViewProxy; +import org.telegram.messenger.AnimatorListenerAdapterProxy; import org.telegram.ui.ActionBar.ActionBar; import org.telegram.ui.ActionBar.ActionBarMenu; import org.telegram.ui.ActionBar.ActionBarMenuItem; +import org.telegram.ui.Components.AnimatedFileDrawable; import org.telegram.ui.Components.CheckBox; import org.telegram.ui.Components.ClippingImageView; import org.telegram.messenger.ImageReceiver; @@ -87,7 +93,9 @@ import org.telegram.ui.Components.PhotoFilterView; import org.telegram.ui.Components.PickerBottomLayout; import org.telegram.ui.Components.PhotoViewerCaptionEnterView; import org.telegram.ui.Components.RecyclerListView; +import org.telegram.ui.Components.SeekBar; import org.telegram.ui.Components.SizeNotifierFrameLayoutPhoto; +import org.telegram.ui.Components.VideoPlayer; import java.io.File; import java.util.ArrayList; @@ -129,7 +137,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat private ActionBarMenuItem tuneItem; private ActionBarMenuItem captionItem; private ActionBarMenuItem captionDoneItem; - private AnimatorSetProxy currentActionBarAnimation; + private AnimatorSet currentActionBarAnimation; private PhotoCropView photoCropView; private PhotoFilterView photoFilterView; private AlertDialog visibleDialog; @@ -140,6 +148,38 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat private boolean canShowBottom = true; private int sendPhotoType = 0; private boolean needCaptionLayout; + private AnimatedFileDrawable currentAnimation; + + private AspectRatioFrameLayout aspectRatioFrameLayout; + private TextureView videoTextureView; + private VideoPlayer videoPlayer; + private FrameLayout videoPlayerControlFrameLayout; + private ImageView videoPlayButton; + private TextView videoPlayerTime; + private SeekBar videoPlayerSeekbar; + private boolean playerNeedsPrepare; + private boolean textureUploaded; + private boolean videoCrossfadeStarted; + private float videoCrossfadeAlpha; + private long videoCrossfadeAlphaLastTime; + private boolean isPlaying; + private Runnable updateProgressRunnable = new Runnable() { + @Override + public void run() { + if (videoPlayer != null && videoPlayerSeekbar != null) { + if (!videoPlayerSeekbar.isDragging()) { + PlayerControl playerControl = videoPlayer.getPlayerControl(); + float progress = playerControl.getCurrentPosition() / (float) playerControl.getDuration(); + videoPlayerSeekbar.setProgress(progress); + videoPlayerControlFrameLayout.invalidate(); + updateVideoPlayerTime(); + } + } + if (isPlaying) { + AndroidUtilities.runOnUIThread(updateProgressRunnable, 100); + } + } + }; private float animationValues[][] = new float[2][8]; @@ -147,7 +187,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat private MentionsAdapter mentionsAdapter; private RecyclerListView mentionListView; private LinearLayoutManager mentionLayoutManager; - private AnimatorSetProxy mentionListAnimation; + private AnimatorSet mentionListAnimation; private boolean allowMentions; private int animationInProgress = 0; @@ -172,7 +212,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat private String currentPathObject; private Bitmap currentThumb = null; - private int avatarsUserId; + private int avatarsDialogId; private long currentDialogId; private long mergeDialogId; private int totalImagesCount; @@ -194,8 +234,8 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat private float animationValue; private int currentRotation; private long animationStartTime; - private AnimatorSetProxy imageMoveAnimation; - private AnimatorSetProxy changeModeAnimation; + private AnimatorSet imageMoveAnimation; + private AnimatorSet changeModeAnimation; private GestureDetector gestureDetector; private DecelerateInterpolator interpolator = new DecelerateInterpolator(1.5f); private float pinchStartDistance; @@ -241,6 +281,8 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat private final static int gallery_menu_tune = 7; private final static int gallery_menu_caption = 8; private final static int gallery_menu_caption_done = 9; + private final static int gallery_menu_share = 10; + private final static int gallery_menu_openin = 11; private final static int PAGE_SPACING = AndroidUtilities.dp(30); @@ -412,7 +454,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat public int viewY; public View parentView; public Bitmap thumb; - public int user_id; + public int dialogId; public int index; public int size; public int radius; @@ -638,6 +680,11 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat protected void onDraw(Canvas canvas) { getInstance().onDraw(canvas); } + + @Override + protected boolean drawChild(Canvas canvas, View child, long drawingTime) { + return child != aspectRatioFrameLayout && super.drawChild(canvas, child, drawingTime); + } } private static volatile PhotoViewer Instance = null; @@ -673,6 +720,9 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat if (currentFileNames[a] != null && currentFileNames[a].equals(location)) { radialProgressViews[a].setProgress(1.0f, true); checkProgress(a, true); + if (Build.VERSION.SDK_INT >= 16 && a == 0 && currentMessageObject != null && currentMessageObject.isVideo()) { + onActionClick(false); + } break; } } @@ -684,10 +734,10 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat radialProgressViews[a].setProgress(progress, true); } } - } else if (id == NotificationCenter.userPhotosLoaded) { + } else if (id == NotificationCenter.dialogPhotosLoaded) { int guid = (Integer) args[4]; - int uid = (Integer) args[0]; - if (avatarsUserId == uid && classGuid == guid) { + int did = (Integer) args[0]; + if (avatarsDialogId == did && classGuid == guid) { boolean fromCache = (Boolean) args[3]; int setToImage = -1; @@ -735,7 +785,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat setImageIndex(0, true); } if (fromCache) { - MessagesController.getInstance().loadUserPhotos(avatarsUserId, 0, 80, 0, false, classGuid); + MessagesController.getInstance().loadDialogPhotos(avatarsDialogId, 0, 80, 0, false, classGuid); } } } else if (id == NotificationCenter.mediaCountDidLoaded) { @@ -887,6 +937,47 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } } + private void onSharePressed() { + if (parentActivity == null) { + return; + } + try { + File f = null; + boolean isVideo = false; + + if (currentMessageObject != null) { + isVideo = currentMessageObject.isVideo(); + /*if (currentMessageObject.messageOwner.media instanceof TLRPC.TL_messageMediaWebPage) { + AndroidUtilities.openUrl(parentActivity, currentMessageObject.messageOwner.media.webpage.url); + return; + }*/ + f = FileLoader.getPathToMessage(currentMessageObject.messageOwner); + } else if (currentFileLocation != null) { + f = FileLoader.getPathToAttach(currentFileLocation, avatarsDialogId != 0); + } + + if (f.exists()) { + Intent intent = new Intent(Intent.ACTION_SEND); + if (isVideo) { + intent.setType("video/mp4"); + } else { + intent.setType("image/jpeg"); + } + intent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(f)); + + parentActivity.startActivityForResult(Intent.createChooser(intent, LocaleController.getString("ShareFile", R.string.ShareFile)), 500); + } else { + AlertDialog.Builder builder = new AlertDialog.Builder(parentActivity); + builder.setTitle(LocaleController.getString("AppName", R.string.AppName)); + builder.setPositiveButton(LocaleController.getString("OK", R.string.OK), null); + builder.setMessage(LocaleController.getString("PleaseDownload", R.string.PleaseDownload)); + showAlertDialog(builder); + } + } catch (Exception e) { + FileLog.e("tmessages", e); + } + } + public void setParentActivity(final Activity activity) { if (parentActivity == activity) { return; @@ -983,7 +1074,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat if (currentMessageObject != null) { f = FileLoader.getPathToMessage(currentMessageObject.messageOwner); } else if (currentFileLocation != null) { - f = FileLoader.getPathToAttach(currentFileLocation, avatarsUserId != 0); + f = FileLoader.getPathToAttach(currentFileLocation, avatarsDialogId != 0); } if (f != null && f.exists()) { @@ -1062,6 +1153,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } MessageObject obj = imagesArr.get(currentIndex); if (obj.isSent()) { + closePhoto(false, false); ArrayList arr = new ArrayList<>(); arr.add(obj.getId()); @@ -1074,7 +1166,6 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } MessagesController.getInstance().deleteMessages(arr, random_ids, encryptedChat, obj.messageOwner.to_id.channel_id); - closePhoto(false, false); } } else if (!avatarsArr.isEmpty()) { if (currentIndex < 0 || currentIndex >= avatarsArr.size()) { @@ -1106,7 +1197,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat inputPhoto.id = photo.id; inputPhoto.access_hash = photo.access_hash; MessagesController.getInstance().deleteUserPhoto(inputPhoto); - MessagesStorage.getInstance().clearUserPhoto(avatarsUserId, photo.id); + MessagesStorage.getInstance().clearUserPhoto(avatarsDialogId, photo.id); imagesArrLocations.remove(currentIndex); imagesArrLocationsSizes.remove(currentIndex); avatarsArr.remove(currentIndex); @@ -1142,13 +1233,21 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat layoutParams = (FrameLayout.LayoutParams) mentionListView.getLayoutParams(); layoutParams.bottomMargin = 0; mentionListView.setLayoutParams(layoutParams); - captionTextView.clearAnimation(); captionTextView.setVisibility(View.INVISIBLE); captionEditText.openKeyboard(); lastTitle = actionBar.getTitle(); actionBar.setTitle(LocaleController.getString("PhotoCaption", R.string.PhotoCaption)); } else if (id == gallery_menu_caption_done) { closeCaptionEnter(true); + } else if (id == gallery_menu_share) { + onSharePressed(); + } else if (id == gallery_menu_openin) { + try { + AndroidUtilities.openForView(currentMessageObject, parentActivity); + closePhoto(false, false); + } catch (Exception e) { + FileLog.e("tmessages", e); + } } } @@ -1160,7 +1259,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat return true; } } else if (currentFileLocation != null) { - File f = FileLoader.getPathToAttach(currentFileLocation, avatarsUserId != 0); + File f = FileLoader.getPathToAttach(currentFileLocation, avatarsDialogId != 0); if (f.exists()) { return true; } @@ -1172,7 +1271,13 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat ActionBarMenu menu = actionBar.createMenu(); menuItem = menu.addItem(0, R.drawable.ic_ab_other); + String str = LocaleController.getString("OpenInBrowser", R.string.OpenInBrowser); + if (!TextUtils.isEmpty(str)) { //TODO add new string later + str = str.substring(0,1).toUpperCase() + str.substring(1).toLowerCase(); + } + menuItem.addSubItem(gallery_menu_openin, str, 0); menuItem.addSubItem(gallery_menu_showall, LocaleController.getString("ShowAllMedia", R.string.ShowAllMedia), 0); + menuItem.addSubItem(gallery_menu_share, LocaleController.getString("ShareFile", R.string.ShareFile), 0); menuItem.addSubItem(gallery_menu_save, LocaleController.getString("SaveToGallery", R.string.SaveToGallery), 0); menuItem.addSubItem(gallery_menu_delete, LocaleController.getString("Delete", R.string.Delete), 0); @@ -1222,44 +1327,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat shareButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - if (parentActivity == null) { - return; - } - try { - File f = null; - boolean isVideo = false; - - if (currentMessageObject != null) { - isVideo = currentMessageObject.isVideo(); - /*if (currentMessageObject.messageOwner.media instanceof TLRPC.TL_messageMediaWebPage) { - AndroidUtilities.openUrl(parentActivity, currentMessageObject.messageOwner.media.webpage.url); - return; - }*/ - f = FileLoader.getPathToMessage(currentMessageObject.messageOwner); - } else if (currentFileLocation != null) { - f = FileLoader.getPathToAttach(currentFileLocation, avatarsUserId != 0); - } - - if (f.exists()) { - Intent intent = new Intent(Intent.ACTION_SEND); - if (isVideo) { - intent.setType("video/mp4"); - } else { - intent.setType("image/jpeg"); - } - intent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(f)); - - parentActivity.startActivityForResult(Intent.createChooser(intent, LocaleController.getString("ShareFile", R.string.ShareFile)), 500); - } else { - AlertDialog.Builder builder = new AlertDialog.Builder(parentActivity); - builder.setTitle(LocaleController.getString("AppName", R.string.AppName)); - builder.setPositiveButton(LocaleController.getString("OK", R.string.OK), null); - builder.setMessage(LocaleController.getString("PleaseDownload", R.string.PleaseDownload)); - showAlertDialog(builder); - } - } catch (Exception e) { - FileLog.e("tmessages", e); - } + onSharePressed(); } }); @@ -1283,6 +1351,83 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat dateTextView.setGravity(Gravity.LEFT); bottomLayout.addView(dateTextView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, Gravity.TOP | Gravity.LEFT, 16, 25, 50, 0)); + if (Build.VERSION.SDK_INT >= 16) { + videoPlayerSeekbar = new SeekBar(containerView.getContext()); + videoPlayerSeekbar.setColors(0x66ffffff, 0xffffffff, 0xffffffff); + videoPlayerSeekbar.setDelegate(new SeekBar.SeekBarDelegate() { + @Override + public void onSeekBarDrag(float progress) { + if (videoPlayer != null) { + videoPlayer.getPlayerControl().seekTo((int) (progress * videoPlayer.getDuration())); + } + } + }); + + videoPlayerControlFrameLayout = new FrameLayout(containerView.getContext()) { + + @Override + public boolean onTouchEvent(MotionEvent event) { + int x = (int) event.getX(); + int y = (int) event.getY(); + if (videoPlayerSeekbar.onTouch(event.getAction(), event.getX() - AndroidUtilities.dp(48), event.getY())) { + getParent().requestDisallowInterceptTouchEvent(true); + invalidate(); + return true; + } + return super.onTouchEvent(event); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + long duration; + if (videoPlayer != null) { + duration = videoPlayer.getDuration(); + if (duration == ExoPlayer.UNKNOWN_TIME) { + duration = 0; + } + } else { + duration = 0; + } + duration /= 1000; + int size = (int) Math.ceil(videoPlayerTime.getPaint().measureText(String.format("%02d:%02d / %02d:%02d", duration / 60, duration % 60, duration / 60, duration % 60))); + videoPlayerSeekbar.setSize(getMeasuredWidth() - AndroidUtilities.dp(48 + 16) - size, getMeasuredHeight()); + } + + @Override + protected void onDraw(Canvas canvas) { + canvas.save(); + canvas.translate(AndroidUtilities.dp(48), 0); + videoPlayerSeekbar.draw(canvas); + canvas.restore(); + } + }; + videoPlayerControlFrameLayout.setWillNotDraw(false); + bottomLayout.addView(videoPlayerControlFrameLayout, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, Gravity.TOP | Gravity.LEFT)); + + videoPlayButton = new ImageView(containerView.getContext()); + videoPlayButton.setScaleType(ImageView.ScaleType.CENTER); + videoPlayerControlFrameLayout.addView(videoPlayButton, LayoutHelper.createFrame(48, 48, Gravity.LEFT | Gravity.TOP)); + videoPlayButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (videoPlayer != null) { + if (isPlaying) { + videoPlayer.getPlayerControl().pause(); + } else { + videoPlayer.getPlayerControl().start(); + } + } + } + }); + + videoPlayerTime = new TextView(containerView.getContext()); + videoPlayerTime.setTextColor(0xffffffff); + videoPlayerTime.setGravity(Gravity.CENTER_VERTICAL); + videoPlayerTime.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 13); + videoPlayerControlFrameLayout.addView(videoPlayerTime, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.MATCH_PARENT, Gravity.RIGHT | Gravity.TOP, 0, 0, 8, 0)); + } + pickerView = new PickerBottomLayout(actvityContext); pickerView.setBackgroundColor(0x7f000000); containerView.addView(pickerView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 48, Gravity.BOTTOM | Gravity.LEFT)); @@ -1400,13 +1545,11 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat if (size - ActionBar.getCurrentActionBarHeight() * 2 < height) { allowMentions = false; if (mentionListView != null && mentionListView.getVisibility() == View.VISIBLE) { - mentionListView.clearAnimation(); mentionListView.setVisibility(View.INVISIBLE); } } else { allowMentions = true; if (mentionListView != null && mentionListView.getVisibility() == View.INVISIBLE) { - mentionListView.clearAnimation(); mentionListView.setVisibility(View.VISIBLE); } } @@ -1415,6 +1558,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat containerView.addView(captionEditText, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, Gravity.BOTTOM | Gravity.LEFT, 0, 0, 0, -400)); mentionListView = new RecyclerListView(actvityContext); + mentionListView.setTag(5); mentionLayoutManager = new LinearLayoutManager(actvityContext) { @Override public boolean supportsPredictiveItemAnimations() { @@ -1445,22 +1589,21 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } if (mentionListView.getVisibility() == View.VISIBLE) { - ViewProxy.setAlpha(mentionListView, 1.0f); + mentionListView.setAlpha(1.0f); return; } else { mentionLayoutManager.scrollToPositionWithOffset(0, 10000); } if (allowMentions) { mentionListView.setVisibility(View.VISIBLE); - mentionListAnimation = new AnimatorSetProxy(); + mentionListAnimation = new AnimatorSet(); mentionListAnimation.playTogether( - ObjectAnimatorProxy.ofFloat(mentionListView, "alpha", 0.0f, 1.0f) + ObjectAnimator.ofFloat(mentionListView, "alpha", 0.0f, 1.0f) ); mentionListAnimation.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (mentionListAnimation != null && mentionListAnimation.equals(animation)) { - mentionListView.clearAnimation(); mentionListAnimation = null; } } @@ -1468,8 +1611,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat mentionListAnimation.setDuration(200); mentionListAnimation.start(); } else { - ViewProxy.setAlpha(mentionListView, 1.0f); - mentionListView.clearAnimation(); + mentionListView.setAlpha(1.0f); mentionListView.setVisibility(View.INVISIBLE); } } else { @@ -1482,15 +1624,14 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat return; } if (allowMentions) { - mentionListAnimation = new AnimatorSetProxy(); + mentionListAnimation = new AnimatorSet(); mentionListAnimation.playTogether( - ObjectAnimatorProxy.ofFloat(mentionListView, "alpha", 0.0f) + ObjectAnimator.ofFloat(mentionListView, "alpha", 0.0f) ); mentionListAnimation.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (mentionListAnimation != null && mentionListAnimation.equals(animation)) { - mentionListView.clearAnimation(); mentionListView.setVisibility(View.GONE); mentionListAnimation = null; } @@ -1499,7 +1640,6 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat mentionListAnimation.setDuration(200); mentionListAnimation.start(); } else { - mentionListView.clearAnimation(); mentionListView.setVisibility(View.GONE); } } @@ -1557,6 +1697,173 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat }); } + private void updateVideoPlayerTime() { + String newText; + if (videoPlayer == null) { + newText = "00:00 / 00:00"; + } else { + long current = videoPlayer.getCurrentPosition() / 1000; + long total = videoPlayer.getDuration() / 1000; + if (total != ExoPlayer.UNKNOWN_TIME && current != ExoPlayer.UNKNOWN_TIME) { + newText = String.format("%02d:%02d / %02d:%02d", current / 60, current % 60, total / 60, total % 60); + } else { + newText = "00:00 / 00:00"; + } + } + if (!TextUtils.equals(videoPlayerTime.getText(), newText)) { + videoPlayerTime.setText(newText); + } + } + + @SuppressLint("NewApi") + private void preparePlayer(File file, boolean playWhenReady) { + if (parentActivity == null) { + return; + } + releasePlayer(); + if (videoTextureView == null) { + aspectRatioFrameLayout = new AspectRatioFrameLayout(parentActivity); + aspectRatioFrameLayout.setVisibility(View.INVISIBLE); + containerView.addView(aspectRatioFrameLayout, 0, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, Gravity.CENTER)); + + videoTextureView = new TextureView(parentActivity); + videoTextureView.setOpaque(false); + videoTextureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() { + @Override + public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { + if (videoPlayer != null) { + videoPlayer.setSurface(new Surface(videoTextureView.getSurfaceTexture())); + } + } + + @Override + public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { + + } + + @Override + public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { + if (videoPlayer != null) { + videoPlayer.blockingClearSurface(); + } + return true; + } + + @Override + public void onSurfaceTextureUpdated(SurfaceTexture surface) { + if (!textureUploaded) { + textureUploaded = true; + containerView.invalidate(); + } + } + }); + aspectRatioFrameLayout.addView(videoTextureView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, Gravity.CENTER)); + } + textureUploaded = false; + videoCrossfadeStarted = false; + videoTextureView.setAlpha(videoCrossfadeAlpha = 0.0f); + if (videoPlayer == null) { + videoPlayer = new VideoPlayer(new VideoPlayer.ExtractorRendererBuilder(parentActivity, "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36", Uri.fromFile(file))); + videoPlayer.addListener(new VideoPlayer.Listener() { + @Override + public void onStateChanged(boolean playWhenReady, int playbackState) { + if (videoPlayer == null) { + return; + } + if (playbackState == VideoPlayer.STATE_READY && aspectRatioFrameLayout.getVisibility() != View.VISIBLE) { + aspectRatioFrameLayout.setVisibility(View.VISIBLE); + } + if (videoPlayer.getPlayerControl().isPlaying() && playbackState != VideoPlayer.STATE_ENDED) { + if (!isPlaying) { + isPlaying = true; + videoPlayButton.setImageResource(R.drawable.inline_video_pause); + AndroidUtilities.runOnUIThread(updateProgressRunnable); + } + } else if (isPlaying) { + isPlaying = false; + videoPlayButton.setImageResource(R.drawable.inline_video_play); + AndroidUtilities.cancelRunOnUIThread(updateProgressRunnable); + if (playbackState == VideoPlayer.STATE_ENDED) { + if (!videoPlayerSeekbar.isDragging()) { + videoPlayerSeekbar.setProgress(0.0f); + videoPlayerControlFrameLayout.invalidate(); + videoPlayer.seekTo(0); + videoPlayer.getPlayerControl().pause(); + } + } + } + updateVideoPlayerTime(); + } + + @Override + public void onError(Exception e) { + FileLog.e("tmessages", e); + } + + @Override + public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { + if (aspectRatioFrameLayout != null) { + aspectRatioFrameLayout.setAspectRatio(height == 0 ? 1 : (width * pixelWidthHeightRatio) / height); + } + } + }); + long duration; + if (videoPlayer != null) { + duration = videoPlayer.getDuration(); + if (duration == ExoPlayer.UNKNOWN_TIME) { + duration = 0; + } + } else { + duration = 0; + } + duration /= 1000; + int size = (int) Math.ceil(videoPlayerTime.getPaint().measureText(String.format("%02d:%02d / %02d:%02d", duration / 60, duration % 60, duration / 60, duration % 60))); + + playerNeedsPrepare = true; + } + if (playerNeedsPrepare) { + videoPlayer.prepare(); + playerNeedsPrepare = false; + } + if (videoPlayerControlFrameLayout != null) { + videoPlayerControlFrameLayout.setVisibility(View.VISIBLE); + dateTextView.setVisibility(View.GONE); + nameTextView.setVisibility(View.GONE); + shareButton.setVisibility(View.GONE); + menuItem.showSubItem(gallery_menu_share); + } + if (videoTextureView.getSurfaceTexture() != null) { + videoPlayer.setSurface(new Surface(videoTextureView.getSurfaceTexture())); + } + videoPlayer.setPlayWhenReady(playWhenReady); + } + + private void releasePlayer() { + if (videoPlayer != null) { + videoPlayer.release(); + videoPlayer = null; + } + if (aspectRatioFrameLayout != null) { + containerView.removeView(aspectRatioFrameLayout); + aspectRatioFrameLayout = null; + } + if (videoTextureView != null) { + videoTextureView = null; + } + if (isPlaying) { + isPlaying = false; + videoPlayButton.setImageResource(R.drawable.inline_video_play); + AndroidUtilities.cancelRunOnUIThread(updateProgressRunnable); + } + if (videoPlayerControlFrameLayout != null) { + videoPlayerControlFrameLayout.setVisibility(View.GONE); + dateTextView.setVisibility(View.VISIBLE); + nameTextView.setVisibility(View.VISIBLE); + shareButton.setVisibility(View.VISIBLE); + menuItem.hideSubItem(gallery_menu_share); + } + } + private void updateCaptionTextForCurrentPhoto(Object object) { CharSequence caption = null; if (object instanceof MediaController.PhotoEntry) { @@ -1729,31 +2036,28 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat zoomAnimation = true; } - imageMoveAnimation = new AnimatorSetProxy(); + imageMoveAnimation = new AnimatorSet(); if (currentEditMode == 1) { imageMoveAnimation.playTogether( - ObjectAnimatorProxy.ofFloat(editorDoneLayout, "translationY", AndroidUtilities.dp(48)), - ObjectAnimatorProxy.ofFloat(PhotoViewer.this, "animationValue", 0, 1), - ObjectAnimatorProxy.ofFloat(photoCropView, "alpha", 0) + ObjectAnimator.ofFloat(editorDoneLayout, "translationY", AndroidUtilities.dp(48)), + ObjectAnimator.ofFloat(PhotoViewer.this, "animationValue", 0, 1), + ObjectAnimator.ofFloat(photoCropView, "alpha", 0) ); } else if (currentEditMode == 2) { photoFilterView.shutdown(); imageMoveAnimation.playTogether( - ObjectAnimatorProxy.ofFloat(photoFilterView.getToolsView(), "translationY", AndroidUtilities.dp(126)), - ObjectAnimatorProxy.ofFloat(PhotoViewer.this, "animationValue", 0, 1) + ObjectAnimator.ofFloat(photoFilterView.getToolsView(), "translationY", AndroidUtilities.dp(126)), + ObjectAnimator.ofFloat(PhotoViewer.this, "animationValue", 0, 1) ); } imageMoveAnimation.setDuration(200); imageMoveAnimation.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (currentEditMode == 1) { - photoCropView.clearAnimation(); - editorDoneLayout.clearAnimation(); editorDoneLayout.setVisibility(View.GONE); photoCropView.setVisibility(View.GONE); } else if (currentEditMode == 2) { - photoFilterView.getToolsView().clearAnimation(); containerView.removeView(photoFilterView); photoFilterView = null; } @@ -1766,21 +2070,21 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat updateMinMax(scale); containerView.invalidate(); - AnimatorSetProxy animatorSet = new AnimatorSetProxy(); - ArrayList arrayList = new ArrayList<>(); - arrayList.add(ObjectAnimatorProxy.ofFloat(pickerView, "translationY", 0)); - arrayList.add(ObjectAnimatorProxy.ofFloat(actionBar, "translationY", 0)); + AnimatorSet animatorSet = new AnimatorSet(); + ArrayList arrayList = new ArrayList<>(); + arrayList.add(ObjectAnimator.ofFloat(pickerView, "translationY", 0)); + arrayList.add(ObjectAnimator.ofFloat(actionBar, "translationY", 0)); if (needCaptionLayout) { - arrayList.add(ObjectAnimatorProxy.ofFloat(captionTextView, "translationY", 0)); + arrayList.add(ObjectAnimator.ofFloat(captionTextView, "translationY", 0)); } if (sendPhotoType == 0) { - arrayList.add(ObjectAnimatorProxy.ofFloat(checkImageView, "alpha", 1)); + arrayList.add(ObjectAnimator.ofFloat(checkImageView, "alpha", 1)); } animatorSet.playTogether(arrayList); animatorSet.setDuration(200); animatorSet.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationStart(Object animation) { + public void onAnimationStart(Animator animation) { pickerView.setVisibility(View.VISIBLE); actionBar.setVisibility(View.VISIBLE); if (needCaptionLayout) { @@ -1790,18 +2094,6 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat checkImageView.setVisibility(View.VISIBLE); } } - - @Override - public void onAnimationEnd(Object animation) { - pickerView.clearAnimation(); - actionBar.clearAnimation(); - if (needCaptionLayout) { - captionTextView.clearAnimation(); - } - if (sendPhotoType == 0) { - checkImageView.clearAnimation(); - } - } }); animatorSet.start(); } @@ -1833,32 +2125,28 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } editorDoneLayout.doneButtonTextView.setText(LocaleController.getString("Crop", R.string.Crop)); - changeModeAnimation = new AnimatorSetProxy(); - ArrayList arrayList = new ArrayList<>(); - arrayList.add(ObjectAnimatorProxy.ofFloat(pickerView, "translationY", 0, AndroidUtilities.dp(96))); - arrayList.add(ObjectAnimatorProxy.ofFloat(actionBar, "translationY", 0, -actionBar.getHeight())); + changeModeAnimation = new AnimatorSet(); + ArrayList arrayList = new ArrayList<>(); + arrayList.add(ObjectAnimator.ofFloat(pickerView, "translationY", 0, AndroidUtilities.dp(96))); + arrayList.add(ObjectAnimator.ofFloat(actionBar, "translationY", 0, -actionBar.getHeight())); if (needCaptionLayout) { - arrayList.add(ObjectAnimatorProxy.ofFloat(captionTextView, "translationY", 0, AndroidUtilities.dp(96))); + arrayList.add(ObjectAnimator.ofFloat(captionTextView, "translationY", 0, AndroidUtilities.dp(96))); } if (sendPhotoType == 0) { - arrayList.add(ObjectAnimatorProxy.ofFloat(checkImageView, "alpha", 1, 0)); + arrayList.add(ObjectAnimator.ofFloat(checkImageView, "alpha", 1, 0)); } changeModeAnimation.playTogether(arrayList); changeModeAnimation.setDuration(200); changeModeAnimation.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { changeModeAnimation = null; - pickerView.clearAnimation(); - actionBar.clearAnimation(); pickerView.setVisibility(View.GONE); actionBar.setVisibility(View.GONE); if (needCaptionLayout) { - captionTextView.clearAnimation(); captionTextView.setVisibility(View.INVISIBLE); } if (sendPhotoType == 0) { - checkImageView.clearAnimation(); checkImageView.setVisibility(View.GONE); } @@ -1882,22 +2170,22 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat zoomAnimation = true; } - imageMoveAnimation = new AnimatorSetProxy(); + imageMoveAnimation = new AnimatorSet(); imageMoveAnimation.playTogether( - ObjectAnimatorProxy.ofFloat(editorDoneLayout, "translationY", AndroidUtilities.dp(48), 0), - ObjectAnimatorProxy.ofFloat(PhotoViewer.this, "animationValue", 0, 1), - ObjectAnimatorProxy.ofFloat(photoCropView, "alpha", 0, 1) + ObjectAnimator.ofFloat(editorDoneLayout, "translationY", AndroidUtilities.dp(48), 0), + ObjectAnimator.ofFloat(PhotoViewer.this, "animationValue", 0, 1), + ObjectAnimator.ofFloat(photoCropView, "alpha", 0, 1) ); imageMoveAnimation.setDuration(200); imageMoveAnimation.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationStart(Object animation) { + public void onAnimationStart(Animator animation) { editorDoneLayout.setVisibility(View.VISIBLE); photoCropView.setVisibility(View.VISIBLE); } @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { imageMoveAnimation = null; currentEditMode = mode; animateToScale = 1; @@ -1906,8 +2194,6 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat scale = 1; updateMinMax(scale); containerView.invalidate(); - editorDoneLayout.clearAnimation(); - photoCropView.clearAnimation(); } }); imageMoveAnimation.start(); @@ -1949,35 +2235,31 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } }); //photoFilterView.setEditViewFirst(); - ViewProxy.setTranslationY(photoFilterView.getToolsView(), AndroidUtilities.dp(126)); + photoFilterView.getToolsView().setTranslationY(AndroidUtilities.dp(126)); } - changeModeAnimation = new AnimatorSetProxy(); - ArrayList arrayList = new ArrayList<>(); - arrayList.add(ObjectAnimatorProxy.ofFloat(pickerView, "translationY", 0, AndroidUtilities.dp(96))); - arrayList.add(ObjectAnimatorProxy.ofFloat(actionBar, "translationY", 0, -actionBar.getHeight())); + changeModeAnimation = new AnimatorSet(); + ArrayList arrayList = new ArrayList<>(); + arrayList.add(ObjectAnimator.ofFloat(pickerView, "translationY", 0, AndroidUtilities.dp(96))); + arrayList.add(ObjectAnimator.ofFloat(actionBar, "translationY", 0, -actionBar.getHeight())); if (needCaptionLayout) { - arrayList.add(ObjectAnimatorProxy.ofFloat(captionTextView, "translationY", 0, AndroidUtilities.dp(96))); + arrayList.add(ObjectAnimator.ofFloat(captionTextView, "translationY", 0, AndroidUtilities.dp(96))); } if (sendPhotoType == 0) { - arrayList.add(ObjectAnimatorProxy.ofFloat(checkImageView, "alpha", 1, 0)); + arrayList.add(ObjectAnimator.ofFloat(checkImageView, "alpha", 1, 0)); } changeModeAnimation.playTogether(arrayList); changeModeAnimation.setDuration(200); changeModeAnimation.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { changeModeAnimation = null; - pickerView.clearAnimation(); - actionBar.clearAnimation(); pickerView.setVisibility(View.GONE); actionBar.setVisibility(View.GONE); if (needCaptionLayout) { - captionTextView.clearAnimation(); captionTextView.setVisibility(View.INVISIBLE); } if (sendPhotoType == 0) { - checkImageView.clearAnimation(); checkImageView.setVisibility(View.GONE); } @@ -2000,20 +2282,20 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat zoomAnimation = true; } - imageMoveAnimation = new AnimatorSetProxy(); + imageMoveAnimation = new AnimatorSet(); imageMoveAnimation.playTogether( - ObjectAnimatorProxy.ofFloat(PhotoViewer.this, "animationValue", 0, 1), - ObjectAnimatorProxy.ofFloat(photoFilterView.getToolsView(), "translationY", AndroidUtilities.dp(126), 0) + ObjectAnimator.ofFloat(PhotoViewer.this, "animationValue", 0, 1), + ObjectAnimator.ofFloat(photoFilterView.getToolsView(), "translationY", AndroidUtilities.dp(126), 0) ); imageMoveAnimation.setDuration(200); imageMoveAnimation.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationStart(Object animation) { + public void onAnimationStart(Animator animation) { } @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { photoFilterView.init(); imageMoveAnimation = null; currentEditMode = mode; @@ -2023,7 +2305,6 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat scale = 1; updateMinMax(scale); containerView.invalidate(); - photoFilterView.getToolsView().clearAnimation(); } }); imageMoveAnimation.start(); @@ -2034,14 +2315,14 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } private void toggleCheckImageView(boolean show) { - AnimatorSetProxy animatorSet = new AnimatorSetProxy(); - ArrayList arrayList = new ArrayList<>(); - arrayList.add(ObjectAnimatorProxy.ofFloat(pickerView, "alpha", show ? 1.0f : 0.0f)); + AnimatorSet animatorSet = new AnimatorSet(); + ArrayList arrayList = new ArrayList<>(); + arrayList.add(ObjectAnimator.ofFloat(pickerView, "alpha", show ? 1.0f : 0.0f)); if (needCaptionLayout) { - arrayList.add(ObjectAnimatorProxy.ofFloat(captionTextView, "alpha", show ? 1.0f : 0.0f)); + arrayList.add(ObjectAnimator.ofFloat(captionTextView, "alpha", show ? 1.0f : 0.0f)); } if (sendPhotoType == 0) { - arrayList.add(ObjectAnimatorProxy.ofFloat(checkImageView, "alpha", show ? 1.0f : 0.0f)); + arrayList.add(ObjectAnimator.ofFloat(checkImageView, "alpha", show ? 1.0f : 0.0f)); } animatorSet.playTogether(arrayList); animatorSet.setDuration(200); @@ -2063,25 +2344,23 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat bottomLayout.setEnabled(show); if (animated) { - ArrayList arrayList = new ArrayList<>(); - arrayList.add(ObjectAnimatorProxy.ofFloat(actionBar, "alpha", show ? 1.0f : 0.0f)); - arrayList.add(ObjectAnimatorProxy.ofFloat(bottomLayout, "alpha", show ? 1.0f : 0.0f)); + ArrayList arrayList = new ArrayList<>(); + arrayList.add(ObjectAnimator.ofFloat(actionBar, "alpha", show ? 1.0f : 0.0f)); + arrayList.add(ObjectAnimator.ofFloat(bottomLayout, "alpha", show ? 1.0f : 0.0f)); if (captionTextView.getTag() != null) { - arrayList.add(ObjectAnimatorProxy.ofFloat(captionTextView, "alpha", show ? 1.0f : 0.0f)); + arrayList.add(ObjectAnimator.ofFloat(captionTextView, "alpha", show ? 1.0f : 0.0f)); } - currentActionBarAnimation = new AnimatorSetProxy(); + currentActionBarAnimation = new AnimatorSet(); currentActionBarAnimation.playTogether(arrayList); if (!show) { currentActionBarAnimation.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (currentActionBarAnimation != null && currentActionBarAnimation.equals(animation)) { actionBar.setVisibility(View.GONE); if (canShowBottom) { - bottomLayout.clearAnimation(); bottomLayout.setVisibility(View.GONE); if (captionTextView.getTag() != null) { - captionTextView.clearAnimation(); captionTextView.setVisibility(View.INVISIBLE); } } @@ -2094,18 +2373,16 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat currentActionBarAnimation.setDuration(200); currentActionBarAnimation.start(); } else { - ViewProxy.setAlpha(actionBar, show ? 1.0f : 0.0f); - ViewProxy.setAlpha(bottomLayout, show ? 1.0f : 0.0f); + actionBar.setAlpha(show ? 1.0f : 0.0f); + bottomLayout.setAlpha(show ? 1.0f : 0.0f); if (captionTextView.getTag() != null) { - ViewProxy.setAlpha(captionTextView, show ? 1.0f : 0.0f); + captionTextView.setAlpha(show ? 1.0f : 0.0f); } if (!show) { actionBar.setVisibility(View.GONE); if (canShowBottom) { - bottomLayout.clearAnimation(); bottomLayout.setVisibility(View.GONE); if (captionTextView.getTag() != null) { - captionTextView.clearAnimation(); captionTextView.setVisibility(View.INVISIBLE); } } @@ -2221,7 +2498,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat currentFileNames[0] = null; currentFileNames[1] = null; currentFileNames[2] = null; - avatarsUserId = 0; + avatarsDialogId = 0; totalImagesCount = 0; totalImagesCountMerge = 0; currentEditMode = 0; @@ -2250,32 +2527,27 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat bottomLayout.setVisibility(View.VISIBLE); shareButton.setVisibility(View.GONE); menuItem.hideSubItem(gallery_menu_showall); - ViewProxy.setTranslationY(actionBar, 0); - ViewProxy.setTranslationY(pickerView, 0); - ViewProxy.setAlpha(checkImageView, 1.0f); - ViewProxy.setAlpha(pickerView, 1.0f); - checkImageView.clearAnimation(); - pickerView.clearAnimation(); - editorDoneLayout.clearAnimation(); + menuItem.hideSubItem(gallery_menu_share); + menuItem.hideSubItem(gallery_menu_openin); + actionBar.setTranslationY(0); + pickerView.setTranslationY(0); + checkImageView.setAlpha(1.0f); + pickerView.setAlpha(1.0f); checkImageView.setVisibility(View.GONE); pickerView.setVisibility(View.GONE); cropItem.setVisibility(View.GONE); tuneItem.setVisibility(View.GONE); captionItem.setVisibility(View.GONE); captionDoneItem.setVisibility(View.GONE); - captionEditText.clearAnimation(); captionEditText.setVisibility(View.GONE); mentionListView.setVisibility(View.GONE); editorDoneLayout.setVisibility(View.GONE); captionTextView.setTag(null); - captionTextView.clearAnimation(); captionTextView.setVisibility(View.INVISIBLE); if (photoCropView != null) { - photoCropView.clearAnimation(); photoCropView.setVisibility(View.GONE); } if (photoFilterView != null) { - photoFilterView.clearAnimation(); photoFilterView.setVisibility(View.GONE); } @@ -2287,22 +2559,26 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat if (messageObject != null && messages == null) { imagesArr.add(messageObject); - if (!(messageObject.messageOwner.media instanceof TLRPC.TL_messageMediaWebPage) && (messageObject.messageOwner.action == null || messageObject.messageOwner.action instanceof TLRPC.TL_messageActionEmpty)) { + if (currentAnimation != null) { + needSearchImageInArr = false; + } else if (!(messageObject.messageOwner.media instanceof TLRPC.TL_messageMediaWebPage) && (messageObject.messageOwner.action == null || messageObject.messageOwner.action instanceof TLRPC.TL_messageActionEmpty)) { needSearchImageInArr = true; imagesByIds[0].put(messageObject.getId(), messageObject); menuItem.showSubItem(gallery_menu_showall); - } else { - menuItem.hideSubItem(gallery_menu_showall); } setImageIndex(0, true); } else if (fileLocation != null) { - avatarsUserId = object.user_id; + avatarsDialogId = object.dialogId; imagesArrLocations.add(fileLocation); imagesArrLocationsSizes.add(object.size); avatarsArr.add(new TLRPC.TL_photoEmpty()); - bottomLayout.clearAnimation(); - shareButton.setVisibility(View.VISIBLE); + shareButton.setVisibility(videoPlayerControlFrameLayout == null || videoPlayerControlFrameLayout.getVisibility() != View.VISIBLE ? View.VISIBLE : View.GONE); menuItem.hideSubItem(gallery_menu_showall); + if (shareButton.getVisibility() == View.VISIBLE) { + menuItem.hideSubItem(gallery_menu_share); + } else { + menuItem.showSubItem(gallery_menu_share); + } setImageIndex(0, true); currentUserAvatarLocation = fileLocation; } else if (messages != null) { @@ -2326,7 +2602,6 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat imagesArrLocals.addAll(photos); setImageIndex(index, true); pickerView.setVisibility(View.VISIBLE); - bottomLayout.clearAnimation(); bottomLayout.setVisibility(View.GONE); canShowBottom = false; Object obj = imagesArrLocals.get(index); @@ -2348,13 +2623,18 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat updateSelectedCount(); } - if (currentDialogId != 0 && totalImagesCount == 0) { - SharedMediaQuery.getMediaCount(currentDialogId, SharedMediaQuery.MEDIA_PHOTOVIDEO, classGuid, true); - if (mergeDialogId != 0) { - SharedMediaQuery.getMediaCount(mergeDialogId, SharedMediaQuery.MEDIA_PHOTOVIDEO, classGuid, true); + if (currentAnimation == null) { + if (currentDialogId != 0 && totalImagesCount == 0) { + SharedMediaQuery.getMediaCount(currentDialogId, SharedMediaQuery.MEDIA_PHOTOVIDEO, classGuid, true); + if (mergeDialogId != 0) { + SharedMediaQuery.getMediaCount(mergeDialogId, SharedMediaQuery.MEDIA_PHOTOVIDEO, classGuid, true); + } + } else if (avatarsDialogId != 0) { + MessagesController.getInstance().loadDialogPhotos(avatarsDialogId, 0, 80, 0, true, classGuid); } - } else if (avatarsUserId != 0) { - MessagesController.getInstance().loadUserPhotos(avatarsUserId, 0, 80, 0, true, classGuid); + } + if (currentMessageObject != null && currentMessageObject.isVideo()) { + onActionClick(false); } } @@ -2367,7 +2647,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } private void setImageIndex(int index, boolean init) { - if (currentIndex == index) { + if (currentIndex == index || placeProvider == null) { return; } if (!init) { @@ -2396,6 +2676,11 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } else { menuItem.hideSubItem(gallery_menu_delete); } + if (isVideo && Build.VERSION.SDK_INT >= 16) { + menuItem.showSubItem(gallery_menu_openin); + } else { + menuItem.hideSubItem(gallery_menu_openin); + } if (currentMessageObject.isFromUser()) { TLRPC.User user = MessagesController.getInstance().getUser(currentMessageObject.messageOwner.from_id); if (user != null) { @@ -2421,52 +2706,72 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat CharSequence caption = currentMessageObject.caption; setCurrentCaption(caption); - if (totalImagesCount + totalImagesCountMerge != 0 && !needSearchImageInArr) { - if (opennedFromMedia) { - if (imagesArr.size() < totalImagesCount + totalImagesCountMerge && !loadingMoreImages && currentIndex > imagesArr.size() - 5) { - int loadFromMaxId = imagesArr.isEmpty() ? 0 : imagesArr.get(imagesArr.size() - 1).getId(); - int loadIndex = 0; - if (endReached[loadIndex] && mergeDialogId != 0) { - loadIndex = 1; - if (!imagesArr.isEmpty() && imagesArr.get(imagesArr.size() - 1).getDialogId() != mergeDialogId) { - loadFromMaxId = 0; - } - } - - SharedMediaQuery.loadMedia(loadIndex == 0 ? currentDialogId : mergeDialogId, 0, 80, loadFromMaxId, SharedMediaQuery.MEDIA_PHOTOVIDEO, true, classGuid); - loadingMoreImages = true; - } - actionBar.setTitle(LocaleController.formatString("Of", R.string.Of, currentIndex + 1, totalImagesCount + totalImagesCountMerge)); - } else { - if (imagesArr.size() < totalImagesCount + totalImagesCountMerge && !loadingMoreImages && currentIndex < 5) { - int loadFromMaxId = imagesArr.isEmpty() ? 0 : imagesArr.get(0).getId(); - int loadIndex = 0; - if (endReached[loadIndex] && mergeDialogId != 0) { - loadIndex = 1; - if (!imagesArr.isEmpty() && imagesArr.get(0).getDialogId() != mergeDialogId) { - loadFromMaxId = 0; - } - } - - SharedMediaQuery.loadMedia(loadIndex == 0 ? currentDialogId : mergeDialogId, 0, 80, loadFromMaxId, SharedMediaQuery.MEDIA_PHOTOVIDEO, true, classGuid); - loadingMoreImages = true; - } - actionBar.setTitle(LocaleController.formatString("Of", R.string.Of, (totalImagesCount + totalImagesCountMerge - imagesArr.size()) + currentIndex + 1, totalImagesCount + totalImagesCountMerge)); - } - } else if (currentMessageObject.messageOwner.media instanceof TLRPC.TL_messageMediaWebPage) { - actionBar.setTitle(LocaleController.getString("AttachPhoto", R.string.AttachPhoto)); - } - if (currentMessageObject.messageOwner.ttl != 0) { + if (currentAnimation != null) { menuItem.hideSubItem(gallery_menu_save); - shareButton.setVisibility(View.GONE); - } else { - menuItem.showSubItem(gallery_menu_save); + menuItem.hideSubItem(gallery_menu_share); + if (!currentMessageObject.canDeleteMessage(null)) { + menuItem.setVisibility(View.GONE); + } shareButton.setVisibility(View.VISIBLE); + actionBar.setTitle(LocaleController.getString("AttachGif", R.string.AttachGif)); + } else { + if (totalImagesCount + totalImagesCountMerge != 0 && !needSearchImageInArr) { + if (opennedFromMedia) { + if (imagesArr.size() < totalImagesCount + totalImagesCountMerge && !loadingMoreImages && currentIndex > imagesArr.size() - 5) { + int loadFromMaxId = imagesArr.isEmpty() ? 0 : imagesArr.get(imagesArr.size() - 1).getId(); + int loadIndex = 0; + if (endReached[loadIndex] && mergeDialogId != 0) { + loadIndex = 1; + if (!imagesArr.isEmpty() && imagesArr.get(imagesArr.size() - 1).getDialogId() != mergeDialogId) { + loadFromMaxId = 0; + } + } + + SharedMediaQuery.loadMedia(loadIndex == 0 ? currentDialogId : mergeDialogId, 0, 80, loadFromMaxId, SharedMediaQuery.MEDIA_PHOTOVIDEO, true, classGuid); + loadingMoreImages = true; + } + actionBar.setTitle(LocaleController.formatString("Of", R.string.Of, currentIndex + 1, totalImagesCount + totalImagesCountMerge)); + } else { + if (imagesArr.size() < totalImagesCount + totalImagesCountMerge && !loadingMoreImages && currentIndex < 5) { + int loadFromMaxId = imagesArr.isEmpty() ? 0 : imagesArr.get(0).getId(); + int loadIndex = 0; + if (endReached[loadIndex] && mergeDialogId != 0) { + loadIndex = 1; + if (!imagesArr.isEmpty() && imagesArr.get(0).getDialogId() != mergeDialogId) { + loadFromMaxId = 0; + } + } + + SharedMediaQuery.loadMedia(loadIndex == 0 ? currentDialogId : mergeDialogId, 0, 80, loadFromMaxId, SharedMediaQuery.MEDIA_PHOTOVIDEO, true, classGuid); + loadingMoreImages = true; + } + actionBar.setTitle(LocaleController.formatString("Of", R.string.Of, (totalImagesCount + totalImagesCountMerge - imagesArr.size()) + currentIndex + 1, totalImagesCount + totalImagesCountMerge)); + } + } else if (currentMessageObject.messageOwner.media instanceof TLRPC.TL_messageMediaWebPage) { + if (currentMessageObject.isVideo()) { + actionBar.setTitle(LocaleController.getString("AttachVideo", R.string.AttachVideo)); + } else { + actionBar.setTitle(LocaleController.getString("AttachPhoto", R.string.AttachPhoto)); + } + } + if (currentMessageObject.messageOwner.ttl != 0) { + menuItem.hideSubItem(gallery_menu_save); + shareButton.setVisibility(View.GONE); + menuItem.hideSubItem(gallery_menu_share); + } else { + menuItem.showSubItem(gallery_menu_save); + shareButton.setVisibility(videoPlayerControlFrameLayout == null || videoPlayerControlFrameLayout.getVisibility() != View.VISIBLE ? View.VISIBLE : View.GONE); + if (shareButton.getVisibility() == View.VISIBLE) { + menuItem.hideSubItem(gallery_menu_share); + } else { + menuItem.showSubItem(gallery_menu_share); + } + } } } else if (!imagesArrLocations.isEmpty()) { nameTextView.setText(""); dateTextView.setText(""); - if (avatarsUserId == UserConfig.getClientUserId() && !avatarsArr.isEmpty()) { + if (avatarsDialogId == UserConfig.getClientUserId() && !avatarsArr.isEmpty()) { menuItem.showSubItem(gallery_menu_delete); } else { menuItem.hideSubItem(gallery_menu_delete); @@ -2482,7 +2787,12 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } actionBar.setTitle(LocaleController.formatString("Of", R.string.Of, currentIndex + 1, imagesArrLocations.size())); menuItem.showSubItem(gallery_menu_save); - shareButton.setVisibility(View.VISIBLE); + shareButton.setVisibility(videoPlayerControlFrameLayout == null || videoPlayerControlFrameLayout.getVisibility() != View.VISIBLE ? View.VISIBLE : View.GONE); + if (shareButton.getVisibility() == View.VISIBLE) { + menuItem.hideSubItem(gallery_menu_share); + } else { + menuItem.showSubItem(gallery_menu_share); + } } else if (!imagesArrLocals.isEmpty()) { Object object = imagesArrLocals.get(index); if (index < 0 || index >= imagesArrLocals.size()) { @@ -2545,6 +2855,10 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat animationStartTime = 0; imageMoveAnimation = null; changeModeAnimation = null; + if (aspectRatioFrameLayout != null) { + aspectRatioFrameLayout.setVisibility(View.INVISIBLE); + } + releasePlayer(); pinchStartDistance = 0; pinchStartScale = 1; @@ -2613,12 +2927,11 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat CharSequence str = Emoji.replaceEmoji(new SpannableStringBuilder(caption.toString()), MessageObject.getTextPaint().getFontMetricsInt(), AndroidUtilities.dp(20), false); captionTextView.setTag(str); captionTextView.setText(str); - ViewProxy.setAlpha(captionTextView, bottomLayout.getVisibility() == View.VISIBLE || pickerView.getVisibility() == View.VISIBLE ? 1.0f : 0.0f); + captionTextView.setAlpha(bottomLayout.getVisibility() == View.VISIBLE || pickerView.getVisibility() == View.VISIBLE ? 1.0f : 0.0f); AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { captionTextViewOld.setTag(null); - captionTextViewOld.clearAnimation(); captionTextViewOld.setVisibility(View.INVISIBLE); captionTextViewNew.setVisibility(bottomLayout.getVisibility() == View.VISIBLE || pickerView.getVisibility() == View.VISIBLE ? View.VISIBLE : View.INVISIBLE); } @@ -2626,7 +2939,6 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } else { captionItem.setIcon(R.drawable.photo_text); captionTextView.setTag(null); - captionTextView.clearAnimation(); captionTextView.setVisibility(View.INVISIBLE); } } @@ -2643,11 +2955,19 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat boolean isVideo = false; if (currentMessageObject != null) { MessageObject messageObject = imagesArr.get(index); - f = FileLoader.getPathToMessage(messageObject.messageOwner); + if (!TextUtils.isEmpty(messageObject.messageOwner.attachPath)) { + f = new File(messageObject.messageOwner.attachPath); + if (!f.exists()) { + f = null; + } + } + if (f == null) { + f = FileLoader.getPathToMessage(messageObject.messageOwner); + } isVideo = messageObject.isVideo(); } else if (currentFileLocation != null) { TLRPC.FileLocation location = imagesArrLocations.get(index); - f = FileLoader.getPathToAttach(location, avatarsUserId != 0); + f = FileLoader.getPathToAttach(location, avatarsDialogId != 0); } else if (currentPathObject != null) { f = new File(FileLoader.getInstance().getDirectory(FileLoader.MEDIA_DIR_DOCUMENT), currentFileNames[a]); if (!f.exists()) { @@ -2758,6 +3078,9 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } else { imageReceiver.setImageBitmap(parentActivity.getResources().getDrawable(R.drawable.photoview_placeholder)); } + } else if (messageObject != null && currentAnimation != null) { + imageReceiver.setImageBitmap(currentAnimation); + currentAnimation.setSecondParentView(containerView); } else { imageReceiver.setNeedsQualityThumb(false); Bitmap placeHolder = null; @@ -2768,7 +3091,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat size[0] = -1; } TLRPC.PhotoSize thumbLocation = messageObject != null ? FileLoader.getClosestPhotoSizeWithSize(messageObject.photoThumbs, 100) : null; - imageReceiver.setImage(fileLocation, null, null, placeHolder != null ? new BitmapDrawable(null, placeHolder) : null, thumbLocation != null ? thumbLocation.location : null, "b", size[0], null, avatarsUserId != 0); + imageReceiver.setImage(fileLocation, null, null, placeHolder != null ? new BitmapDrawable(null, placeHolder) : null, thumbLocation != null ? thumbLocation.location : null, "b", size[0], null, avatarsDialogId != 0); } } else { imageReceiver.setNeedsQualityThumb(false); @@ -2866,7 +3189,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat NotificationCenter.getInstance().addObserver(this, NotificationCenter.FileLoadProgressChanged); NotificationCenter.getInstance().addObserver(this, NotificationCenter.mediaCountDidLoaded); NotificationCenter.getInstance().addObserver(this, NotificationCenter.mediaDidLoaded); - NotificationCenter.getInstance().addObserver(this, NotificationCenter.userPhotosLoaded); + NotificationCenter.getInstance().addObserver(this, NotificationCenter.dialogPhotosLoaded); NotificationCenter.getInstance().addObserver(this, NotificationCenter.emojiDidLoaded); placeProvider = provider; @@ -2883,6 +3206,10 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat if (object != null) { disableShowCheck = true; animationInProgress = 1; + if (messageObject != null) { + currentAnimation = object.imageReceiver.getAnimation(); + } + onPhotoShow(messageObject, fileLocation, messages, photos, index, object); final Rect drawRegion = object.imageReceiver.getDrawRegion(); @@ -2894,13 +3221,13 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat animatingImageView.setNeedRadius(object.radius != 0); animatingImageView.setImageBitmap(object.thumb); - ViewProxy.setAlpha(animatingImageView, 1.0f); - ViewProxy.setPivotX(animatingImageView, 0.0f); - ViewProxy.setPivotY(animatingImageView, 0.0f); - ViewProxy.setScaleX(animatingImageView, object.scale); - ViewProxy.setScaleY(animatingImageView, object.scale); - ViewProxy.setTranslationX(animatingImageView, object.viewX + drawRegion.left * object.scale); - ViewProxy.setTranslationY(animatingImageView, object.viewY + drawRegion.top * object.scale); + animatingImageView.setAlpha(1.0f); + animatingImageView.setPivotX(0.0f); + animatingImageView.setPivotY(0.0f); + animatingImageView.setScaleX(object.scale); + animatingImageView.setScaleY(object.scale); + animatingImageView.setTranslationX(object.viewX + drawRegion.left * object.scale); + animatingImageView.setTranslationY(object.viewY + drawRegion.top * object.scale); final ViewGroup.LayoutParams layoutParams = animatingImageView.getLayoutParams(); layoutParams.width = (drawRegion.right - drawRegion.left); layoutParams.height = (drawRegion.bottom - drawRegion.top); @@ -2929,10 +3256,10 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat clipTop = Math.max(clipTop, clipVertical); clipBottom = Math.max(clipBottom, clipVertical); - animationValues[0][0] = ViewProxy.getScaleX(animatingImageView); - animationValues[0][1] = ViewProxy.getScaleY(animatingImageView); - animationValues[0][2] = ViewProxy.getTranslationX(animatingImageView); - animationValues[0][3] = ViewProxy.getTranslationY(animatingImageView); + animationValues[0][0] = animatingImageView.getScaleX(); + animationValues[0][1] = animatingImageView.getScaleY(); + animationValues[0][2] = animatingImageView.getTranslationX(); + animationValues[0][3] = animatingImageView.getTranslationY(); animationValues[0][4] = clipHorizontal * object.scale; animationValues[0][5] = clipTop * object.scale; animationValues[0][6] = clipBottom * object.scale; @@ -2949,19 +3276,19 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat animatingImageView.setAnimationProgress(0); backgroundDrawable.setAlpha(0); - ViewProxy.setAlpha(containerView, 0); + containerView.setAlpha(0); - final AnimatorSetProxy animatorSet = new AnimatorSetProxy(); + final AnimatorSet animatorSet = new AnimatorSet(); animatorSet.playTogether( - ObjectAnimatorProxy.ofFloat(animatingImageView, "animationProgress", 0.0f, 1.0f), - ObjectAnimatorProxy.ofInt(backgroundDrawable, "alpha", 0, 255), - ObjectAnimatorProxy.ofFloat(containerView, "alpha", 0.0f, 1.0f) + ObjectAnimator.ofFloat(animatingImageView, "animationProgress", 0.0f, 1.0f), + ObjectAnimator.ofInt(backgroundDrawable, "alpha", 0, 255), + ObjectAnimator.ofFloat(containerView, "alpha", 0.0f, 1.0f) ); animationEndRunnable = new Runnable() { @Override public void run() { - if (containerView == null) { + if (containerView == null || windowView == null) { return; } if (Build.VERSION.SDK_INT >= 18) { @@ -2992,7 +3319,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat animatorSet.setDuration(200); animatorSet.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { @@ -3009,7 +3336,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { - NotificationCenter.getInstance().setAllowedNotificationsDutingAnimation(new int[]{NotificationCenter.dialogsNeedReload, NotificationCenter.closeChats, NotificationCenter.mediaCountDidLoaded, NotificationCenter.mediaDidLoaded}); + NotificationCenter.getInstance().setAllowedNotificationsDutingAnimation(new int[]{NotificationCenter.dialogsNeedReload, NotificationCenter.closeChats, NotificationCenter.mediaCountDidLoaded, NotificationCenter.mediaDidLoaded, NotificationCenter.dialogPhotosLoaded}); NotificationCenter.getInstance().setAnimationInProgress(true); animatorSet.start(); } @@ -3034,7 +3361,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } backgroundDrawable.setAlpha(255); - ViewProxy.setAlpha(containerView, 1.0f); + containerView.setAlpha(1.0f); onPhotoShow(messageObject, fileLocation, messages, photos, index, object); } } @@ -3075,6 +3402,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat return; } + releasePlayer(); captionEditText.onDestroy(); parentChatActivity = null; NotificationCenter.getInstance().removeObserver(this, NotificationCenter.FileDidFailedLoad); @@ -3082,7 +3410,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat NotificationCenter.getInstance().removeObserver(this, NotificationCenter.FileLoadProgressChanged); NotificationCenter.getInstance().removeObserver(this, NotificationCenter.mediaCountDidLoaded); NotificationCenter.getInstance().removeObserver(this, NotificationCenter.mediaDidLoaded); - NotificationCenter.getInstance().removeObserver(this, NotificationCenter.userPhotosLoaded); + NotificationCenter.getInstance().removeObserver(this, NotificationCenter.dialogPhotosLoaded); NotificationCenter.getInstance().removeObserver(this, NotificationCenter.emojiDidLoaded); ConnectionsManager.getInstance().cancelRequestsForGuid(classGuid); @@ -3101,7 +3429,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat animatingImageView.setVisibility(View.VISIBLE); containerView.invalidate(); - AnimatorSetProxy animatorSet = new AnimatorSetProxy(); + AnimatorSet animatorSet = new AnimatorSet(); final ViewGroup.LayoutParams layoutParams = animatingImageView.getLayoutParams(); Rect drawRegion = null; @@ -3127,10 +3455,10 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat float height = layoutParams.height * scale * scale2; float xPos = (AndroidUtilities.displaySize.x - width) / 2.0f; float yPos = (AndroidUtilities.displaySize.y - AndroidUtilities.statusBarHeight - height) / 2.0f; - ViewProxy.setTranslationX(animatingImageView, xPos + translationX); - ViewProxy.setTranslationY(animatingImageView, yPos + translationY); - ViewProxy.setScaleX(animatingImageView, scale * scale2); - ViewProxy.setScaleY(animatingImageView, scale * scale2); + animatingImageView.setTranslationX(xPos + translationX); + animatingImageView.setTranslationY(yPos + translationY); + animatingImageView.setScaleX(scale * scale2); + animatingImageView.setScaleY(scale * scale2); if (object != null) { object.imageReceiver.setVisible(false, true); @@ -3151,10 +3479,10 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat clipTop = Math.max(clipTop, clipVertical); clipBottom = Math.max(clipBottom, clipVertical); - animationValues[0][0] = ViewProxy.getScaleX(animatingImageView); - animationValues[0][1] = ViewProxy.getScaleY(animatingImageView); - animationValues[0][2] = ViewProxy.getTranslationX(animatingImageView); - animationValues[0][3] = ViewProxy.getTranslationY(animatingImageView); + animationValues[0][0] = animatingImageView.getScaleX(); + animationValues[0][1] = animatingImageView.getScaleY(); + animationValues[0][2] = animatingImageView.getTranslationX(); + animationValues[0][3] = animatingImageView.getTranslationY(); animationValues[0][4] = 0; animationValues[0][5] = 0; animationValues[0][6] = 0; @@ -3170,16 +3498,16 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat animationValues[1][7] = object.radius; animatorSet.playTogether( - ObjectAnimatorProxy.ofFloat(animatingImageView, "animationProgress", 0.0f, 1.0f), - ObjectAnimatorProxy.ofInt(backgroundDrawable, "alpha", 0), - ObjectAnimatorProxy.ofFloat(containerView, "alpha", 0.0f) + ObjectAnimator.ofFloat(animatingImageView, "animationProgress", 0.0f, 1.0f), + ObjectAnimator.ofInt(backgroundDrawable, "alpha", 0), + ObjectAnimator.ofFloat(containerView, "alpha", 0.0f) ); } else { animatorSet.playTogether( - ObjectAnimatorProxy.ofInt(backgroundDrawable, "alpha", 0), - ObjectAnimatorProxy.ofFloat(animatingImageView, "alpha", 0.0f), - ObjectAnimatorProxy.ofFloat(animatingImageView, "translationY", translationY >= 0 ? AndroidUtilities.displaySize.y : -AndroidUtilities.displaySize.y), - ObjectAnimatorProxy.ofFloat(containerView, "alpha", 0.0f) + ObjectAnimator.ofInt(backgroundDrawable, "alpha", 0), + ObjectAnimator.ofFloat(animatingImageView, "alpha", 0.0f), + ObjectAnimator.ofFloat(animatingImageView, "translationY", translationY >= 0 ? AndroidUtilities.displaySize.y : -AndroidUtilities.displaySize.y), + ObjectAnimator.ofFloat(containerView, "alpha", 0.0f) ); } @@ -3197,7 +3525,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat animatorSet.setDuration(200); animatorSet.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { @@ -3215,12 +3543,12 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } animatorSet.start(); } else { - AnimatorSetProxy animatorSet = new AnimatorSetProxy(); + AnimatorSet animatorSet = new AnimatorSet(); animatorSet.playTogether( - ObjectAnimatorProxy.ofFloat(containerView, "scaleX", 0.9f), - ObjectAnimatorProxy.ofFloat(containerView, "scaleY", 0.9f), - ObjectAnimatorProxy.ofInt(backgroundDrawable, "alpha", 0), - ObjectAnimatorProxy.ofFloat(containerView, "alpha", 0.0f) + ObjectAnimator.ofFloat(containerView, "scaleX", 0.9f), + ObjectAnimator.ofFloat(containerView, "scaleY", 0.9f), + ObjectAnimator.ofInt(backgroundDrawable, "alpha", 0), + ObjectAnimator.ofFloat(containerView, "alpha", 0.0f) ); animationInProgress = 2; animationEndRunnable = new Runnable() { @@ -3234,15 +3562,14 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } animationInProgress = 0; onPhotoClosed(object); - ViewProxy.setScaleX(containerView, 1.0f); - ViewProxy.setScaleY(containerView, 1.0f); - containerView.clearAnimation(); + containerView.setScaleX(1.0f); + containerView.setScaleY(1.0f); } }; animatorSet.setDuration(200); animatorSet.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (animationEndRunnable != null) { animationEndRunnable.run(); animationEndRunnable = null; @@ -3255,12 +3582,18 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } animatorSet.start(); } + if (currentAnimation != null) { + currentAnimation.setSecondParentView(null); + currentAnimation = null; + centerImage.setImageBitmap((Drawable) null); + } } public void destroyPhotoViewer() { if (parentActivity == null || windowView == null) { return; } + releasePlayer(); try { if (windowView.getParent() != null) { WindowManager wm = (WindowManager) parentActivity.getSystemService(Context.WINDOW_SERVICE); @@ -3283,6 +3616,10 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat currentFileLocation = null; currentPathObject = null; currentThumb = null; + if (currentAnimation != null) { + currentAnimation.setSecondParentView(null); + currentAnimation = null; + } for (int a = 0; a < 3; a++) { if (radialProgressViews[a] != null) { radialProgressViews[a].setBackgroundState(-1, false); @@ -3334,6 +3671,10 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } public void onPause() { + if (currentAnimation != null) { + closePhoto(false, false); + return; + } if (captionDoneItem.getVisibility() != View.GONE) { closeCaptionEnter(true); } @@ -3677,15 +4018,15 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat animateToX = newTx; animateToY = newTy; animationStartTime = System.currentTimeMillis(); - imageMoveAnimation = new AnimatorSetProxy(); + imageMoveAnimation = new AnimatorSet(); imageMoveAnimation.playTogether( - ObjectAnimatorProxy.ofFloat(this, "animationValue", 0, 1) + ObjectAnimator.ofFloat(this, "animationValue", 0, 1) ); imageMoveAnimation.setInterpolator(interpolator); imageMoveAnimation.setDuration(duration); imageMoveAnimation.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { imageMoveAnimation = null; containerView.invalidate(); } @@ -3702,6 +4043,7 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat return animationValue; } + @SuppressLint("NewApi") private void onDraw(Canvas canvas) { if (animationInProgress == 1 || !isVisible && animationInProgress != 2) { return; @@ -3828,27 +4170,36 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat canvas.restore(); } - float tranlateX = currentTranslationX; + float translateX = currentTranslationX; float scaleDiff = 0; float alpha = 1; - if (!zoomAnimation && tranlateX > maxX && currentEditMode == 0) { - alpha = Math.min(1.0f, (tranlateX - maxX) / canvas.getWidth()); + if (!zoomAnimation && translateX > maxX && currentEditMode == 0) { + alpha = Math.min(1.0f, (translateX - maxX) / canvas.getWidth()); scaleDiff = alpha * 0.3f; alpha = 1.0f - alpha; - tranlateX = maxX; + translateX = maxX; } + boolean drawTextureView = Build.VERSION.SDK_INT >= 16 && aspectRatioFrameLayout != null && aspectRatioFrameLayout.getVisibility() == View.VISIBLE; if (centerImage.hasBitmapImage()) { canvas.save(); canvas.translate(getContainerViewWidth() / 2 + getAdditionX(), getContainerViewHeight() / 2 + getAdditionY()); - canvas.translate(tranlateX, currentTranslationY); + canvas.translate(translateX, currentTranslationY); canvas.scale(currentScale - scaleDiff, currentScale - scaleDiff); if (currentEditMode == 1) { - photoCropView.setBitmapParams(currentScale, tranlateX, currentTranslationY); + photoCropView.setBitmapParams(currentScale, translateX, currentTranslationY); } int bitmapWidth = centerImage.getBitmapWidth(); int bitmapHeight = centerImage.getBitmapHeight(); + if (drawTextureView && textureUploaded) { + float scale1 = bitmapWidth / (float) bitmapHeight; + float scale2 = videoTextureView.getMeasuredWidth() / (float) videoTextureView.getMeasuredHeight(); + if (Math.abs(scale1 - scale2) > 0.01f) { + bitmapWidth = videoTextureView.getMeasuredWidth(); + bitmapHeight = videoTextureView.getMeasuredHeight(); + } + } float scaleX = (float) getContainerViewWidth() / (float) bitmapWidth; float scaleY = (float) getContainerViewHeight() / (float) bitmapHeight; @@ -3856,17 +4207,41 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat int width = (int) (bitmapWidth * scale); int height = (int) (bitmapHeight * scale); - centerImage.setAlpha(alpha); - centerImage.setImageCoords(-width / 2, -height / 2, width, height); - centerImage.draw(canvas); + if (!drawTextureView || !textureUploaded || !videoCrossfadeStarted || videoCrossfadeAlpha != 1.0f) { + centerImage.setAlpha(alpha); + centerImage.setImageCoords(-width / 2, -height / 2, width, height); + centerImage.draw(canvas); + } + if (drawTextureView) { + if (!videoCrossfadeStarted && textureUploaded) { + videoCrossfadeStarted = true; + videoCrossfadeAlpha = 0.0f; + videoCrossfadeAlphaLastTime = System.currentTimeMillis(); + } + canvas.translate(-width / 2, -height / 2); + videoTextureView.setAlpha(alpha * videoCrossfadeAlpha); + aspectRatioFrameLayout.draw(canvas); + if (videoCrossfadeStarted && videoCrossfadeAlpha < 1.0f) { + long newUpdateTime = System.currentTimeMillis(); + long dt = newUpdateTime - videoCrossfadeAlphaLastTime; + videoCrossfadeAlphaLastTime = newUpdateTime; + videoCrossfadeAlpha += dt / 300.0f; + containerView.invalidate(); + if (videoCrossfadeAlpha > 1.0f) { + videoCrossfadeAlpha = 1.0f; + } + } + } + canvas.restore(); + } + if (!drawTextureView && (videoPlayerControlFrameLayout == null || videoPlayerControlFrameLayout.getVisibility() != View.VISIBLE)) { + canvas.save(); + canvas.translate(translateX, currentTranslationY / currentScale); + radialProgressViews[0].setScale(1.0f - scaleDiff); + radialProgressViews[0].setAlpha(alpha); + radialProgressViews[0].onDraw(canvas); canvas.restore(); } - canvas.save(); - canvas.translate(tranlateX, currentTranslationY / currentScale); - radialProgressViews[0].setScale(1.0f - scaleDiff); - radialProgressViews[0].setAlpha(alpha); - radialProgressViews[0].onDraw(canvas); - canvas.restore(); if (sideImage == leftImage) { if (sideImage.hasBitmapImage()) { @@ -3921,35 +4296,38 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat } } - private void onActionClick() { + private void onActionClick(boolean download) { if (currentMessageObject == null || currentFileNames[0] == null) { return; } - boolean loadFile = false; + File file = null; if (currentMessageObject.messageOwner.attachPath != null && currentMessageObject.messageOwner.attachPath.length() != 0) { - File f = new File(currentMessageObject.messageOwner.attachPath); - if (f.exists()) { - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setDataAndType(Uri.fromFile(f), "video/mp4"); - parentActivity.startActivityForResult(intent, 500); - } else { - loadFile = true; - } - } else { - File cacheFile = FileLoader.getPathToMessage(currentMessageObject.messageOwner); - if (cacheFile.exists()) { - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setDataAndType(Uri.fromFile(cacheFile), "video/mp4"); - parentActivity.startActivityForResult(intent, 500); - } else { - loadFile = true; + file = new File(currentMessageObject.messageOwner.attachPath); + if (!file.exists()) { + file = null; } } - if (loadFile) { - if (!FileLoader.getInstance().isLoadingFile(currentFileNames[0])) { - FileLoader.getInstance().loadFile(currentMessageObject.getDocument(), true, false); + if (file == null) { + file = FileLoader.getPathToMessage(currentMessageObject.messageOwner); + if (!file.exists()) { + file = null; + } + } + if (file == null) { + if (download) { + if (!FileLoader.getInstance().isLoadingFile(currentFileNames[0])) { + FileLoader.getInstance().loadFile(currentMessageObject.getDocument(), true, false); + } else { + FileLoader.getInstance().cancelLoadFile(currentMessageObject.getDocument()); + } + } + } else { + if (Build.VERSION.SDK_INT >= 16) { + preparePlayer(file, true); } else { - FileLoader.getInstance().cancelLoadFile(currentMessageObject.getDocument()); + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setDataAndType(Uri.fromFile(file), "video/mp4"); + parentActivity.startActivityForResult(intent, 500); } } } @@ -3995,14 +4373,15 @@ public class PhotoViewer implements NotificationCenter.NotificationCenterDelegat return false; } if (canShowBottom) { - if (radialProgressViews[0] != null && containerView != null) { + boolean drawTextureView = Build.VERSION.SDK_INT >= 16 && aspectRatioFrameLayout != null && aspectRatioFrameLayout.getVisibility() == View.VISIBLE; + if (radialProgressViews[0] != null && containerView != null && !drawTextureView) { int state = radialProgressViews[0].backgroundState; if (state > 0 && state <= 3) { float x = e.getX(); float y = e.getY(); - if (x >= (getContainerViewWidth() - AndroidUtilities.dp(64)) / 2.0f && x <= (getContainerViewWidth() + AndroidUtilities.dp(64)) / 2.0f && - y >= (getContainerViewHeight() - AndroidUtilities.dp(64)) / 2.0f && y <= (getContainerViewHeight() + AndroidUtilities.dp(64)) / 2.0f) { - onActionClick(); + if (x >= (getContainerViewWidth() - AndroidUtilities.dp(100)) / 2.0f && x <= (getContainerViewWidth() + AndroidUtilities.dp(100)) / 2.0f && + y >= (getContainerViewHeight() - AndroidUtilities.dp(100)) / 2.0f && y <= (getContainerViewHeight() + AndroidUtilities.dp(100)) / 2.0f) { + onActionClick(true); checkProgress(0, true); return true; } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/PopupNotificationActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/PopupNotificationActivity.java index 599240296..8573bfb89 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/PopupNotificationActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/PopupNotificationActivity.java @@ -58,7 +58,6 @@ import org.telegram.ui.ActionBar.ActionBarMenu; import org.telegram.ui.Components.AvatarDrawable; import org.telegram.ui.Components.BackupImageView; import org.telegram.ui.Components.ChatActivityEnterView; -import org.telegram.ui.Components.FrameLayoutFixed; import org.telegram.ui.Components.LayoutHelper; import org.telegram.ui.Components.PopupAudioView; import org.telegram.ui.Components.RecordStatusDrawable; @@ -105,7 +104,7 @@ public class PopupNotificationActivity extends Activity implements NotificationC private boolean startedMoving = false; private Runnable onAnimationEndRunnable = null; - private class FrameLayoutTouch extends FrameLayoutFixed { + private class FrameLayoutTouch extends FrameLayout { public FrameLayoutTouch(Context context) { super(context); } @@ -135,7 +134,7 @@ public class PopupNotificationActivity extends Activity implements NotificationC } } - public class FrameLayoutAnimationListener extends FrameLayoutFixed { + public class FrameLayoutAnimationListener extends FrameLayout { public FrameLayoutAnimationListener(Context context) { super(context); } @@ -360,7 +359,7 @@ public class PopupNotificationActivity extends Activity implements NotificationC View view = menu.addItemResource(2, R.layout.popup_count_layout); countText = (TextView) view.findViewById(R.id.count_text); - avatarContainer = new FrameLayoutFixed(this); + avatarContainer = new FrameLayout(this); avatarContainer.setPadding(AndroidUtilities.dp(4), 0, AndroidUtilities.dp(4), 0); actionBar.addView(avatarContainer); FrameLayout.LayoutParams layoutParams2 = (FrameLayout.LayoutParams) avatarContainer.getLayoutParams(); @@ -844,29 +843,31 @@ public class PopupNotificationActivity extends Activity implements NotificationC if (rightView != null) { int offset = ((FrameLayout.LayoutParams) rightView.getLayoutParams()).leftMargin; reuseView(rightView); - rightView = getViewForMessage(currentMessageNum + 1, false); - int widht = AndroidUtilities.displaySize.x - AndroidUtilities.dp(24); - FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) rightView.getLayoutParams(); - layoutParams.gravity = Gravity.TOP | Gravity.LEFT; - layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT; - layoutParams.width = widht; - layoutParams.leftMargin = offset; - rightView.setLayoutParams(layoutParams); - rightView.invalidate(); + if ((rightView = getViewForMessage(currentMessageNum + 1, false)) != null) { + int widht = AndroidUtilities.displaySize.x - AndroidUtilities.dp(24); + FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) rightView.getLayoutParams(); + layoutParams.gravity = Gravity.TOP | Gravity.LEFT; + layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT; + layoutParams.width = widht; + layoutParams.leftMargin = offset; + rightView.setLayoutParams(layoutParams); + rightView.invalidate(); + } } } else if (move == 4) { if (leftView != null) { int offset = ((FrameLayout.LayoutParams) leftView.getLayoutParams()).leftMargin; reuseView(leftView); - leftView = getViewForMessage(0, false); - int widht = AndroidUtilities.displaySize.x - AndroidUtilities.dp(24); - FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) leftView.getLayoutParams(); - layoutParams.gravity = Gravity.TOP | Gravity.LEFT; - layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT; - layoutParams.width = widht; - layoutParams.leftMargin = offset; - leftView.setLayoutParams(layoutParams); - leftView.invalidate(); + if ((leftView = getViewForMessage(0, false)) != null) { + int widht = AndroidUtilities.displaySize.x - AndroidUtilities.dp(24); + FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) leftView.getLayoutParams(); + layoutParams.gravity = Gravity.TOP | Gravity.LEFT; + layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT; + layoutParams.width = widht; + layoutParams.leftMargin = offset; + leftView.setLayoutParams(layoutParams); + leftView.invalidate(); + } } } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/PrivacyUsersActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/PrivacyUsersActivity.java index 2dd66ce53..9c7412527 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/PrivacyUsersActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/PrivacyUsersActivity.java @@ -11,7 +11,6 @@ package org.telegram.ui; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; -import android.os.Build; import android.os.Bundle; import android.view.Gravity; import android.view.MotionEvent; @@ -155,9 +154,7 @@ public class PrivacyUsersActivity extends BaseFragment implements NotificationCe listView.setDivider(null); listView.setDividerHeight(0); listView.setAdapter(listViewAdapter = new ListAdapter(context)); - if (Build.VERSION.SDK_INT >= 11) { - listView.setVerticalScrollbarPosition(LocaleController.isRTL ? ListView.SCROLLBAR_POSITION_LEFT : ListView.SCROLLBAR_POSITION_RIGHT); - } + listView.setVerticalScrollbarPosition(LocaleController.isRTL ? ListView.SCROLLBAR_POSITION_LEFT : ListView.SCROLLBAR_POSITION_RIGHT); frameLayout.addView(listView); layoutParams = (FrameLayout.LayoutParams) listView.getLayoutParams(); layoutParams.width = LayoutHelper.MATCH_PARENT; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ProfileActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/ProfileActivity.java index 605d9e9c8..da09dbca9 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ProfileActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ProfileActivity.java @@ -8,6 +8,8 @@ package org.telegram.ui; +import android.animation.Animator; +import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.StateListAnimator; import android.annotation.SuppressLint; @@ -40,9 +42,7 @@ import android.widget.ImageView; import org.telegram.messenger.AndroidUtilities; import org.telegram.PhoneFormat.PhoneFormat; -import org.telegram.messenger.AnimationCompat.AnimatorListenerAdapterProxy; -import org.telegram.messenger.AnimationCompat.AnimatorSetProxy; -import org.telegram.messenger.AnimationCompat.ObjectAnimatorProxy; +import org.telegram.messenger.AnimatorListenerAdapterProxy; import org.telegram.messenger.ChatObject; import org.telegram.messenger.LocaleController; import org.telegram.messenger.MessagesStorage; @@ -67,7 +67,6 @@ import org.telegram.messenger.MessageObject; import org.telegram.messenger.UserConfig; import org.telegram.messenger.Utilities; import org.telegram.ui.ActionBar.BackDrawable; -import org.telegram.messenger.AnimationCompat.ViewProxy; import org.telegram.ui.ActionBar.SimpleTextView; import org.telegram.ui.Cells.AboutLinkCell; import org.telegram.ui.Cells.DividerCell; @@ -106,7 +105,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. private SimpleTextView nameTextView[] = new SimpleTextView[2]; private SimpleTextView onlineTextView[] = new SimpleTextView[2]; private ImageView writeButton; - private AnimatorSetProxy writeButtonAnimation; + private AnimatorSet writeButtonAnimation; private AvatarDrawable avatarDrawable; private ActionBarMenuItem animatingItem; private TopView topView; @@ -398,7 +397,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. MessagesController.getInstance().blockUser(user_id); } else { MessagesController.getInstance().unblockUser(user_id); - SendMessagesHelper.getInstance().sendMessage("/start", user_id, null, null, false, false, null, null, null); + SendMessagesHelper.getInstance().sendMessage("/start", user_id, null, null, false, null, null, null); finishFragment(); } } @@ -558,6 +557,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. return false; } }; + listView.setTag(6); listView.setPadding(0, AndroidUtilities.dp(88), 0, 0); listView.setBackgroundColor(0xffffffff); listView.setVerticalScrollBarEnabled(false); @@ -794,8 +794,8 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. avatarImage = new BackupImageView(context); avatarImage.setRoundRadius(AndroidUtilities.dp(21)); - ViewProxy.setPivotX(avatarImage, 0); - ViewProxy.setPivotY(avatarImage, 0); + avatarImage.setPivotX(0); + avatarImage.setPivotY(0); frameLayout.addView(avatarImage, LayoutHelper.createFrame(42, 42, Gravity.TOP | Gravity.LEFT, 64, 0, 0, 0)); avatarImage.setOnClickListener(new View.OnClickListener() { @Override @@ -827,8 +827,8 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. nameTextView[a].setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); nameTextView[a].setLeftDrawableTopPadding(-AndroidUtilities.dp(1.3f)); nameTextView[a].setRightDrawableTopPadding(-AndroidUtilities.dp(1.3f)); - ViewProxy.setPivotX(nameTextView[a], 0); - ViewProxy.setPivotY(nameTextView[a], 0); + nameTextView[a].setPivotX(0); + nameTextView[a].setPivotY(0); frameLayout.addView(nameTextView[a], LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.LEFT | Gravity.TOP, 118, 0, a == 0 ? 48 : 0, 0)); onlineTextView[a] = new SimpleTextView(context); @@ -965,14 +965,9 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. public void onClick(DialogInterface dialogInterface, int i) { if (i == 0) { try { - if (Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.HONEYCOMB) { - android.text.ClipboardManager clipboard = (android.text.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); - clipboard.setText("@" + user.username); - } else { - android.content.ClipboardManager clipboard = (android.content.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); - android.content.ClipData clip = android.content.ClipData.newPlainText("label", "@" + user.username); - clipboard.setPrimaryClip(clip); - } + android.content.ClipboardManager clipboard = (android.content.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); + android.content.ClipData clip = android.content.ClipData.newPlainText("label", "@" + user.username); + clipboard.setPrimaryClip(clip); } catch (Exception e) { FileLog.e("tmessages", e); } @@ -1001,14 +996,9 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. } } else if (i == 1) { try { - if (Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.HONEYCOMB) { - android.text.ClipboardManager clipboard = (android.text.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); - clipboard.setText("+" + user.phone); - } else { - android.content.ClipboardManager clipboard = (android.content.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); - android.content.ClipData clip = android.content.ClipData.newPlainText("label", "+" + user.phone); - clipboard.setPrimaryClip(clip); - } + android.content.ClipboardManager clipboard = (android.content.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); + android.content.ClipData clip = android.content.ClipData.newPlainText("label", "+" + user.phone); + clipboard.setPrimaryClip(clip); } catch (Exception e) { FileLog.e("tmessages", e); } @@ -1032,14 +1022,9 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. if (about == null) { return; } - if (Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.HONEYCOMB) { - android.text.ClipboardManager clipboard = (android.text.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); - clipboard.setText(about); - } else { - android.content.ClipboardManager clipboard = (android.content.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); - android.content.ClipData clip = android.content.ClipData.newPlainText("label", about); - clipboard.setPrimaryClip(clip); - } + android.content.ClipboardManager clipboard = (android.content.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); + android.content.ClipData clip = android.content.ClipData.newPlainText("label", about); + clipboard.setPrimaryClip(clip); } catch (Exception e) { FileLog.e("tmessages", e); } @@ -1100,7 +1085,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. return; } loadingUsers = true; - final int delay = Build.VERSION.SDK_INT >= 11 && !participantsMap.isEmpty() && reload ? 300 : 0; + final int delay = !participantsMap.isEmpty() && reload ? 300 : 0; final TLRPC.TL_channels_getParticipants req = new TLRPC.TL_channels_getParticipants(); req.channel = MessagesController.getInputChannel(chat_id); @@ -1218,13 +1203,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. listView.setTopGlowOffset(extraHeight); if (writeButton != null) { - if (Build.VERSION.SDK_INT < 11) { - layoutParams = (FrameLayout.LayoutParams) writeButton.getLayoutParams(); - layoutParams.topMargin = (actionBar.getOccupyStatusBar() ? AndroidUtilities.statusBarHeight : 0) + ActionBar.getCurrentActionBarHeight() + extraHeight - AndroidUtilities.dp(29.5f); - writeButton.setLayoutParams(layoutParams); - } else { - ViewProxy.setTranslationY(writeButton, (actionBar.getOccupyStatusBar() ? AndroidUtilities.statusBarHeight : 0) + ActionBar.getCurrentActionBarHeight() + extraHeight - AndroidUtilities.dp(29.5f)); - } + writeButton.setTranslationY((actionBar.getOccupyStatusBar() ? AndroidUtilities.statusBarHeight : 0) + ActionBar.getCurrentActionBarHeight() + extraHeight - AndroidUtilities.dp(29.5f)); if (!openAnimationInProgress) { final boolean setVisible = diff > 0.2f; @@ -1232,42 +1211,35 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. if (setVisible != currentVisible) { if (setVisible) { writeButton.setTag(null); - if (Build.VERSION.SDK_INT < 11) { - writeButton.setVisibility(View.VISIBLE); - } } else { writeButton.setTag(0); } if (writeButtonAnimation != null) { - AnimatorSetProxy old = writeButtonAnimation; + AnimatorSet old = writeButtonAnimation; writeButtonAnimation = null; old.cancel(); } - writeButtonAnimation = new AnimatorSetProxy(); + writeButtonAnimation = new AnimatorSet(); if (setVisible) { writeButtonAnimation.setInterpolator(new DecelerateInterpolator()); writeButtonAnimation.playTogether( - ObjectAnimatorProxy.ofFloat(writeButton, "scaleX", 1.0f), - ObjectAnimatorProxy.ofFloat(writeButton, "scaleY", 1.0f), - ObjectAnimatorProxy.ofFloat(writeButton, "alpha", 1.0f) + ObjectAnimator.ofFloat(writeButton, "scaleX", 1.0f), + ObjectAnimator.ofFloat(writeButton, "scaleY", 1.0f), + ObjectAnimator.ofFloat(writeButton, "alpha", 1.0f) ); } else { writeButtonAnimation.setInterpolator(new AccelerateInterpolator()); writeButtonAnimation.playTogether( - ObjectAnimatorProxy.ofFloat(writeButton, "scaleX", 0.2f), - ObjectAnimatorProxy.ofFloat(writeButton, "scaleY", 0.2f), - ObjectAnimatorProxy.ofFloat(writeButton, "alpha", 0.0f) + ObjectAnimator.ofFloat(writeButton, "scaleX", 0.2f), + ObjectAnimator.ofFloat(writeButton, "scaleY", 0.2f), + ObjectAnimator.ofFloat(writeButton, "alpha", 0.0f) ); } writeButtonAnimation.setDuration(150); writeButtonAnimation.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (writeButtonAnimation != null && writeButtonAnimation.equals(animation)) { - writeButton.clearAnimation(); - if (Build.VERSION.SDK_INT < 11) { - writeButton.setVisibility(setVisible ? View.VISIBLE : View.GONE); - } writeButtonAnimation = null; } } @@ -1278,29 +1250,20 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. } float avatarY = (actionBar.getOccupyStatusBar() ? AndroidUtilities.statusBarHeight : 0) + ActionBar.getCurrentActionBarHeight() / 2.0f * (1.0f + diff) - 21 * AndroidUtilities.density + 27 * AndroidUtilities.density * diff; - if (Build.VERSION.SDK_INT < 11) { - layoutParams = (FrameLayout.LayoutParams) avatarImage.getLayoutParams(); - layoutParams.height = layoutParams.width = (int) Math.ceil(AndroidUtilities.dp(42) * (42 + 18 * diff) / 42.0f); - layoutParams.leftMargin = (int) Math.ceil(AndroidUtilities.dp(64) - AndroidUtilities.dp(47) * diff); - layoutParams.topMargin = (int) Math.ceil(avatarY); - avatarImage.setLayoutParams(layoutParams); - avatarImage.setRoundRadius(layoutParams.height / 2); - } else { - ViewProxy.setScaleX(avatarImage, (42 + 18 * diff) / 42.0f); - ViewProxy.setScaleY(avatarImage, (42 + 18 * diff) / 42.0f); - ViewProxy.setTranslationX(avatarImage, -AndroidUtilities.dp(47) * diff); - ViewProxy.setTranslationY(avatarImage, (float) Math.ceil(avatarY)); - } + avatarImage.setScaleX((42 + 18 * diff) / 42.0f); + avatarImage.setScaleY((42 + 18 * diff) / 42.0f); + avatarImage.setTranslationX(-AndroidUtilities.dp(47) * diff); + avatarImage.setTranslationY((float) Math.ceil(avatarY)); for (int a = 0; a < 2; a++) { if (nameTextView[a] == null) { continue; } - ViewProxy.setTranslationX(nameTextView[a], -21 * AndroidUtilities.density * diff); - ViewProxy.setTranslationY(nameTextView[a], (float) Math.floor(avatarY) + AndroidUtilities.dp(1.3f) + AndroidUtilities.dp(7) * diff); - ViewProxy.setTranslationX(onlineTextView[a], -21 * AndroidUtilities.density * diff); - ViewProxy.setTranslationY(onlineTextView[a], (float) Math.floor(avatarY) + AndroidUtilities.dp(24) + (float) Math.floor(11 * AndroidUtilities.density) * diff); - ViewProxy.setScaleX(nameTextView[a], 1.0f + 0.12f * diff); - ViewProxy.setScaleY(nameTextView[a], 1.0f + 0.12f * diff); + nameTextView[a].setTranslationX(-21 * AndroidUtilities.density * diff); + nameTextView[a].setTranslationY((float) Math.floor(avatarY) + AndroidUtilities.dp(1.3f) + AndroidUtilities.dp(7) * diff); + onlineTextView[a].setTranslationX(-21 * AndroidUtilities.density * diff); + onlineTextView[a].setTranslationY((float) Math.floor(avatarY) + AndroidUtilities.dp(24) + (float) Math.floor(11 * AndroidUtilities.density) * diff); + nameTextView[a].setScaleX(1.0f + 0.12f * diff); + nameTextView[a].setScaleY(1.0f + 0.12f * diff); if (a == 1 && !openAnimationInProgress) { int width; if (AndroidUtilities.isTablet()) { @@ -1308,18 +1271,18 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. } else { width = AndroidUtilities.displaySize.x; } - width = (int) (width - AndroidUtilities.dp(118 + 8 + 40 * (1.0f - diff)) - ViewProxy.getTranslationX(nameTextView[a])); - float width2 = nameTextView[a].getPaint().measureText(nameTextView[a].getText().toString()) * ViewProxy.getScaleX(nameTextView[a]) + nameTextView[a].getSideDrawablesSize(); + width = (int) (width - AndroidUtilities.dp(118 + 8 + 40 * (1.0f - diff)) - nameTextView[a].getTranslationX()); + float width2 = nameTextView[a].getPaint().measureText(nameTextView[a].getText().toString()) * nameTextView[a].getScaleX() + nameTextView[a].getSideDrawablesSize(); layoutParams = (FrameLayout.LayoutParams) nameTextView[a].getLayoutParams(); if (width < width2) { - layoutParams.width = (int) Math.ceil(width / ViewProxy.getScaleX(nameTextView[a])); + layoutParams.width = (int) Math.ceil(width / nameTextView[a].getScaleX()); } else { layoutParams.width = LayoutHelper.WRAP_CONTENT; } nameTextView[a].setLayoutParams(layoutParams); layoutParams = (FrameLayout.LayoutParams) onlineTextView[a].getLayoutParams(); - layoutParams.rightMargin = (int) Math.ceil(ViewProxy.getTranslationX(onlineTextView[a]) + AndroidUtilities.dp(8) + AndroidUtilities.dp(40) * (1.0f - diff)); + layoutParams.rightMargin = (int) Math.ceil(onlineTextView[a].getTranslationX() + AndroidUtilities.dp(8) + AndroidUtilities.dp(40) * (1.0f - diff)); onlineTextView[a].setLayoutParams(layoutParams); } } @@ -1537,7 +1500,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. public void setPlayProfileAnimation(boolean value) { SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE); - if (!AndroidUtilities.isTablet() && Build.VERSION.SDK_INT > 10 && preferences.getBoolean("view_animations", true)) { + if (!AndroidUtilities.isTablet() && preferences.getBoolean("view_animations", true)) { playProfileAnimation = value; } } @@ -1565,9 +1528,9 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. public void setAnimationProgress(float progress) { animationProgress = progress; - ViewProxy.setAlpha(listView, progress); + listView.setAlpha(progress); - ViewProxy.setTranslationX(listView, AndroidUtilities.dp(48) - AndroidUtilities.dp(48) * progress); + listView.setTranslationX(AndroidUtilities.dp(48) - AndroidUtilities.dp(48) * progress); int color = AvatarDrawable.getProfileBackColorForId(user_id != 0 || ChatObject.isChannel(chat_id) && !currentChat.megagroup ? 5 : chat_id); int r = Color.red(Theme.ACTION_BAR_COLOR); @@ -1608,9 +1571,9 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. } @Override - protected AnimatorSetProxy onCustomTransitionAnimation(final boolean isOpen, final Runnable callback) { + protected AnimatorSet onCustomTransitionAnimation(final boolean isOpen, final Runnable callback) { if (playProfileAnimation && allowProfileAnimation) { - final AnimatorSetProxy animatorSet = new AnimatorSetProxy(); + final AnimatorSet animatorSet = new AnimatorSet(); animatorSet.setDuration(180); if (Build.VERSION.SDK_INT > 15) { listView.setLayerType(View.LAYER_TYPE_HARDWARE, null); @@ -1639,49 +1602,49 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. initialAnimationExtraHeight = AndroidUtilities.dp(88); fragmentView.setBackgroundColor(0); setAnimationProgress(0); - ArrayList animators = new ArrayList<>(); - animators.add(ObjectAnimatorProxy.ofFloat(this, "animationProgress", 0.0f, 1.0f)); + ArrayList animators = new ArrayList<>(); + animators.add(ObjectAnimator.ofFloat(this, "animationProgress", 0.0f, 1.0f)); if (writeButton != null) { - ViewProxy.setScaleX(writeButton, 0.2f); - ViewProxy.setScaleY(writeButton, 0.2f); - ViewProxy.setAlpha(writeButton, 0.0f); - animators.add(ObjectAnimatorProxy.ofFloat(writeButton, "scaleX", 1.0f)); - animators.add(ObjectAnimatorProxy.ofFloat(writeButton, "scaleY", 1.0f)); - animators.add(ObjectAnimatorProxy.ofFloat(writeButton, "alpha", 1.0f)); + writeButton.setScaleX(0.2f); + writeButton.setScaleY(0.2f); + writeButton.setAlpha(0.0f); + animators.add(ObjectAnimator.ofFloat(writeButton, "scaleX", 1.0f)); + animators.add(ObjectAnimator.ofFloat(writeButton, "scaleY", 1.0f)); + animators.add(ObjectAnimator.ofFloat(writeButton, "alpha", 1.0f)); } for (int a = 0; a < 2; a++) { - ViewProxy.setAlpha(onlineTextView[a], a == 0 ? 1.0f : 0.0f); - ViewProxy.setAlpha(nameTextView[a], a == 0 ? 1.0f : 0.0f); - animators.add(ObjectAnimatorProxy.ofFloat(onlineTextView[a], "alpha", a == 0 ? 0.0f : 1.0f)); - animators.add(ObjectAnimatorProxy.ofFloat(nameTextView[a], "alpha", a == 0 ? 0.0f : 1.0f)); + onlineTextView[a].setAlpha(a == 0 ? 1.0f : 0.0f); + nameTextView[a].setAlpha(a == 0 ? 1.0f : 0.0f); + animators.add(ObjectAnimator.ofFloat(onlineTextView[a], "alpha", a == 0 ? 0.0f : 1.0f)); + animators.add(ObjectAnimator.ofFloat(nameTextView[a], "alpha", a == 0 ? 0.0f : 1.0f)); } if (animatingItem != null) { - ViewProxy.setAlpha(animatingItem, 1.0f); - animators.add(ObjectAnimatorProxy.ofFloat(animatingItem, "alpha", 0.0f)); + animatingItem.setAlpha(1.0f); + animators.add(ObjectAnimator.ofFloat(animatingItem, "alpha", 0.0f)); } animatorSet.playTogether(animators); } else { initialAnimationExtraHeight = extraHeight; - ArrayList animators = new ArrayList<>(); - animators.add(ObjectAnimatorProxy.ofFloat(this, "animationProgress", 1.0f, 0.0f)); + ArrayList animators = new ArrayList<>(); + animators.add(ObjectAnimator.ofFloat(this, "animationProgress", 1.0f, 0.0f)); if (writeButton != null) { - animators.add(ObjectAnimatorProxy.ofFloat(writeButton, "scaleX", 0.2f)); - animators.add(ObjectAnimatorProxy.ofFloat(writeButton, "scaleY", 0.2f)); - animators.add(ObjectAnimatorProxy.ofFloat(writeButton, "alpha", 0.0f)); + animators.add(ObjectAnimator.ofFloat(writeButton, "scaleX", 0.2f)); + animators.add(ObjectAnimator.ofFloat(writeButton, "scaleY", 0.2f)); + animators.add(ObjectAnimator.ofFloat(writeButton, "alpha", 0.0f)); } for (int a = 0; a < 2; a++) { - animators.add(ObjectAnimatorProxy.ofFloat(onlineTextView[a], "alpha", a == 0 ? 1.0f : 0.0f)); - animators.add(ObjectAnimatorProxy.ofFloat(nameTextView[a], "alpha", a == 0 ? 1.0f : 0.0f)); + animators.add(ObjectAnimator.ofFloat(onlineTextView[a], "alpha", a == 0 ? 1.0f : 0.0f)); + animators.add(ObjectAnimator.ofFloat(nameTextView[a], "alpha", a == 0 ? 1.0f : 0.0f)); } if (animatingItem != null) { - ViewProxy.setAlpha(animatingItem, 0.0f); - animators.add(ObjectAnimatorProxy.ofFloat(animatingItem, "alpha", 1.0f)); + animatingItem.setAlpha(0.0f); + animators.add(ObjectAnimator.ofFloat(animatingItem, "alpha", 1.0f)); } animatorSet.playTogether(animators); } animatorSet.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (Build.VERSION.SDK_INT > 15) { listView.setLayerType(View.LAYER_TYPE_NONE, null); } @@ -1739,11 +1702,15 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. object.viewY = coords[1] - AndroidUtilities.statusBarHeight; object.parentView = avatarImage; object.imageReceiver = avatarImage.getImageReceiver(); - object.user_id = user_id; + if (user_id != 0) { + object.dialogId = user_id; + } else if (chat_id != 0) { + object.dialogId = -chat_id; + } object.thumb = object.imageReceiver.getBitmap(); object.size = -1; object.radius = avatarImage.getImageReceiver().getRoundRadius(); - object.scale = ViewProxy.getScaleX(avatarImage); + object.scale = avatarImage.getScaleX(); return object; } return null; @@ -2302,7 +2269,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. presentFragment(new ChatActivity(args), true); removeSelfFromStack(); TLRPC.User user = MessagesController.getInstance().getUser(user_id); - SendMessagesHelper.getInstance().sendMessage(user, dialog_id, null, true, null, null); + SendMessagesHelper.getInstance().sendMessage(user, dialog_id, null, null, null); } } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ProfileNotificationsActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/ProfileNotificationsActivity.java index a1cd46f5e..6331dc07f 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ProfileNotificationsActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ProfileNotificationsActivity.java @@ -181,7 +181,7 @@ public class ProfileNotificationsActivity extends BaseFragment implements Notifi } MessagesStorage.getInstance().setDialogFlags(dialog_id, which == 2 ? 1 : 0); editor.commit(); - TLRPC.Dialog dialog = MessagesController.getInstance().dialogs_dict.get(dialog_id); + TLRPC.TL_dialog dialog = MessagesController.getInstance().dialogs_dict.get(dialog_id); if (dialog != null) { dialog.notify_settings = new TLRPC.TL_peerNotifySettings(); if (which == 2) { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/SecretPhotoViewer.java b/TMessagesProj/src/main/java/org/telegram/ui/SecretPhotoViewer.java index b7dbcd7cd..3125447a1 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/SecretPhotoViewer.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/SecretPhotoViewer.java @@ -203,7 +203,7 @@ public class SecretPhotoViewer implements NotificationCenter.NotificationCenterD windowView.setFocusable(true); windowView.setFocusableInTouchMode(true); if (Build.VERSION.SDK_INT >= 23) { - windowView.setFitsSystemWindows(true); //TODO ? + windowView.setFitsSystemWindows(true); } containerView = new FrameLayoutDrawer(activity); @@ -263,7 +263,7 @@ public class SecretPhotoViewer implements NotificationCenter.NotificationCenterD File file = FileLoader.getPathToAttach(sizeFull); Bitmap bitmap = null; BitmapFactory.Options options = null; - if (Build.VERSION.SDK_INT >= 14 && Build.VERSION.SDK_INT < 21) { + if (Build.VERSION.SDK_INT < 21) { options = new BitmapFactory.Options(); options.inDither = true; options.inPreferredConfig = Bitmap.Config.ARGB_8888; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/SettingsActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/SettingsActivity.java index ea53eb90d..9bd62fcf6 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/SettingsActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/SettingsActivity.java @@ -8,6 +8,8 @@ package org.telegram.ui; +import android.animation.Animator; +import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.StateListAnimator; import android.annotation.SuppressLint; @@ -49,12 +51,12 @@ import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ListView; import android.widget.TextView; +import android.widget.Toast; import org.telegram.messenger.AndroidUtilities; -import org.telegram.messenger.AnimationCompat.AnimatorListenerAdapterProxy; -import org.telegram.messenger.AnimationCompat.AnimatorSetProxy; -import org.telegram.messenger.AnimationCompat.ObjectAnimatorProxy; +import org.telegram.messenger.AnimatorListenerAdapterProxy; import org.telegram.PhoneFormat.PhoneFormat; +import org.telegram.messenger.ContactsController; import org.telegram.messenger.MediaController; import org.telegram.messenger.UserObject; import org.telegram.messenger.ApplicationLoader; @@ -76,7 +78,6 @@ import org.telegram.messenger.UserConfig; import org.telegram.messenger.MessageObject; import org.telegram.ui.ActionBar.BottomSheet; import org.telegram.ui.Adapters.BaseFragmentAdapter; -import org.telegram.messenger.AnimationCompat.ViewProxy; import org.telegram.ui.Cells.CheckBoxCell; import org.telegram.ui.Cells.TextInfoCell; import org.telegram.ui.Cells.EmptyCell; @@ -108,7 +109,7 @@ public class SettingsActivity extends BaseFragment implements NotificationCenter private TextView nameTextView; private TextView onlineTextView; private ImageView writeButton; - private AnimatorSetProxy writeButtonAnimation; + private AnimatorSet writeButtonAnimation; private AvatarUpdater avatarUpdater = new AvatarUpdater(); private View extraHeightView; private View shadowView; @@ -249,9 +250,7 @@ public class SettingsActivity extends BaseFragment implements NotificationCenter mobileDownloadRow = rowCount++; wifiDownloadRow = rowCount++; roamingDownloadRow = rowCount++; - if (Build.VERSION.SDK_INT >= 11) { - autoplayGifsRow = rowCount++; - } + autoplayGifsRow = rowCount++; saveToGalleryRow = rowCount++; messagesSectionRow = rowCount++; messagesSectionRow2 = rowCount++; @@ -561,12 +560,8 @@ public class SettingsActivity extends BaseFragment implements NotificationCenter maskValues[a] = (mask & MediaController.AUTODOWNLOAD_MASK_MUSIC) != 0; name = LocaleController.getString("AttachMusic", R.string.AttachMusic); } else if (a == 5) { - if (Build.VERSION.SDK_INT >= 11) { - maskValues[a] = (mask & MediaController.AUTODOWNLOAD_MASK_GIF) != 0; - name = LocaleController.getString("AttachGif", R.string.AttachGif); - } else { - continue; - } + maskValues[a] = (mask & MediaController.AUTODOWNLOAD_MASK_GIF) != 0; + name = LocaleController.getString("AttachGif", R.string.AttachGif); } CheckBoxCell checkBoxCell = new CheckBoxCell(getParentActivity()); checkBoxCell.setTag(a); @@ -647,10 +642,49 @@ public class SettingsActivity extends BaseFragment implements NotificationCenter } }); + listView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { + + private int pressCount = 0; + + @Override + public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { + if (position == versionRow) { + pressCount++; + if (pressCount >= 2) { + AlertDialog.Builder builder = new AlertDialog.Builder(getParentActivity()); + builder.setTitle("Debug Menu"); + builder.setItems(new CharSequence[]{ + "Import Contacts", + "Reload Contacts" + }, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + if (which == 0) { + ContactsController.getInstance().forceImportContacts(); + } else if (which == 1) { + ContactsController.getInstance().loadContacts(false, true); + } + } + }); + builder.setNegativeButton(LocaleController.getString("Cancel", R.string.Cancel), null); + showDialog(builder.create()); + } else { + try { + Toast.makeText(getParentActivity(), "¯\\_(ツ)_/¯", Toast.LENGTH_SHORT).show(); + } catch (Exception e) { + FileLog.e("tmessages", e); + } + } + return true; + } + return false; + } + }); + frameLayout.addView(actionBar); extraHeightView = new View(context); - ViewProxy.setPivotY(extraHeightView, 0); + extraHeightView.setPivotY(0); extraHeightView.setBackgroundColor(AvatarDrawable.getProfileBackColorForId(5)); frameLayout.addView(extraHeightView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 88)); @@ -660,8 +694,8 @@ public class SettingsActivity extends BaseFragment implements NotificationCenter avatarImage = new BackupImageView(context); avatarImage.setRoundRadius(AndroidUtilities.dp(21)); - ViewProxy.setPivotX(avatarImage, 0); - ViewProxy.setPivotY(avatarImage, 0); + avatarImage.setPivotX(0); + avatarImage.setPivotY(0); frameLayout.addView(avatarImage, LayoutHelper.createFrame(42, 42, Gravity.TOP | Gravity.LEFT, 64, 0, 0, 0)); avatarImage.setOnClickListener(new View.OnClickListener() { @Override @@ -683,8 +717,8 @@ public class SettingsActivity extends BaseFragment implements NotificationCenter nameTextView.setEllipsize(TextUtils.TruncateAt.END); nameTextView.setGravity(Gravity.LEFT); nameTextView.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")); - ViewProxy.setPivotX(nameTextView, 0); - ViewProxy.setPivotY(nameTextView, 0); + nameTextView.setPivotX(0); + nameTextView.setPivotY(0); frameLayout.addView(nameTextView, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.LEFT | Gravity.TOP, 118, 0, 48, 0)); onlineTextView = new TextView(context); @@ -813,11 +847,11 @@ public class SettingsActivity extends BaseFragment implements NotificationCenter object.viewY = coords[1] - AndroidUtilities.statusBarHeight; object.parentView = avatarImage; object.imageReceiver = avatarImage.getImageReceiver(); - object.user_id = UserConfig.getClientUserId(); + object.dialogId = UserConfig.getClientUserId(); object.thumb = object.imageReceiver.getBitmap(); object.size = -1; object.radius = avatarImage.getImageReceiver().getRoundRadius(); - object.scale = ViewProxy.getScaleX(avatarImage); + object.scale = avatarImage.getScaleX(); return object; } } @@ -861,7 +895,7 @@ public class SettingsActivity extends BaseFragment implements NotificationCenter return 0; } - public void performAskAQuestion() { + private void performAskAQuestion() { final SharedPreferences preferences = ApplicationLoader.applicationContext.getSharedPreferences("mainconfig", Activity.MODE_PRIVATE); int uid = preferences.getInt("support_id", 0); TLRPC.User supportUser = null; @@ -999,22 +1033,17 @@ public class SettingsActivity extends BaseFragment implements NotificationCenter if (layoutParams.topMargin != newTop) { layoutParams.topMargin = newTop; listView.setLayoutParams(layoutParams); - ViewProxy.setTranslationY(extraHeightView, newTop); + extraHeightView.setTranslationY(newTop); } } if (avatarImage != null) { float diff = extraHeight / (float) AndroidUtilities.dp(88); - ViewProxy.setScaleY(extraHeightView, diff); - ViewProxy.setTranslationY(shadowView, newTop + extraHeight); + extraHeightView.setScaleY(diff); + shadowView.setTranslationY(newTop + extraHeight); - if (Build.VERSION.SDK_INT < 11) { - layoutParams = (FrameLayout.LayoutParams) writeButton.getLayoutParams(); - layoutParams.topMargin = (actionBar.getOccupyStatusBar() ? AndroidUtilities.statusBarHeight : 0) + ActionBar.getCurrentActionBarHeight() + extraHeight - AndroidUtilities.dp(29.5f); - writeButton.setLayoutParams(layoutParams); - } else { - ViewProxy.setTranslationY(writeButton, (actionBar.getOccupyStatusBar() ? AndroidUtilities.statusBarHeight : 0) + ActionBar.getCurrentActionBarHeight() + extraHeight - AndroidUtilities.dp(29.5f)); - } + + writeButton.setTranslationY((actionBar.getOccupyStatusBar() ? AndroidUtilities.statusBarHeight : 0) + ActionBar.getCurrentActionBarHeight() + extraHeight - AndroidUtilities.dp(29.5f)); final boolean setVisible = diff > 0.2f; boolean currentVisible = writeButton.getTag() == null; @@ -1026,32 +1055,31 @@ public class SettingsActivity extends BaseFragment implements NotificationCenter writeButton.setTag(0); } if (writeButtonAnimation != null) { - AnimatorSetProxy old = writeButtonAnimation; + AnimatorSet old = writeButtonAnimation; writeButtonAnimation = null; old.cancel(); } - writeButtonAnimation = new AnimatorSetProxy(); + writeButtonAnimation = new AnimatorSet(); if (setVisible) { writeButtonAnimation.setInterpolator(new DecelerateInterpolator()); writeButtonAnimation.playTogether( - ObjectAnimatorProxy.ofFloat(writeButton, "scaleX", 1.0f), - ObjectAnimatorProxy.ofFloat(writeButton, "scaleY", 1.0f), - ObjectAnimatorProxy.ofFloat(writeButton, "alpha", 1.0f) + ObjectAnimator.ofFloat(writeButton, "scaleX", 1.0f), + ObjectAnimator.ofFloat(writeButton, "scaleY", 1.0f), + ObjectAnimator.ofFloat(writeButton, "alpha", 1.0f) ); } else { writeButtonAnimation.setInterpolator(new AccelerateInterpolator()); writeButtonAnimation.playTogether( - ObjectAnimatorProxy.ofFloat(writeButton, "scaleX", 0.2f), - ObjectAnimatorProxy.ofFloat(writeButton, "scaleY", 0.2f), - ObjectAnimatorProxy.ofFloat(writeButton, "alpha", 0.0f) + ObjectAnimator.ofFloat(writeButton, "scaleX", 0.2f), + ObjectAnimator.ofFloat(writeButton, "scaleY", 0.2f), + ObjectAnimator.ofFloat(writeButton, "alpha", 0.0f) ); } writeButtonAnimation.setDuration(150); writeButtonAnimation.addListener(new AnimatorListenerAdapterProxy() { @Override - public void onAnimationEnd(Object animation) { + public void onAnimationEnd(Animator animation) { if (writeButtonAnimation != null && writeButtonAnimation.equals(animation)) { - writeButton.clearAnimation(); writeButton.setVisibility(setVisible ? View.VISIBLE : View.GONE); writeButtonAnimation = null; } @@ -1060,17 +1088,17 @@ public class SettingsActivity extends BaseFragment implements NotificationCenter writeButtonAnimation.start(); } - ViewProxy.setScaleX(avatarImage, (42 + 18 * diff) / 42.0f); - ViewProxy.setScaleY(avatarImage, (42 + 18 * diff) / 42.0f); + avatarImage.setScaleX((42 + 18 * diff) / 42.0f); + avatarImage.setScaleY((42 + 18 * diff) / 42.0f); float avatarY = (actionBar.getOccupyStatusBar() ? AndroidUtilities.statusBarHeight : 0) + ActionBar.getCurrentActionBarHeight() / 2.0f * (1.0f + diff) - 21 * AndroidUtilities.density + 27 * AndroidUtilities.density * diff; - ViewProxy.setTranslationX(avatarImage, -AndroidUtilities.dp(47) * diff); - ViewProxy.setTranslationY(avatarImage, (float) Math.ceil(avatarY)); - ViewProxy.setTranslationX(nameTextView, -21 * AndroidUtilities.density * diff); - ViewProxy.setTranslationY(nameTextView, (float) Math.floor(avatarY) - (float) Math.ceil(AndroidUtilities.density) + (float) Math.floor(7 * AndroidUtilities.density * diff)); - ViewProxy.setTranslationX(onlineTextView, -21 * AndroidUtilities.density * diff); - ViewProxy.setTranslationY(onlineTextView, (float) Math.floor(avatarY) + AndroidUtilities.dp(22) + (float )Math.floor(11 * AndroidUtilities.density) * diff); - ViewProxy.setScaleX(nameTextView, 1.0f + 0.12f * diff); - ViewProxy.setScaleY(nameTextView, 1.0f + 0.12f * diff); + avatarImage.setTranslationX(-AndroidUtilities.dp(47) * diff); + avatarImage.setTranslationY((float) Math.ceil(avatarY)); + nameTextView.setTranslationX(-21 * AndroidUtilities.density * diff); + nameTextView.setTranslationY((float) Math.floor(avatarY) - (float) Math.ceil(AndroidUtilities.density) + (float) Math.floor(7 * AndroidUtilities.density * diff)); + onlineTextView.setTranslationX(-21 * AndroidUtilities.density * diff); + onlineTextView.setTranslationY((float) Math.floor(avatarY) + AndroidUtilities.dp(22) + (float )Math.floor(11 * AndroidUtilities.density) * diff); + nameTextView.setScaleX(1.0f + 0.12f * diff); + nameTextView.setScaleY(1.0f + 0.12f * diff); } } @@ -1154,7 +1182,7 @@ public class SettingsActivity extends BaseFragment implements NotificationCenter i == askQuestionRow || i == sendLogsRow || i == sendByEnterRow || i == autoplayGifsRow || i == privacyRow || i == wifiDownloadRow || i == mobileDownloadRow || i == clearLogsRow || i == roamingDownloadRow || i == languageRow || i == usernameRow || i == switchBackendButtonRow || i == telegramFaqRow || i == contactsSortRow || i == contactsReimportRow || i == saveToGalleryRow || - i == stickersRow || i == cacheRow || i == raiseToSpeakRow || i == privacyPolicyRow || i == customTabsRow || i == directShareRow; + i == stickersRow || i == cacheRow || i == raiseToSpeakRow || i == privacyPolicyRow || i == customTabsRow || i == directShareRow || i == versionRow; } @Override @@ -1352,13 +1380,11 @@ public class SettingsActivity extends BaseFragment implements NotificationCenter } text += LocaleController.getString("AttachMusic", R.string.AttachMusic); } - if (Build.VERSION.SDK_INT >= 11) { - if ((mask & MediaController.AUTODOWNLOAD_MASK_GIF) != 0) { - if (text.length() != 0) { - text += ", "; - } - text += LocaleController.getString("AttachGif", R.string.AttachGif); + if ((mask & MediaController.AUTODOWNLOAD_MASK_GIF) != 0) { + if (text.length() != 0) { + text += ", "; } + text += LocaleController.getString("AttachGif", R.string.AttachGif); } if (text.length() == 0) { text = LocaleController.getString("NoMediaAutoDownload", R.string.NoMediaAutoDownload); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/StickerPreviewViewer.java b/TMessagesProj/src/main/java/org/telegram/ui/StickerPreviewViewer.java index 9970ed5e3..2e898ba88 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/StickerPreviewViewer.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/StickerPreviewViewer.java @@ -400,6 +400,9 @@ public class StickerPreviewViewer { } private void onDraw(Canvas canvas) { + if (containerView == null || backgroundDrawable == null) { + return; + } backgroundDrawable.setAlpha((int) (180 * showProgress)); backgroundDrawable.setBounds(0, 0, containerView.getWidth(), containerView.getHeight()); backgroundDrawable.draw(canvas); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/StickersActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/StickersActivity.java index a6644a08a..edff6df2e 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/StickersActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/StickersActivity.java @@ -13,7 +13,6 @@ import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.graphics.Canvas; -import android.os.Build; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.view.View; @@ -148,6 +147,7 @@ public class StickersActivity extends BaseFragment implements NotificationCenter listView = new RecyclerListView(context); listView.setFocusable(true); + listView.setTag(7); LinearLayoutManager layoutManager = new LinearLayoutManager(context); layoutManager.setOrientation(LinearLayoutManager.VERTICAL); listView.setLayoutManager(layoutManager); @@ -272,14 +272,9 @@ public class StickersActivity extends BaseFragment implements NotificationCenter } } else if (which == 3) { try { - if (Build.VERSION.SDK_INT < 11) { - android.text.ClipboardManager clipboard = (android.text.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); - clipboard.setText(String.format(Locale.US, "https://telegram.me/addstickers/%s", stickerSet.set.short_name)); - } else { - android.content.ClipboardManager clipboard = (android.content.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); - android.content.ClipData clip = android.content.ClipData.newPlainText("label", String.format(Locale.US, "https://telegram.me/addstickers/%s", stickerSet.set.short_name)); - clipboard.setPrimaryClip(clip); - } + android.content.ClipboardManager clipboard = (android.content.ClipboardManager) ApplicationLoader.applicationContext.getSystemService(Context.CLIPBOARD_SERVICE); + android.content.ClipData clip = android.content.ClipData.newPlainText("label", String.format(Locale.US, "https://telegram.me/addstickers/%s", stickerSet.set.short_name)); + clipboard.setPrimaryClip(clip); Toast.makeText(getParentActivity(), LocaleController.getString("LinkCopied", R.string.LinkCopied), Toast.LENGTH_SHORT).show(); } catch (Exception e) { FileLog.e("tmessages", e); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/TwoStepVerificationActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/TwoStepVerificationActivity.java index 25c793b26..63e638ee6 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/TwoStepVerificationActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/TwoStepVerificationActivity.java @@ -19,7 +19,6 @@ import android.text.InputType; import android.text.method.PasswordTransformationMethod; import android.util.TypedValue; import android.view.ActionMode; -import android.view.ContextMenu; import android.view.Gravity; import android.view.KeyEvent; import android.view.Menu; @@ -219,30 +218,22 @@ public class TwoStepVerificationActivity extends BaseFragment implements Notific return false; } }); - if (android.os.Build.VERSION.SDK_INT < 11) { - passwordEditText.setOnCreateContextMenuListener(new View.OnCreateContextMenuListener() { - public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { - menu.clear(); - } - }); - } else { - passwordEditText.setCustomSelectionActionModeCallback(new ActionMode.Callback() { - public boolean onPrepareActionMode(ActionMode mode, Menu menu) { - return false; - } + passwordEditText.setCustomSelectionActionModeCallback(new ActionMode.Callback() { + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + return false; + } - public void onDestroyActionMode(ActionMode mode) { - } + public void onDestroyActionMode(ActionMode mode) { + } - public boolean onCreateActionMode(ActionMode mode, Menu menu) { - return false; - } + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + return false; + } - public boolean onActionItemClicked(ActionMode mode, MenuItem item) { - return false; - } - }); - } + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + return false; + } + }); bottomTextView = new TextView(context); bottomTextView.setTextColor(0xff757575); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/WallpapersActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/WallpapersActivity.java index 56a53fc45..fa90e559d 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/WallpapersActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/WallpapersActivity.java @@ -20,7 +20,6 @@ import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; import android.graphics.drawable.Drawable; import android.net.Uri; -import android.os.Build; import android.os.Bundle; import android.provider.MediaStore; import android.view.Gravity; @@ -192,14 +191,13 @@ public class WallpapersActivity extends BaseFragment implements NotificationCent RecyclerListView listView = new RecyclerListView(context); listView.setClipToPadding(false); + listView.setTag(8); listView.setPadding(AndroidUtilities.dp(40), 0, AndroidUtilities.dp(40), 0); LinearLayoutManager layoutManager = new LinearLayoutManager(context); layoutManager.setOrientation(LinearLayoutManager.HORIZONTAL); listView.setLayoutManager(layoutManager); listView.setDisallowInterceptTouchEvents(true); - if (Build.VERSION.SDK_INT >= 9) { - listView.setOverScrollMode(RecyclerListView.OVER_SCROLL_NEVER); - } + listView.setOverScrollMode(RecyclerListView.OVER_SCROLL_NEVER); listView.setAdapter(listAdapter = new ListAdapter(context)); frameLayout.addView(listView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 102, Gravity.LEFT | Gravity.BOTTOM)); listView.setOnItemClickListener(new RecyclerListView.OnItemClickListener() { diff --git a/TMessagesProj/src/main/res/drawable-hdpi/chat_badge.9.png b/TMessagesProj/src/main/res/drawable-hdpi/chat_badge.9.png new file mode 100755 index 000000000..50fd7f07d Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/chat_badge.9.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/inline_video_pause.png b/TMessagesProj/src/main/res/drawable-hdpi/inline_video_pause.png new file mode 100644 index 000000000..4832b585c Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/inline_video_pause.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/inline_video_play.png b/TMessagesProj/src/main/res/drawable-hdpi/inline_video_play.png new file mode 100644 index 000000000..981f5fb21 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-hdpi/inline_video_play.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/pagedown.png b/TMessagesProj/src/main/res/drawable-hdpi/pagedown.png old mode 100644 new mode 100755 index 21ed36ba4..0142cce7d Binary files a/TMessagesProj/src/main/res/drawable-hdpi/pagedown.png and b/TMessagesProj/src/main/res/drawable-hdpi/pagedown.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/search_down.png b/TMessagesProj/src/main/res/drawable-hdpi/search_down.png old mode 100755 new mode 100644 index f7831fa7a..756bae2fd Binary files a/TMessagesProj/src/main/res/drawable-hdpi/search_down.png and b/TMessagesProj/src/main/res/drawable-hdpi/search_down.png differ diff --git a/TMessagesProj/src/main/res/drawable-hdpi/search_up.png b/TMessagesProj/src/main/res/drawable-hdpi/search_up.png old mode 100755 new mode 100644 index 41839a16b..e19ca0cd3 Binary files a/TMessagesProj/src/main/res/drawable-hdpi/search_up.png and b/TMessagesProj/src/main/res/drawable-hdpi/search_up.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/chat_badge.9.png b/TMessagesProj/src/main/res/drawable-mdpi/chat_badge.9.png new file mode 100755 index 000000000..493995bcd Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/chat_badge.9.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/inline_video_pause.png b/TMessagesProj/src/main/res/drawable-mdpi/inline_video_pause.png new file mode 100644 index 000000000..27d8ea66e Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/inline_video_pause.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/inline_video_play.png b/TMessagesProj/src/main/res/drawable-mdpi/inline_video_play.png new file mode 100644 index 000000000..e3b8d462a Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-mdpi/inline_video_play.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/pagedown.png b/TMessagesProj/src/main/res/drawable-mdpi/pagedown.png old mode 100644 new mode 100755 index ce2e10bf0..98e51e823 Binary files a/TMessagesProj/src/main/res/drawable-mdpi/pagedown.png and b/TMessagesProj/src/main/res/drawable-mdpi/pagedown.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/search_down.png b/TMessagesProj/src/main/res/drawable-mdpi/search_down.png old mode 100755 new mode 100644 index 143e47d83..405e4c6d3 Binary files a/TMessagesProj/src/main/res/drawable-mdpi/search_down.png and b/TMessagesProj/src/main/res/drawable-mdpi/search_down.png differ diff --git a/TMessagesProj/src/main/res/drawable-mdpi/search_up.png b/TMessagesProj/src/main/res/drawable-mdpi/search_up.png old mode 100755 new mode 100644 index 9c3d94a2b..5c2152e51 Binary files a/TMessagesProj/src/main/res/drawable-mdpi/search_up.png and b/TMessagesProj/src/main/res/drawable-mdpi/search_up.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/chat_badge.9.png b/TMessagesProj/src/main/res/drawable-xhdpi/chat_badge.9.png new file mode 100755 index 000000000..51610d0c2 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/chat_badge.9.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/inline_video_pause.png b/TMessagesProj/src/main/res/drawable-xhdpi/inline_video_pause.png new file mode 100644 index 000000000..5ac38bd6f Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/inline_video_pause.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/inline_video_play.png b/TMessagesProj/src/main/res/drawable-xhdpi/inline_video_play.png new file mode 100644 index 000000000..99e0e6fb8 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xhdpi/inline_video_play.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/pagedown.png b/TMessagesProj/src/main/res/drawable-xhdpi/pagedown.png old mode 100644 new mode 100755 index c84cc4057..685d8306a Binary files a/TMessagesProj/src/main/res/drawable-xhdpi/pagedown.png and b/TMessagesProj/src/main/res/drawable-xhdpi/pagedown.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/search_down.png b/TMessagesProj/src/main/res/drawable-xhdpi/search_down.png old mode 100755 new mode 100644 index 2b8bd80cc..0c6e1e029 Binary files a/TMessagesProj/src/main/res/drawable-xhdpi/search_down.png and b/TMessagesProj/src/main/res/drawable-xhdpi/search_down.png differ diff --git a/TMessagesProj/src/main/res/drawable-xhdpi/search_up.png b/TMessagesProj/src/main/res/drawable-xhdpi/search_up.png old mode 100755 new mode 100644 index a212e27c0..6cded70aa Binary files a/TMessagesProj/src/main/res/drawable-xhdpi/search_up.png and b/TMessagesProj/src/main/res/drawable-xhdpi/search_up.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/chat_badge.9.png b/TMessagesProj/src/main/res/drawable-xxhdpi/chat_badge.9.png new file mode 100755 index 000000000..501e2eb61 Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/chat_badge.9.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/inline_video_pause.png b/TMessagesProj/src/main/res/drawable-xxhdpi/inline_video_pause.png new file mode 100644 index 000000000..5016a920f Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/inline_video_pause.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/inline_video_play.png b/TMessagesProj/src/main/res/drawable-xxhdpi/inline_video_play.png new file mode 100644 index 000000000..ef8b9cf6b Binary files /dev/null and b/TMessagesProj/src/main/res/drawable-xxhdpi/inline_video_play.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/pagedown.png b/TMessagesProj/src/main/res/drawable-xxhdpi/pagedown.png old mode 100644 new mode 100755 index 96affd8d8..2d58a5f80 Binary files a/TMessagesProj/src/main/res/drawable-xxhdpi/pagedown.png and b/TMessagesProj/src/main/res/drawable-xxhdpi/pagedown.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/search_down.png b/TMessagesProj/src/main/res/drawable-xxhdpi/search_down.png old mode 100755 new mode 100644 index 55352cb7d..3b6909c7e Binary files a/TMessagesProj/src/main/res/drawable-xxhdpi/search_down.png and b/TMessagesProj/src/main/res/drawable-xxhdpi/search_down.png differ diff --git a/TMessagesProj/src/main/res/drawable-xxhdpi/search_up.png b/TMessagesProj/src/main/res/drawable-xxhdpi/search_up.png old mode 100755 new mode 100644 index 35730561d..fd54f5b41 Binary files a/TMessagesProj/src/main/res/drawable-xxhdpi/search_up.png and b/TMessagesProj/src/main/res/drawable-xxhdpi/search_up.png differ diff --git a/TMessagesProj/src/main/res/values-ar/strings.xml b/TMessagesProj/src/main/res/values-ar/strings.xml index ecc4dbe36..de547d726 100644 --- a/TMessagesProj/src/main/res/values-ar/strings.xml +++ b/TMessagesProj/src/main/res/values-ar/strings.xml @@ -69,6 +69,8 @@ المحادثات هل ترغب في حذف %1$s من الاقتراحات؟ معاينة الرابط + مسودة + تم مسح السجل نوع المجموعة نوع القناة @@ -107,8 +109,6 @@ أعضاء القائمة السوداء هم أعضاء تم حذفهم من المجموعة ولا يمكنهم العودة لها إلى بدعوة من المشرف. روابط الدعوة لن تمكنهم من العودة للمجموعة. قناة جديدة اسم القناة - التعليقات - إذا قمت بتفعيل التعليقات، الأعضاء سيتمكنون من مناقشة ما تنشره في قناتك. إضافة جهات اتصال لقناتك يستطيع الناس مشاركة هذا الرابط مع غيرهم ويمكنهم إيجاد قناتك من خلال البحث في تيليجرام. يستطيع الناس مشاركة هذا الرابط مع غيرهم ويمكنهم إيجاد مجموعتك من خلال البحث في تيليجرام. @@ -133,11 +133,8 @@ غادر القناة الإعدادات اشترك - معلومات القناة رسالة جماعية رسالة جماعية صامتة - تعليق - إظهار التعليقات ما هي القنوات؟ القنوات هي أداة جديدة للرسائل الجماعية لمجموعة كبيرة من المستقبلين. أنشاء قناة @@ -469,7 +466,7 @@ تحذير هذا القرار لا يمكن الرجوع عنه. لن تتمكن من إرجاع المجموعات الخارقة لتصبح مجموعات عادية. ]]>تم الوصول للحد القصى.]]>\n\nلتتمكن من تخطي هذا الحد والحصول على مميزات إضافية، يمكنك الترقية لمجموعة خارقة:\n\n• المجموعات الخارقة يمكن أن تصل إلى %1$s عضو\n• الأعضاء الجدد يمكنهم رؤية سجل المحادثات بالكامل \n• الرسائل المحذوفة ستختفي عن كل الأعضاء\n• يستطيع الأعضاء التعديل على رسائلهم\n• يمكن لمنشئ المجموعة إنشاء رابط دعوة لها - ]]>في المجموعات الخارقة]]>\n\n• الأعضاء الجدد يمكنهم رؤية سجل المحادثات بالكامل \n• الرسائل المحذوفة ستختفي عن كل الأعضاء\n• يستطيع الأعضاء التعديل على رسائلهم\n• يمكن لمنشئ المجموعة إنشاء رابط دعوة لها + ]]>في المجموعات الخارقة:]]>\n\n• الأعضاء الجدد يمكنهم رؤية سجل المحادثات بالكامل \n• الرسائل المحذوفة ستختفي عن كل الأعضاء\n• يستطيع الأعضاء التعديل على رسائلهم\n• يمكن لمنشئ المجموعة إنشاء رابط دعوة لها ]]>ملاحظة:]]> لا يمكنك الرجوع عن هذا القرار. مشاركة @@ -1038,12 +1035,6 @@ %1$d رسائل %1$d رسالة %1$d رسالة - %1$d تعليقات - %1$d تعليق - %1$d تعليقان - %1$d تعليقات - %1$d تعليق - %1$d تعليق لا يوجد ملفات %1$d ملف %1$d ملف diff --git a/TMessagesProj/src/main/res/values-de/strings.xml b/TMessagesProj/src/main/res/values-de/strings.xml index f82376c81..6d98d1d21 100644 --- a/TMessagesProj/src/main/res/values-de/strings.xml +++ b/TMessagesProj/src/main/res/values-de/strings.xml @@ -69,6 +69,8 @@ CHATS Lösche %1$s aus Vorschlägen? Linkvorschau + Entwurf + Verlauf wurde gelöscht Gruppenart Kanalart @@ -107,8 +109,6 @@ Blockierte Nutzer können nur durch Admins erneut hinzugefügt werden. Einladungslinks funktionieren nicht. Neuer Kanal Kanalname - Kommentare - Wenn du Kommentare aktivierst, können sich alle an der Diskussion beteiligen. Kontakte zum Kanal hinzufügen Jeder kann diesen Link teilen und deinen Kanal in der Telegramsuche finden. Jeder kann diesen Link teilen und deine Gruppe in der Telegramsuche finden. @@ -133,11 +133,8 @@ Kanal verlassen Einstellungen BEITRETEN - Info Broadcast Lautloser Broadcast - Kommentar - Kommentare zeigen Was ist ein Kanal? In einem Kanal kannst du deine Nachrichten an ein großes Publikum schicken. KANAL ERSTELLEN @@ -468,8 +465,8 @@ In Supergruppe ändern Warnung Du kannst die Supergruppe nicht mehr in eine normale Gruppe ändern. - ]]>Gruppenlimit erreicht.]]>\n\nFür weitere Funktionen und um das Limit aufzuheben in Supergruppe ändern:\n\n• Bis zu %1$s sind nun möglich\n• Neue Mitglieder sehen gesamten Verlauf\n• Mitglieder können eigene Nachrichten bearbeiten\n• Gruppenersteller kann die Gruppe öffentlich machen - ]]>In Supergruppen:]]>\n\n• Neue Mitglieder sehen gesamten Verlauf\n• Nachrichten werden bei allen gelöscht\n• Jeder kann eigene Nachrichten bearbeiten\n• Gründer kann Gruppe öffentlich machen + ]]>Gruppenlimit erreicht.]]>\n\nFür weitere Funktionen und um das Limit aufzuheben in Supergruppe ändern:\n\n• Bis zu %1$s sind nun möglich\n• Neue Mitglieder sehen gesamten Verlauf\n• Gelöschte Nachrichten werden bei allen Mitgliedern entfernt\n•Gruppenersteller kann die Gruppe öffentlich machen + ]]>In Supergruppen:]]>\n\n• Neue Mitglieder sehen gesamten Verlauf\n• Nachrichten werden bei allen gelöscht\n• Gründer kann Gruppe öffentlich machen ]]>Wichtig:]]> Die Änderung kann nicht rückgängig gemacht werden. Teilen @@ -1038,12 +1035,6 @@ %1$d Nachrichten %1$d Nachrichten %1$d Nachrichten - %1$d Kommentare - %1$d Kommentar - %1$d Kommentare - %1$d Kommentare - %1$d Kommentare - %1$d Kommentare keine Objekte %1$d Objekt %1$d Objekte @@ -1212,4 +1203,4 @@ HH:mm h:mm a %1$s um %2$s - \ No newline at end of file + diff --git a/TMessagesProj/src/main/res/values-es/strings.xml b/TMessagesProj/src/main/res/values-es/strings.xml index bdcb4f49d..d3198fd82 100644 --- a/TMessagesProj/src/main/res/values-es/strings.xml +++ b/TMessagesProj/src/main/res/values-es/strings.xml @@ -39,7 +39,7 @@ ayer Sin resultados Aún sin chats... - Envía mensajes pulsando el botón para\nredactar, en la parte inferior derecha,\no pulsa el botón menú para más opciones. + Envía mensajes tocando el botón para\nredactar, en la parte inferior derecha,\no pulsa el botón menú para más opciones. Esperando red... Conectando... Actualizando... @@ -56,7 +56,7 @@ Cuenta eliminada Elige el chat Mantén pulsado para ver - %1$s usa una versión antigua de Telegram, así que las fotos secretas serán mostradas en un modo de compatibilidad.\n\nCuando %2$s actualice Telegram, las fotos con autodestrucción de 1 minuto o menos funcionarán con el modo \'Mantén pulsado para ver\', y te notificaremos siempre que la otra parte haga una captura de pantalla. + %1$s usa una versión antigua de Telegram, así que las fotos secretas serán mostradas en un modo de compatibilidad.\n\nCuando %2$s actualice Telegram, las fotos con autodestrucción de 1 minuto o menos funcionarán con el modo “Mantén pulsado para ver”, y te notificaremos siempre que la otra parte haga una captura de pantalla. MENSAJES Buscar Silenciar notificaciones @@ -69,6 +69,8 @@ CHATS ¿Eliminar a %1$s de las sugerencias? Vista previa del enlace + Borrador + Historial borrado Tipo de grupo Tipo de canal @@ -107,8 +109,6 @@ Los usuarios bloqueados son eliminados del grupo y sólo pueden volver si son invitados por un administrador. Los enlaces de invitación no funcionan para ellos. Nuevo canal Nombre del canal - Comentarios - Si activas los comentarios, las personas podrán hablar de tus mensajes en el canal. Añadir contactos a tu canal Las personas pueden compartir este enlace con los demás y encontrar tu canal usando la búsqueda de Telegram. Las personas pueden compartir este enlace con los demás y encontrar tu grupo usando la búsqueda de Telegram. @@ -132,12 +132,9 @@ Dejar canal Dejar el canal Ajustes - UNIRSE - Información + UNIRME Difundir Difusión en silencio - Comentario - mostrar comentarios ¿Qué es un canal? Los canales son una nueva herramienta para difundir tus mensajes a grandes audiencias. CREAR CANAL @@ -181,7 +178,7 @@ Este usuario aún no se ha unido al canal. ¿Quieres invitarlo? Cualquiera que tenga Telegram instalada podrá unirse a tu canal siguiendo este enlace. Puedes añadir administradores para que te ayuden en el canal. Mantén pulsado para eliminarlos. - ¿Quieres unirte al canal \'%1$s\'? + ¿Quieres unirte al canal “%1$s”? Lo sentimos, este chat ya no es accesible. Lamentablemente, fuiste suspendido de participar en grupos públicos. Lo sentimos, este chat ya no es accesible. @@ -320,7 +317,7 @@ https://telegram.org/faq/es#no-puedo-enviar-mensajes-a-quienes-no-son-mis-contactos Más información Enviar a... - Pulsa y ve los GIF guardados + Toca y ve los GIF guardados Anclar Notificar a los miembros Desanclar @@ -383,7 +380,7 @@ %1$s te eliminó del grupo %2$s %1$s dejó el grupo %2$s ¡%1$s se unió a Telegram! - %1$s,\nDetectamos un inicio de sesión en tu cuenta desde un nuevo dispositivo, el %2$s\n\nDispositivo: %3$s\nUbicación: %4$s\n\nSi no eras tú, puedes ir a Ajustes - Privacidad y seguridad - Sesiones activas y cerrar esa sesión.\n\nSi crees que alguien ha iniciado la sesión sin tu consentimiento, puedes activar la verificación en dos pasos, en los ajustes de privacidad y seguridad.\n\nAtentamente,\nEl equipo de Telegram + %1$s,\nDetectamos un inicio de sesión en tu cuenta desde un nuevo dispositivo, el %2$s\n\nDispositivo: %3$s\nUbicación: %4$s\n\nSi no eras tú, puedes ir a Ajustes > Privacidad y seguridad > Sesiones activas y cerrar esa sesión.\n\nSi crees que alguien ha iniciado la sesión sin tu consentimiento, puedes activar la verificación en dos pasos, en los ajustes de privacidad y seguridad.\n\nAtentamente,\nEl equipo de Telegram %1$s actualizó su foto de perfil %1$s se unió al grupo %2$s con un enlace de invitación Responder @@ -437,7 +434,7 @@ Nombre del grupo Nombre del grupo %1$d/%2$d miembros - ¿Quieres unirte al chat \'%1$s\'? + ¿Quieres unirte al chat “%1$s”? Lo sentimos. Este grupo está lleno. Lo sentimos, este chat no existe. Enlace copiado al portapapeles @@ -468,8 +465,8 @@ Convertir en supergrupo Advertencia Esta acción es irreversible. No puedes convertir un supergrupo en un grupo normal. - ]]>Límite de miembros alcanzado.]]>\n\nPara superar el límite y tener características adicionales, conviértelo en un supergrupo:\n\n• Permiten hasta %1$s\n• Nuevos miembros ven todo el historial\n• Un mensaje eliminado se borra para todos\n• Un miembro puede editar sus mensajes\n• El creador puede generar un enlace público - ]]>En los supergrupos:]]>\n\n• Los nuevos miembros ven todo el historial\n• Un mensaje eliminado se borra para todos\n• Un miembro puede editar sus mensajes\n• El creador puede generar un enlace público + ]]>Límite de miembros alcanzado.]]>\n\nPara superar el límite y tener características adicionales, conviértelo en un supergrupo:\n\n Permiten hasta %1$s\n• Nuevos miembros ven todo el historial\n• Un mensaje eliminado se borra para todos\n• El creador puede generar un enlace público + ]]>En los supergrupos:]]>\n\n• Los nuevos miembros ven todo el historial\n• Un mensaje eliminado se borra para todos\n• El creador puede generar un enlace público ]]>Importante:]]> Esta acción no se puede deshacer. Compartir @@ -508,7 +505,7 @@ Ocurrió un error. Stickers - Los artistas pueden añadir sus propios packs de stickers usando el bot @stickers.\n\nLos usuarios pueden añadir stickers pulsando y eligiendo \"Añadir stickers\". + Los artistas pueden añadir sus propios packs de stickers usando el bot @stickers.\n\nLos usuarios pueden añadir stickers tocando y eligiendo “Añadir stickers”. Añadir stickers Añadir a stickers Stickers no encontrados @@ -552,7 +549,7 @@ Sólo si está silenciado Fondo de chat Mensajes - Enviar con \'Intro\' + Enviar con “Intro” Cerrar todas las otras sesiones Eventos Un contacto se unió a Telegram @@ -603,7 +600,7 @@ Máxima Nunca Repetir notificaciones - Puedes cambiar tu número de Telegram aquí. Tu cuenta y todos tus datos de la nube — mensajes, archivos, grupos, contactos, etc., se moverán al nuevo número.\n\nImportante:]]> Todos tus contactos de Telegram tendrán tu nuevo número]]> añadido a sus agendas de contactos, siempre que hayan tenido tu número viejo y no los hayas bloqueado en Telegram. + Puedes cambiar tu número de Telegram aquí. Tu cuenta y todos tus datos de la nube, mensajes, archivos, grupos, contactos, etc., se moverán al nuevo número.\n\nImportante:]]> Todos tus contactos de Telegram tendrán tu nuevo número]]> añadido a sus agendas de contactos, siempre que hayan tenido tu número viejo y no los hayas bloqueado en Telegram. Todos tus contactos de Telegram tendrán tu número nuevo añadido a sus agendas de contactos, siempre que hayan tenido tu número viejo y no los hayas bloqueado en Telegram. CAMBIAR NÚMERO Nuevo número @@ -655,7 +652,7 @@ Puedes iniciar sesión en Telegram desde otro móvil, tablet o computador, usando el mismo número de teléfono. Todos tus datos se sincronizarán al instante. Sesiones activas Controla tus sesiones en otros dispositivos. - Pulsa una sesión para cerrarla. + Toca una sesión para cerrarla. ¿Cerrar esta sesión? aplicación no oficial @@ -938,7 +935,7 @@ Cargando... No tienes reproductor de vídeo. Por favor, instala uno para continuar. Por favor, envía un correo electrónico a sms@stel.com y cuéntanos tu problema. - No tienes aplicaciones que puedan manejar el tipo de archivo \'%1$s\'. Por favor, instala una para continuar. + No tienes aplicaciones que puedan manejar el tipo de archivo “%1$s”. Por favor, instala una para continuar. Este usuario aún no tiene Telegram. ¿Enviarle una invitación? ¿Quieres hacerlo? ¿Añadir a %1$s al chat %2$s? @@ -978,7 +975,7 @@ Este bot no puede unirse a grupos. ¿Quieres permitir las vistas previas ampliadas en chats secretos? Ten en cuenta que son generadas en los servidores de Telegram. Ten en cuenta que los bots integrados son hechos por terceros. Para que funcione, los símbolos escritos después del alias del bot, son enviados al desarrollador respectivo. - ¿Quieres habilitar \"Elevar para hablar\" para mensajes de voz? + ¿Quieres habilitar “Elevar para hablar” para mensajes de voz? No puedes editar este mensaje. Por favor, permite a Telegram recibir SMS, para ingresar el código automáticamente. Por favor, permite a Telegram recibir llamadas, para ingresar el código automáticamente. @@ -1038,12 +1035,6 @@ %1$d mensajes %1$d mensajes %1$d mensajes - %1$d comentarios - %1$d comentario - %1$d comentarios - %1$d comentarios - %1$d comentarios - %1$d comentarios Sin ítems %1$d ítem %1$d ítems @@ -1212,4 +1203,4 @@ HH:mm h:mm a %1$s a las %2$s - \ No newline at end of file + diff --git a/TMessagesProj/src/main/res/values-it/strings.xml b/TMessagesProj/src/main/res/values-it/strings.xml index 61e3c426c..950a01bb8 100644 --- a/TMessagesProj/src/main/res/values-it/strings.xml +++ b/TMessagesProj/src/main/res/values-it/strings.xml @@ -69,6 +69,8 @@ CHAT Eliminare %1$s dai suggerimenti? Anteprima link + Bozza + Cronologia eliminata Tipo di gruppo Tipo di canale @@ -107,8 +109,6 @@ Gli utenti in lista nera sono rimossi dal gruppo e possono tornare solo se invitati da un amministratore. I link di invito non funzionano per loro. Nuovo canale Nome canale - Commenti - Se attivi i commenti, i membri potranno discutere quello che pubblichi nel canale. Aggiungi contatti al tuo canale Le persone possono condividere questo link con gli altri e trovare il tuo canale usando la ricerca di Telegram. Le persone possono condividere questo link con gli altri e trovare il tuo gruppo usando la ricerca di Telegram. @@ -133,11 +133,8 @@ Lascia il canale Impostazioni UNISCITI - Info canale Post Post silenzioso - Commento - mostra commenti Cos\'è un canale? I canali sono un nuovo strumento per diffondere i tuoi messaggi a un grande pubblico. CREA CANALE @@ -159,8 +156,8 @@ Sei sicuro di voler lasciare il canale? Perderai tutti i messaggi in questo canale. Modifica - Per favore ricorda che se scegli un link pubblico per il tuo gruppo, chiunque sarà in grado di trovarlo nella ricerca e unirsi.\n\nNon creare questo link se vuoi che il tuo supergruppo rimanga privato. - Per favore ricorda che se scegli un link pubblico per il tuo canale, chiunque sarà in grado di trovarlo nella ricerca e unirsi.\n\nNon creare questo link se vuoi che il tuo canale rimanga privato. + Per favore nota che se scegli un link pubblico per il tuo gruppo, chiunque sarà in grado di trovarlo nella ricerca e unirsi.\n\nNon creare questo link se vuoi che il tuo supergruppo rimanga privato. + Per favore nota che se scegli un link pubblico per il tuo canale, chiunque sarà in grado di trovarlo nella ricerca e unirsi.\n\nNon creare questo link se vuoi che il tuo canale rimanga privato. Per favore scegli un link per il tuo canale pubblico, in modo che possa essere trovato nella ricerca e condiviso con altri.\n\nSe non sei interessato, ti consigliamo di creare un canale privato. Canale creato Foto del canale cambiata @@ -258,16 +255,16 @@ sta scrivendo… sta scrivendo… stanno scrivendo… - %1$s sta registrando un messaggio vocale... + %1$s sta registrando un audio... %1$s sta inviando un audio... %1$s sta inviando una foto... %1$s sta inviando un video... %1$s sta inviando un file... - registrando un messaggio vocale... - inviando un audio... - inviando una foto... - inviando un video... - inviando un file... + sta registrando un audio... + sta inviando un audio... + sta inviando una foto... + sta inviando un video... + sta inviando un file... Hai una domanda\nsu Telegram? Scatta foto Galleria @@ -468,8 +465,8 @@ Converti in supergruppo Attenzione Questa azione è irreversibile. Non è possibile trasformare un supergruppo in un gruppo normale. - ]]>Limite membri raggiunto.]]>\n\nPer superare il limite e sbloccare nuove funzioni, aggiorna a supergruppo:\n\n• Il supergruppi hanno massimo %1$s\n• I nuovi membri vedono tutta la cronologia\n• I messaggi eliminati scompaiono per tutti\n• I membri possono modificare i loro messaggi\n• Il creatore può creare un link pubblico per il gruppo - ]]>Nei supergruppi:]]>\n\n• I nuovi membri vedono tutta la cronologia\n• I messaggi eliminati scompaiono per tutti\n• I membri possono modificare i loro messaggi\n• Il creatore può creare un link pubblico per il gruppo + ]]>Limite membri raggiunto.]]>\n\nPer superare il limite e sbloccare nuove funzioni, aggiorna a supergruppo:\n\n• Il supergruppi hanno massimo %1$s\n• I nuovi membri vedono tutta la cronologia\n• I messaggi eliminati scompaiono per tutti\n• Il creatore può creare un link pubblico per il gruppo + ]]>Nei supergruppi:]]>\n\n• I nuovi membri vedono tutta la cronologia\n• I messaggi eliminati scompaiono per tutti\n• Il creatore può creare un link pubblico per il gruppo ]]>Nota:]]> questa azione non può essere annullata. Condividi @@ -545,7 +542,7 @@ Annulla tutte le impostazioni di notifica personalizzate per tutti i tuoi contatti e gruppi Notifiche e suoni Utenti bloccati - Disconnetti + Esci Nessun suono Predefinite Supporto @@ -635,8 +632,8 @@ Database locale Eliminare i messaggi salvati nella cache? Eliminando il database locale, verranno cancellati i messaggi nella cache e il database verrà compresso per risparmiare spazio. Telegram ha bisogno di alcuni dati per funzionare, quindi il database non sarà azzerato.\n\nQuesta operazione può richiedere alcuni minuti. - Pulisci cache - Pulisci + Svuota cache + Svuota Calcolando... Documenti Foto @@ -750,7 +747,7 @@ Sei sicuro di voler eliminare questo video? Annullare le modifiche? Cancellare la cronologia di ricerca? - Pulisci + Cancella Foto Video Aggiungi una didascalia... @@ -837,7 +834,7 @@ Non condividere con gli utenti... Aggiungi utenti Spiacenti, troppe richieste. Impossibile cambiare le impostazioni di privacy ora, attendi. - Disconnette tutti i dispositivi tranne questo. + Esce da tutti i dispositivi tranne questo. Tieni premuto sull\'utente per eliminarlo. Gruppi Chi può aggiungermi ai gruppi? @@ -855,7 +852,7 @@ Modifica video Video originale Video modificato - Inviando un video... + Invio video... Comprimi video bot @@ -948,7 +945,7 @@ Vuoi inoltrare i messaggi a %1$s? Inviare i messaggi a %1$s? Inviare contatto a %1$s? - Sei sicuro di volerti disconnettere?\n\nRicorda che puoi usare Telegram su tutti i tuoi dispositivi contemporaneamente.\n\nRicorda, quando ti disconnetti, elimini tutte le Chat Segrete. + Sei sicuro di voler uscire?\n\nNota che puoi usare Telegram su tutti i tuoi dispositivi contemporaneamente.\n\nRicorda, uscendo eliminerai tutte le Chat Segrete. Terminare tutte le altre sessioni? Sei sicuro di voler uscire ed eliminare il gruppo? Sei sicuro di voler eliminare questa chat? @@ -976,8 +973,8 @@ Spiacenti, questa funzione non è disponibile nel tuo paese. Non esiste alcun account Telegram con questo username. Questo bot non può unirsi ai gruppi. - Vuoi attivare le anteprime estese per i link nelle Chat Segrete? Ricorda che le anteprime dei link sono generate sui server di Telegram. - Per favore ricorda che i bot inline sono forniti da sviluppatori di terze parti. Per far funzionare il bot, i simboli che digiti dopo l\'username del bot sono inviati al rispettivo sviluppatore. + Vuoi attivare le anteprime estese per i link nelle Chat Segrete? Nota che le anteprime dei link sono generate sui server di Telegram. + Per favore nota che i bot inline sono forniti da sviluppatori di terze parti. Per far funzionare il bot, i simboli che digiti dopo l\'username del bot sono inviati al rispettivo sviluppatore. Vuoi attivare \"Alza per parlare\" per i messaggi vocali? Spiacenti, non puoi modificare questo messaggio. Per favore consenti a Telegram di ricevere SMS così potremo inserire in automatico il codice per te. @@ -1038,12 +1035,6 @@ %1$d messaggi %1$d messaggi %1$d messaggi - %1$d commenti - %1$d commento - %1$d commenti - %1$d commenti - %1$d commenti - %1$d commenti nessun oggetto %1$d oggetto %1$d oggetti @@ -1212,4 +1203,4 @@ HH:mm h:mm a %1$s alle %2$s - \ No newline at end of file + diff --git a/TMessagesProj/src/main/res/values-ko/strings.xml b/TMessagesProj/src/main/res/values-ko/strings.xml index 66a24bfaf..3c9671bea 100644 --- a/TMessagesProj/src/main/res/values-ko/strings.xml +++ b/TMessagesProj/src/main/res/values-ko/strings.xml @@ -69,6 +69,8 @@ 대화 %1$s를 추천에서 삭제하겠습니까? 링크 미리복 + 임시 + 내역이 초기화 됨 그룹 종류 채널 종류 @@ -107,8 +109,6 @@ 그룹방에서 차단되어 퇴장당한 사용자는 관리자가 초대해야지만 그룹방에 입장이 가능합니다. 초대링크로는 초대가 되지 않습니다. 새 채널 채널명 - 코멘트들 - 코멘트를 허용할 경우, 유저들이 회원님 글에 대하여 코멘트 등록이 가능합니다. 채널에 친구 추가 텔레그램 검색을 통하여 다른 유저들이 채널을 찾을 수 있습니다. 텔레그램 검색을 통하여 다른 유저들이 그룹을 찾을 수 있습니다. @@ -133,11 +133,8 @@ 채널 나가기 설정 입장 - 채널 정보 모두에게 메시지 전달 조용한 공지 - 코멘트 - 코멘트 보기 채널이 무엇인가요? 채널은 많은 유저들에게 메시지를 한번에 전송 할 수 있는 새로운 기능입니다. 채널 생성 @@ -469,7 +466,7 @@ 경고 이 작업은 되돌릴 수 없습니다. 슈퍼그룹에서 일반그룹방으로 다운 그레이드는 불가능 합니다. ]]>구성원이 최대치입니다.]]>\n\n추가 기능 및 더 많은 구성원을 추가하려면 슈퍼그룹방으로 업그레이드하세요:\n\n• 슈퍼그룹방은 %1$s명까지 초대가능합니다.\n• 새로운 구성원은 모든 대화내역을 볼 수 있습니다.\n• 메시지 삭제시 모두에게 삭제가 됩니다.\n• 방 생성자는 그룹방 공개링크 생성이 가능합니다. - ]]>슈퍼그룹:]]>\n\n• 새로운 구성원은 모든 대화내역을 볼 수 있습니다.\n• 메시지 삭제시 모두에게 삭제가 됩니다.\n• 메시지를 작성자가 수정 가능합니다.\n• 방 생성자는 그룹방 공개링크 생성이 가능합니다. + ]]>슈퍼그룹:]]>\n\n• 새로운 구성원은 모든 대화내역을 볼 수 있습니다.\n• 메시지 삭제시 모두에게 삭제가 됩니다.\n• 방 생성자는 그룹방 공개링크 생성이 가능합니다. ]]>주위:]]> 이 작업은 되돌릴 수 없습니다. 공유 @@ -1038,12 +1035,6 @@ 메시지 %1$d건 메시지 %1$d건 메시지 %1$d건 - %1$d 코멘트 - %1$d 코멘트 - %1$d 코멘트 - %1$d 코멘트 - %1$d 코멘트 - %1$d 코멘트 아이템이 없습니다. %1$d개 아이템 %1$d개 아이템 @@ -1212,4 +1203,4 @@ HH:mm a h:mm %1$s %2$s - \ No newline at end of file + diff --git a/TMessagesProj/src/main/res/values-nl/strings.xml b/TMessagesProj/src/main/res/values-nl/strings.xml index bebae46c6..e3eed97f2 100644 --- a/TMessagesProj/src/main/res/values-nl/strings.xml +++ b/TMessagesProj/src/main/res/values-nl/strings.xml @@ -69,6 +69,8 @@ CHATS %1$s uit suggesties verwijderen? Link-voorvertoning + Concept + Geschiedenis gewist Groepsvorm Kanaaltype @@ -107,8 +109,6 @@ Geblokkeerde gebruikers kunnen alleen worden uitgenodigd door beheerders, uitnodigingslinks werken niet voor hen. Nieuw kanaal Kanaalnaam - Reacties - Als je reacties inschakelt kunnen leden reageren op je bericht in het kanaal. Contacten aan je kanaal toevoegen Deze link kan gedeeld worden met anderen en je kanaal kan worden gevonden via de zoekfunctie. Deze link kan gedeeld worden met anderen en je groep kan worden gevonden via de zoekfunctie. @@ -133,11 +133,8 @@ Kanaal verlaten Instellingen LID WORDEN - Kanaalinformatie Massabericht Stil massabericht - Reactie - reacties weergeven Wat is een kanaal? Kanalen zijn een nieuwe functie om berichten naar een groot publiek te sturen. MAAK KANAAL @@ -468,8 +465,8 @@ Opwaarderen naar supergroep Waarschuwing Groep echt omzetten naar supergroep? Je kunt dit niet ongedaan maken. - ]]>Ledenlimiet bereikt.]]>\n\nWil je extra functies en een hogere limiet? Waardeer op naar een supergroep:\n\n• Supergroepen hebben tot %1$s\n• Nieuwe leden zien de hele geschiedenis\n•Gewiste berichten gelden voor iedereen\n• Leden kunnen eigen berichten bewerken\n• Maker kan een publieke groepslink instellen - ]]>Supergroepen:]]>\n\n• Nieuwe leden zien de hele geschiedenis\n• Gewiste berichten gelden voor iedereen\n• Leden kunnen eigen berichten bewerken\n• Maker kan een publieke groepslink instellen + ]]>Ledenlimiet bereikt.]]>\n\nWil je extra functies en een hogere limiet? Waardeer op naar een supergroep:\n\n• Supergroepen hebben tot %1$s\n• Nieuwe leden zien de hele geschiedenis\n•Gewiste berichten gelden voor iedereen\n• Maker kan een publieke groepslink instellen + ]]>Supergroepen:]]>\n\n• Nieuwe leden zien de hele geschiedenis\n• Gewiste berichten gelden voor iedereen\n• Maker kan een publieke groepslink instellen ]]>Let op:]]> Je kunt dit niet ongedaan maken. Delen @@ -556,7 +553,7 @@ Beëindig alle andere sessies Gebeurtenissen Contact lid van Telegram - Vastgezet berichten + Vastgezette berichten Taal De ondersteuning van Telegram wordt gedaan door vrijwilligers. We doen ons best om zo snel mogelijk te antwoorden.
]]>Bekijk ook de veelgestelde vragen]]>: Hier staan de antwoorden op de meeste vragen en belangrijke tips voor het oplossen van problemen]]>.
Vraag een vrijwilliger @@ -1038,12 +1035,6 @@ %1$d berichten %1$d berichten %1$d berichten - %1$d reacties - %1$d reactie - %1$d reacties - %1$d reacties - %1$d reacties - %1$d reacties geen items %1$d item %1$d items @@ -1212,4 +1203,4 @@ HH:mm h:mm a %1$s om %2$s - \ No newline at end of file + diff --git a/TMessagesProj/src/main/res/values-pt-rBR/strings.xml b/TMessagesProj/src/main/res/values-pt-rBR/strings.xml index 0c0c6abff..6d2463656 100644 --- a/TMessagesProj/src/main/res/values-pt-rBR/strings.xml +++ b/TMessagesProj/src/main/res/values-pt-rBR/strings.xml @@ -69,6 +69,8 @@ CONVERSAS Apagar %1$s das sugestões? Prévia do link + Rascunho + O histórico foi apagado Tipo de Grupo Tipo de canal @@ -102,13 +104,11 @@ un1 fixou um mapa un1 fixou um GIF un1 fixou uma música - Este grupo foi atualizado para um supergrupo - %1$s foi atualizado para um supergrupo + Esse grupo foi convertido para um supergrupo + %1$s foi convertido a um supergrupo Usuários bloqueados são removidos do grupo e só podem voltar se convidados por um administrador. Convites por link não funcionam para eles. Novo Canal Nome do canal - Comentários - Se você habilitar comentários, pessoas poderão discutir seu post no canal. Adicionar contatos no canal Pessoas podem compartilhar esse link com outros e encontrar seu canal usando a busca do Telegram. Pessoas podem compartilhar esse link com outros e encontrar o seu grupo usando a busca do Telegram. @@ -133,11 +133,8 @@ Sair do Canal Configurações ENTRAR - Info do Canal Transmissão Mensagem Silenciosa - Comentário - mostrar comentários O que é um Canal? Canais são uma nova ferramenta para transmissão de suas mensagens para grandes audiências. CRIAR CANAL @@ -463,13 +460,13 @@ Apagar e sair do grupo Notificações Remover do grupo - Atualizar para Supergrupo + Converter a Supergrupo Converter a Supergrupo Converter a supergrupo Atenção Essa ação é irreversível. Não é possível voltar de um supergrupo para um grupo normal. - ]]>Limite de membros alcançado.]]>\n\nPara ir além do limite e ter funções adicionais, atualize para um supergrupo:\n\n• Supergrupos podem ter até %1$s\n• Novos membros podem visualizar todo o histórico\n• Mensagens apagadas desaparecerão para todos\n• Membros podem editar as próprias mensagens\n• Criador pode definir um link público para o grupo - ]]>Em supergroupos:]]>\n\n• Novos membros podem visualizar todo o histórico\n• Mensagens apagadas desaparecerão para todos\n• Membros podem editar as próprias mensagens\n• Criador pode definir um link público para o grupo + ]]>Limite de membros atingido.]]>\n\nPara ir além do limite e ter funções adcionais, converta para um supergrupo:\n\n• Supergrupos podem ter até %1$s\n• Novos membros veêm todo o histórico de conversas\n• Administradores deletam mensagens para todos\n• O criador pode definir um link público para o grupo + ]]>Em supergrupos:]]>\n\n• Novos membros podem visualizar todo o histórico\n• Mensagens apagadas desaparecerão para todos\n• Criador pode definir um link público para o grupo ]]>Nota:]]> essa ação não pode ser desfeita. Compartilhar @@ -1038,12 +1035,6 @@ %1$d mensagens %1$d mensagens %1$d mensagens - %1$d comentários - %1$d comentário - %1$d comentários - %1$d comentários - %1$d comentários - %1$d comentários nenhum item %1$d item %1$d itens @@ -1212,4 +1203,4 @@ HH:mm h:mm a %1$s às %2$s - \ No newline at end of file + diff --git a/TMessagesProj/src/main/res/values-pt-rPT/strings.xml b/TMessagesProj/src/main/res/values-pt-rPT/strings.xml index f2ed133fa..e7206b69a 100644 --- a/TMessagesProj/src/main/res/values-pt-rPT/strings.xml +++ b/TMessagesProj/src/main/res/values-pt-rPT/strings.xml @@ -69,6 +69,8 @@ CONVERSAS Apagar %1$s das sugestões? Prévia do link + Rascunho + O histórico foi apagado Tipo de Grupo Tipo de canal @@ -102,13 +104,11 @@ un1 fixou um mapa un1 fixou um GIF un1 fixou uma música - Este grupo foi atualizado para um supergrupo - %1$s foi atualizado para um supergrupo + Esse grupo foi convertido para um supergrupo + %1$s foi convertido a um supergrupo Usuários bloqueados são removidos do grupo e só podem voltar se convidados por um administrador. Convites por link não funcionam para eles. Novo Canal Nome do canal - Comentários - Se você habilitar comentários, pessoas poderão discutir seu post no canal. Adicionar contatos no canal Pessoas podem compartilhar esse link com outros e encontrar seu canal usando a busca do Telegram. Pessoas podem compartilhar esse link com outros e encontrar o seu grupo usando a busca do Telegram. @@ -133,11 +133,8 @@ Sair do Canal Configurações ENTRAR - Info do Canal Transmissão Mensagem Silenciosa - Comentário - mostrar comentários O que é um Canal? Canais são uma nova ferramenta para transmissão de suas mensagens para grandes audiências. CRIAR CANAL @@ -463,13 +460,13 @@ Apagar e sair do grupo Notificações Remover do grupo - Atualizar para Supergrupo + Converter a Supergrupo Converter a Supergrupo Converter a supergrupo Atenção Essa ação é irreversível. Não é possível voltar de um supergrupo para um grupo normal. - ]]>Limite de membros alcançado.]]>\n\nPara ir além do limite e ter funções adicionais, atualize para um supergrupo:\n\n• Supergrupos podem ter até %1$s\n• Novos membros podem visualizar todo o histórico\n• Mensagens apagadas desaparecerão para todos\n• Membros podem editar as próprias mensagens\n• Criador pode definir um link público para o grupo - ]]>Em supergroupos:]]>\n\n• Novos membros podem visualizar todo o histórico\n• Mensagens apagadas desaparecerão para todos\n• Membros podem editar as próprias mensagens\n• Criador pode definir um link público para o grupo + ]]>Limite de membros atingido.]]>\n\nPara ir além do limite e ter funções adcionais, converta para um supergrupo:\n\n• Supergrupos podem ter até %1$s\n• Novos membros veêm todo o histórico de conversas\n• Administradores deletam mensagens para todos\n• O criador pode definir um link público para o grupo + ]]>Em supergrupos:]]>\n\n• Novos membros podem visualizar todo o histórico\n• Mensagens apagadas desaparecerão para todos\n• Criador pode definir um link público para o grupo ]]>Nota:]]> essa ação não pode ser desfeita. Compartilhar @@ -1038,12 +1035,6 @@ %1$d mensagens %1$d mensagens %1$d mensagens - %1$d comentários - %1$d comentário - %1$d comentários - %1$d comentários - %1$d comentários - %1$d comentários nenhum item %1$d item %1$d itens @@ -1212,4 +1203,4 @@ HH:mm h:mm a %1$s às %2$s - \ No newline at end of file + diff --git a/TMessagesProj/src/main/res/values/strings.xml b/TMessagesProj/src/main/res/values/strings.xml index bcbfeafd0..5ae5eb962 100644 --- a/TMessagesProj/src/main/res/values/strings.xml +++ b/TMessagesProj/src/main/res/values/strings.xml @@ -69,6 +69,8 @@ CHATS Delete %1$s from suggestions? Link preview + Draft + History was cleared Group Type Channel Type @@ -107,8 +109,6 @@ Blocked users are removed from the group and can only come back if invited by an admin. Invite links don\'t work for them. New Channel Channel name - Comments - If you enable comments, people will be able to discuss your posts in the channel. Add contacts to your channel People can share this link with others and find your channel using Telegram search. People can share this link with others and find your group using Telegram search. @@ -133,11 +133,8 @@ Leave channel Settings JOIN - Channel info Broadcast Silent Broadcast - Comment - show comments What is a Channel? Channels are a new tool for broadcasting your messages to large audiences. CREATE CHANNEL @@ -468,8 +465,8 @@ Convert to supergroup Warning This action is irreversible. It is not possible to downgrade a supergroup to a regular group. - ]]>Members limit reached.]]>\n\nTo go over the limit and get additional features, upgrade to a supergroup:\n\n• Supergroups can get up to %1$s\n• New members can see the full message history\n• Deleted messages will disappear for all members\n• Members can edit their own messages\n• Creator can set a public link for the group - ]]>In supergroups:]]>\n\n• New members can see the full message history\n• Deleted messages will disappear for all members\n• Members can edit their own messages\n• Creator can set a public link for the group + ]]>Members limit reached.]]>\n\nTo go over the limit and get additional features, upgrade to a supergroup:\n\n• Supergroups can get up to %1$s\n• New members can see the full message history\n• Deleted messages will disappear for all members\n• Admins can pin important messages\n• Creator can set a public link for the group + ]]>In supergroups:]]>\n\n• New members can see the full message history\n• Deleted messages will disappear for all members\n• Admins can pin important messages\n• Creator can set a public link for the group ]]>Note:]]> this action can\'t be undone. Share @@ -1038,12 +1035,6 @@ %1$d messages %1$d messages %1$d messages - %1$d comments - %1$d comment - %1$d comments - %1$d comments - %1$d comments - %1$d comments no items %1$d item %1$d items diff --git a/build.gradle b/build.gradle index 525ae5bde..cc551aed8 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:2.1.0' - classpath 'com.google.gms:google-services:2.1.0' + classpath 'com.android.tools.build:gradle:2.1.2' + classpath 'com.google.gms:google-services:2.1.2' } } \ No newline at end of file